目录
10.1.4、* 表示匹配上一个字符0次或多次(不单独使用)
前瞻导读
本文只记录shell脚本中个人认为写法或用法较为特别的知识点,适用于有Java,C等编程语言基础的同学了解和回忆shell知识点。
手机端的反引号会显示为单引号(使用反引号时都有文字说明),需要注意甄别,PC端无异常。
1、shell脚本入门。
打开虚拟机后,我们直接在家目录创建任意名称的文件夹用于存放我们的脚本文件,这里我创建了名为myShell的文件夹,进入文件夹后创建了第一个shell文件取名为hello.sh,具体命令可参考下图。
进入hello.sh后,我们就可以开始编辑文件内容了,首先我们需要指定脚本使用的解释器(因为shell脚本不需要编译,通过解释器解释后就能直接运行,目前常用的解释器有三个,分别是#!/bin/bash,#!/usr/bin/python,#!/bin/awk),由于我们写的是Shell脚本,所以推荐使用#!bin/bash。
然后我们第一个程序的功能就是打印一下"hello world!",具体代码如下:
编写完成后按ESC , shift + ; , wq , 回车。至此,我们的第一个shell文件就完成了,接下来需要执行我们刚刚写的shell文件,首先我们需要确保我们与被执行shell文件处于同一目录,通过以下任意一种命令都可执行。
执行方式一:
bash hello.sh
执行方式二:
sh hello.sh
执行方式三:
source hello.sh
执行方式四:
# 通过当前目录直接执行文件
./hello.sh
但是执行后收到文件权限不够的反馈,所以第一次直接执行文件时需要为文件增加执行的权限,具体如下(增加权限的命令只有第一次执行文件时需要使用,后面再执行该文件时直接通过 ./文件名 调用):
chmod +x hello.sh
2、函数的定义与调用
shell中的函数与其他编程语言类似,都是函数名加大括号,大括号中放着函数体(即这个函数具体执行的操作),具体语法如下:
函数名(){
语句1
语句2
语句3
......
}
调用函数时,直接通过函数名调用函数即可,如果函数需要传参,通过空格将参数隔开即可:
函数名 [参数1] [参数2] [...]
接下来进入实操,进入上一节创建好的myShell目录,创建名为myFirFun.sh的文件:
进入文件后,编写如下代码:
# 定义函数 myFunName(){ # 因为函数调用时传了参数,使用位置变量获取传递的参数,$1表示获取第一个参数 echo "hello world $1" } # 函数调用 myFunName http://baidu.com
保存文件后,我们执行该shell脚本,结果如下:
3、变量
shell脚本中的变量分为环境变量,本地变量,局部变量,位置变量和特殊变量。
环境变量主要是系统变量,包括PATH、LANG、JAVA_HOME等,不做过多赘述。
3.1、本地变量
概念:定义在shell脚本中、函数外的变量(如果函数内的变量没有被local修饰为局部变量,函数被调用后也能被获取到)。
实际操作:
首先,仍然进入家目录下的myShell目录,创建varTest.sh文件:
进入文件后开始编写测试代码,测试代码如下,主要用于测试函数内部变量和函数外部变量获取,函数内部变量又分为函数调用前获取和函数调用后获取:
#!/bin/bash
outVar=11
echo "测试函数外的变量获取: outVar=$outVar"
myFun1(){
innerVar=22
echo "函数内获取变量 innerVar=$innerVar"
}
echo "调用函数前获取变量 innerVar=$innerVar"
#调用函数
myFun1
echo "调用函数后获取变量 innerVar=$innerVar"
测试结果如下:
如图可知,函数调用前函数外无法获取到未被local修饰的函数体内变量。
3.2、局部变量
概念:定义在函数中的变量。
实际操作:
仍然进入家目录下的myShell目录,创建varTest2.sh文件:
进入文件后开始编写测试代码,测试代码如下,主要用于测试函数内调用变量和函数外调用变量是否有区别:
#!/bin/bash
myFun2(){
local localVar=100
echo "函数内调用变量 localVar=$localVar"
}
echo "函数调用前调用变量 localVar=$localVar"
myFun2
echo "函数调用后调用变量 localVar=$localVar"
测试结果如下:
由测试结果可知,通过local修饰后的局部变量只能在函数内被调用,函数外无法调用(与函数是否调用无关)。
3.3、位置变量
概念:当一条命令或脚本执行时,后面可以跟多个参数,我们使用位置参数变量来表示这些参数
格式:$正整数
注意事项:当正整数的位数大于等于两位时,需要用大括号将整数包裹起来。
实际操作:
进入家目录下的myShell目录,创建varTest3.sh文件:
编写如下代码,主要是区分函数内和函数外调用的是那一部分的参数:
#!/bin/bash
# 这里调用的变量是shell文件传进来的参数
echo "第一个参数:$1"
echo "第五个参数:$5"
echo "第十个参数 不用大括:$10"
echo "第十个参数 用大括号:${10}"
myFun3(){
#函数内调用的变量是调用函数时传递过来的参数
echo "第一个参数是:$1"
}
myFun3 001 002 003
测试结果如下:
由测试结果可知,函数外调用的变量是shell文件调用时传递的参数,函数内调用的变量是调用函数时传递过来的变量。
3.4、特殊变量
概念:在Shell脚本中,特殊变量是一组预定义的变量,它们具有特殊的含义和功能。这些变量在脚本执行过程中自动设置和更新,用于提供有关脚本环境和执行上下文的信息。
参数概述:
$#:位置参数的个数
$*:参数列表,双引号引用为一个长字符串, "a b c d e"
$@:参数列表,双引号引用为一个单独的字符串,"a","b","c","d","e"
$?:上一个命令的退出状态(0表示成功,非0表示失败)
实际操作:
进入家目录下的myShell目录,创建varTest4.sh文件:
编写如下代码,主要是通过实际使用加深对特殊变量的记忆:
#!/bin/bash
echo "位置参数的个数:$#"
echo "位置参数*的参数列表:$*"
echo "位置参数@的参数列表:$@"
echo "上一行命令执行后的结果:$?"
运行结果如下:
需要注意的是,$*和$@的结果在打印时没有将双引号打印出来,所以看不出区别,实际上$*是一整个字符串,$@的每一个参数都是一个单独的字符串。
4、数组
格式:数组名=(值1 值2 值3 ...)
注意事项:
1、数组的下标从0开始,依次递进。
2、数组的任何元素都可以用 ${数组名[下标]} 来引用,不能缺少花括号,演示如下:
# 创建一个数组
arr=(a b c d)
# 获取并打印数组第一个元素的两种方式
# 注意这种特殊写法,$数组名默认是第一个元素
echo $arr
echo ${arr[0]}
# 通过指定下标获取数组第二个元素
echo ${arr[1]}
# 获取并打印数组中全部元素
echo ${arr[*]}
echo ${arr[@]}
3、需要对指定的下标值进行修改时,可以通过 数组名[指定下标]=新值 的形式实现,演示如下:
# 创建一个数组
arr2=(a b c d)
# 将第二个元素的值改为B
arr2[1]=B
# 对不存在的元素进行修改值的操作时就等价与给这个不存在的下标赋值
arr2[4]=e
4、需要删除数组中某个元素甚至删除整个数组时可以使用unset命令,演示如下:
# 删除数组中指定的元素
unset 数组名[指定要删除的元素下标]
# 删除整个数组
unset 数组名
unset 数组名[*]
unset 数组名[@]
5、shell符号的特殊用法
5.1、单引号和双引号
概念:双引号属于弱引用,参数可以扩展;单引号属于强引用,不可嵌套;
实际演示:
# 创建变量a
a=101
# 双引号引用变量a,打印的结果为101
echo "$a"
#单引号引用变量a,打印的结果为$a
echo '$a'
5.2、大括号
概念:大括号可用于扩展,节省代码书写,但不能被引用。
实际演示:
# 业务需求一:在当前目录下创建如下几个目录adir,bdir,cdir,ddir,edir,fdir,gdir
mkdir adir bdir cdir ddir edir fdir gdir
# 使用大括号实现业务需求一
mkdir {a,b,c,d,e,f,g}dir
# 业务需求二:将/etc/目录下的如下文件复制到当前目录:sasl2,rpm,udev,yum
# 使用大括号直接复制多个文件
cp /etc/{sasl2,rpm,udev,yum} ./
5.3、反引号
概念:将shell命令的输出结果赋值给变量。
注意点:反引号无法使用脚本中所创建的变量,命令替换会创建子shell进程来运行相应的命令,子shell是由运行该脚本的shell所创建出来的一个独立的子进程,因此该子进程执行的命令无法使用脚本中所创建的变量。
实际演示:
# 业务需求:假设当前处于工作目录/opt/apps,需要将名为network文件远程拷贝到node2节点的/opt/apps目录中
scp network node2:/opt/apps
# 使用反引号简化命令
scp network node2:`pwd`
想实现反引号同样的效果还能通过$()实现,并且小括号内还支持嵌套,使用更灵活。
# 通过反引号创建一个变量
var1=`echo 'hello'`
# 通过$()创建一个变量
var2=$(echo "hello")
# 通过$()实现嵌套创建一个变量
var3=$(echo $(echo "hello"))
6、算术表达式
shell中提供四种表达式的写法,以下用a,b,c三个变量举例方便理解:
6.1、let算术运算表达式
let a=$b+$c
6.2、$[算术表达式]
a=$[$b+$c]
6.3、$((算术表达式))
a=$((b+c))
6.4、expr算术表达式
注意:表达式中各操作数及运算符之间要有空格,同时变量要使用命令引用
a=`expr $b + $c`
7、条件表达式
shell提供了三种条件表达式的书写格式:
1. [ 表达式 ]
2. test 表达式
参数:
-d :判断是否存在该目录 ,后面跟目录名(file)
-f :判断是否存在该文件,后面跟文件名(directory)
-gt :表示大于(Greater than)
-lt :表示小于(Less than)
-ge:表示大于等于(Grater equal)
-le :表示小于等于(Less equal)
-eq :表示等于(取Equal的前两个字母)
3. [ [ 表达式 ] ]
在比较两个数的大小时,如果我们直接在命令行中使用> < = 等符号判断两个数的大小,结果可能会和预期有一定的出入:
因此,我们在比较大小时一般用命令去代替符号。
注意:如果写表达式时使用[ 表达式 ]的格式去写时,一定要将表达式和两边的方括号之间留出空格,如果连在一起会报错。
8、分支结构
8.1、if语句
8.1.1、单分支结构
# 相比其他编程语言小括号变中括号,用then和fi代替大括号
# 放条件的中括号两边都要注意空格,否则会报错
if [ 条件判断 ]
then
命令
fi
# 下面是模拟then紧跟在条件判断后时一定要加分号方便程序判断
if [ 条件判断 ]; then
条件成立执行,命令;
fi
8.1.2、双分支结构
if [ 条件1 ];then
条件1成立执行,指令集1
else
条件1不成执行指令集2;
fi
8.1.3、多分支结构
需要注意到是,在所有的if分支结构中只有一个fi,then的个数由除else分支之外其他分支的数目决定(if后面紧跟分号和then,这里的if包括elif),这是与其他编程语言差异较大的地方。
if [ 条件1 ];then
条件1成立,执行指令集1
elif [ 条件2 ];then
条件2成立,执行指令集2
else
条件都不成立,执行指令集3
fi
实战巩固:
进入家目录下的myShell目录,创建if.sh文件:
编写如下代码,主要实现调用脚本时传递一个参数,脚本拿到参数后与4作比较,并打印比较的结果,案例简单,方便练习格式。
#!/bin/bash
if [ $1 -gt 4 ];then
echo "$1大于4"
elif [ $1 -eq 4 ];then
echo "$1等于4"
else
echo "$1小于4"
fi
运行脚本结果如下:
8.2、case语句
case语句可以理解为一个没有switch语句的switch语句,因为在C,JAVA等编程语言中,switch只是触发程序体的入口,具体执行哪一部分还是由程序体内的case做判断,shell的case格式如下:
case $变量名称 in “值1")
程序段1
;;
“值2")
程序段2
;;
*)
exit 1
;;
esac
实战巩固:
进入家目录下的myShell目录,创建case.sh文件:
编写如下代码,主要是实现对用户输入数字的判断并返回对应数字的星期数,周一到周五打印星期一到星期五,周六到周日打印周末,如果是其他数字打印错误提示信息。
#!/bin/bash
# read -p 实现的功能是显示引号内的信息并获取用户输入的结果,再将获取的结果赋值给后面紧跟的变量num
read -p "请输入1-7之间任意的整数:" num
case $num in 1)
echo "星期一" ;;
2)
echo "星期二" ;;
3)
echo "星期三";;
4)
echo "星期四";;
5)
echo "星期五";;
[6-7])
echo "周末";;
*)
echo "非法字符,请输入任意1到7的整数";;
esac
测试结果如下:
9、循环语句
9.1、while语句
语句格式:
这两种写法的区别在于do的位置,如果do要与条件判断写在一行,需要在判断条件(也就是中括号包裹的条件)与do之间加一个分号,方便让程序知道这是两个命令(分号表示一条语句的结束)。
while [ condition ] ;do
命令
done
#或者
while [ condition ]
do
命令
done
实战巩固:
案例一:
进入家目录下的myShell目录,创建whileTest1.sh文件:
编写如下代码,主要是实现每两秒打印系统运行时间和负载情况:
#!/bin/bash
# 由于在此案例中我们使用了true,表示程序会一直运行,可以通过ctrl+c实现程序终止
while true
do
# 打印系统运行时间和负载情况
uptime
# 休眠两秒
sleep 2
done
测试结果如下:
案例二:
进入家目录下的myShell目录,创建whileTest2.sh文件:
编写如下代码,主要实现1到100之间所有数累加的计算(新手学shell一定要自己敲一遍这个例子,不仅能练习while语句的格式,还能练习算术表达式和条件表达式的内容):
#!/bin/bash
sum=0
num=1
while [ $num -le 100 ]
do
# 求和
let sum=$num+$sum
# 实现条件值的累加
num=$[$num+1]
done
echo "1到100累加的和为:$sum"
测试结果如下:
9.2、for语句
9.2.1、for语句格式及练习
语句格式
条件判断部分和case的用法很像,程序体的开始和结束与while一样
for 变量名 in 变量取值列表
do
命令
done
实战巩固:
进入家目录下的myShell目录,创建forTest1.sh文件:
编写如下代码,主要是实现打印192.168.1.1~192.168.1.18这18个ip地址:
#!/bin/bash
for num in {1..18}
do
echo "192.168.1.$num"
done
该例取巧的是使用了{数字1..数字2}的方式简化了程序,如果数字1是10,数字2是19,{数字1..数字2}的内容就是从10开始,到19结束的十个整数;如果数字1是19,数字2是15,{数字1..数字2}的内容就是19到15的5个整数。因此,{数字1..数字2}可以简化大量依次数字的写法,且支持内容的正序和反序
运行结果如下:
但是在实际开发中,很少有这样逐个数据获取的需求,为了更加灵活的操作数据集中不同位置的获取,我们接下来学习seq,提高数据选择的灵活性。
9.2.2、seq语句
语句格式:
# 分隔符一定要用英文引号引起来
seq –s 分隔符 起始 步长 终点
# 这里是以空格为分隔符,从1开始,步长为1,到4结束
seq -s " " 1 1 4
实战练习:
进入家目录下的myShell目录,创建forTest2.sh文件:
编写如下代码,主要是实现打印20到45之间所有的偶数:
#!/bin/bash
echo "20到45的偶数有:"
for num in $(seq -s " " 20 2 45)
do
echo "$num"
done
在这个案例中,seq还可以通过反引号的形式出现:`seq -s " " 20 2 45`
测试结果如下:
10、正则表达式
概念:正则表达式使用单个字符串来描述、匹配一系列符合某个语法规则的字符串。在文本编辑器里,正则表达式通常被用来检索、替换符合某个模式的文本。在Linux中sed,grep,awk,vim等命令都支持通过正则表达式进行模式匹配。
准备测试数据文件:
进入家目录下的myShell目录,创建data01.sh文件:
将以下数据复制粘贴到文件中节省时间:
xk
zk
ok
yk
zzk
zxzxk
bxx
cxx
dxx
hello world
are you ok?
areyou ok?
areyou are youok?
aaare you ok?
aare you ok
aaaare you ok
abcre you ok?
xxre you ok
are yyyou ok?
zk kz 1
kz zk 2
okk koo 3
zkkz
kzzk
areyou are youok?
10.1、基本匹配规则
10.1.1、 . 匹配任意单个字符
实战练习:查找/etc/passwd文件中以f开头,p结尾,中间包含任意字符的内容信息
# 写法一
cat /etc/passwd | grep f.p
测试结果如下:
也可以直接通过grep配合正则表达式查找:
# 写法二
grep f.p /etc/passwd
测试结果如下:
10.1.2、^ 匹配一行的开头
实战练习一:在第十章开始时我们准备了数据集data01.sh,接下来我们需要查找文件中所有以a开头的内容。
# 这里的路径可能有差别,这里我使用的是root账户
# 大家可以在自己新建的数据集文件位置使用pwd确定自己的路径,也可以使用相对路径引用数据集文件
# 写法一
cat /root/myShell/data01.sh | grep ^a
#写法二
grep ^a /root/myShell/data01.sh
测试结果如下:
实战练习二:查找数据集data01.sh中以任意字符开始,后面紧跟k的内容。(将.与^相结合的案例)
# 写法一
cat /root/myShell/data01.sh | grep ^.k
# 写法二
grep ^.k /root/myShell/data01.sh
测试结果如下:
10.1.3、$ 匹配一行的结尾
实战练习一:查找数据集data01.sh中以k结尾的内容。
# 写法一
cat /root/myShell/data01.sh | grep k$
# 写法二
grep k$ /root/myShell/data01.sh
测试结果如下:
实战练习二:查找/etc/profile文件中所有的空行。
# 写法一
cat /etc/profile | grep ^$
# 写法二
grep ^$ /etc/profile
测试结果如下:
10.1.4、* 表示匹配上一个字符0次或多次(不单独使用)
实战练习一:查找/etc/group文件中ro和t之间有任意个o的内容信息。
# 写法一
cat /etc/group | grep ro*t
# 写法二
grep ro*t /etc/group
测试结果如下:
实战练习二:查找数据集data01.sh中a和re之间有任意个a的内容。
# 写法一
cat /root/myShell/data01.sh | grep a*re
# 写法二
grep a*re /root/myShell/data01.sh
测试结果如下:
10.1.5、[ ] 表示匹配某个范围内的一个字符
知识点总结:
[数字1,数字2] :匹配包含数字一或者数字二的信息。(注意:数字之间用","分隔)
[数字1-数字2] :匹配包含数字1到数字2之间的任意一个数字的信息。(包含数字1和数字2)
[字母1字母2] :匹配包含字母1字母2中的任意一个字母的信息。(注意:与数字的区别是不需要用逗号分隔)
[字母1-字母2]: 匹配包含字母1到字母2之间的任意一个字母的信息。
实战练习一:查找数据集data01.sh中包含b到e字母信息的内容。
# 写法一
cat /root/myShell/data01.sh | grep [b-e]
# 写法二
grep [b-e] /root/myShell/data01.sh
测试结果如下:
实战练习二:查找数据集data01.sh中包含xk或者zk的信息
# 写法一
cat /root/myShell/data01.sh | grep [zx]k
# 写法二
grep [zx]k /root/myShell/data01.sh
测试结果如下:
实战练习三:查找数据集data01.sh中除xk,zk以外任意字符后紧跟k的信息。
# 写法一
cat /root/myShell/data01.sh | grep [^xz]k
# 写法二
grep [^xz]k /root/myShell/data01.sh
注意:在正则表达式[ ]中,^并不表示开头,而表示非(可以理解为除了…之外)
测试结果如下:
10.1.6、\ 表示转义(不单独使用)
实战练习一:查找/etc/profile文件中包含$PATH的内容信息。
# 写法一
cat /etc/profile | grep \$PATH
# 写法二
grep \$PATH /etc/profile
注意:如果不加转义字符,$PATH表示名为PATH的变量,加了转义字符表示名为$PATH的内容。
测试结果如下:
10.1.7、< > 表示单词首尾边界
重点:
1、<表示左边界,>表示右边界,都需要配合转义字符使用,且都需要被包裹在引号内才会生效。
2、可以单独使用"\<字符"表示左边界,也可以单独使用"字符\>"表示右边界。
实战练习:查找数据集data01.sh中以are开头并且以are结尾的内容。
# 写法一
cat /root/myShell/data01.sh | grep "\<are\>"
# 写法二
grep "\<are\>" /root/myShell/data01.sh
测试结果如下:
10.2、扩展匹配规则
知识点总结:
1、grep默认工作于基本模式,-E选项让grep工作于扩展模式;
2、?,+,{,|,(和),这些符号在扩展模式不需要加反斜杠就有特殊含义
- ? 匹配0到1次
- +匹配1到多次
- {n} 匹配n次
- {n,} 匹配n到多次
- {m,n} 匹配m到n次
- | 表示并集,将左右两边的结果都显示出来
实战练习一:查找数据集data01.sh中以are或you开头并且以are或you结尾的内容
# 写法一
cat /root/myShell/data01.sh | grep -E "\<are\> | \<you\>"
# 写法二
grep -E "\<are\> | \<you\>" /root/myShell/data01.sh
测试结果如下:
实战练习二:查找数据集data01.sh中字符re前面至少包含一个a的内容。
# 写法一:基本模式
grep "a\+re" /root/myShell/data01.sh
# 写法二:工作模式
grep -E "a+re" /root/myShell/data01.sh
测试结果如下:
11、shell脚本检查
知识点汇总:
shell的脚本检查是通过以下语句实现:sh [-nvx] 待检查的脚本文件
• -n :不执行script,仅查询语法的问题;
• -v :在执行script前,先将scripts的内容输出到屏幕上;
• -x :将使用到的script内容显示到屏幕上,这是很有用的参数;
12、真题练习
12.1、Linux中如何将以a开头的文件找出来?
# 当前目录下查找
ll a*
# 整个文件系统中查找
find / -name a*
12.2、通过Shell脚本检查一个文件是否存在?
首先在家目录下的myShell目录,创建train1.sh文件:
编写实现代码:
#!/bin/bash
if [ -f $1 ];then
echo "$1存在"
else
echo "$1不存在"
fi
测试结果如下: