RHCE9学习指南 第21章 用bash写脚本

grep的用法是:
grep 关键字 file 意思是从file中过滤出含有关键字的行。
例如,grep root /var/log/messages,意思是从/var/log/messages中过滤出含有root的行。这里很明确的是过滤含有“root”的行。

如果我要是想在/var/log/messages中过滤出含有IP地址的行呢?IP地址就是一类字符,例如,1.1.1.1是一个IP,192.168.26.100也是一个IP,那么我用什么能表示出来这一类字符呢?

不管是通配符和正则表达式都是为了模糊匹配,为了匹配某一类内容,而不是具体的某个关键字。通配符一般用于在shell中,正则表达式一般用于其他语言中。

不管是通配符还是正则表达式,主要是理解他们的元字符,然后用元字符来组合成我们想要的那一类字符,本章我们主要讲解通配符的使用。

像我们平时说的张某某,这个某就是一个元字符,不是一个定值。指的是姓张,名称含有2个字。张某某可能匹配到张二狗也能匹配到张阿猫,但是没法匹配到李阿三。也匹配不了张三,因为张某某匹配的是姓名为3个字的,但是张三这个姓名只有2个字。

如果说有一个人叫姓“张”名“某”,那么需要匹配“张某”这个人,而不是要匹配张三张四,可以用张\某,某前加个“\”表示转义的意思。

21.1 通配符

通配符里一般用于shell语言里,通配符里常见的元字符如下。

(1)[]:匹配一个字符,匹配的是出现在中括号中的字符。
(2)[abc]:匹配一个字符,且只能是a或b或c。
(3)[a-z]:这里“-”有特殊意义,表示“到”的意思,这里表示a-z,即匹配任一字母。
(4)[0-9]:这里表示匹配任一数字。
如果想去除含有特殊意义的字符,前面加“\”表示转义,即去除此字符的特殊意义。
(5)[a\-z]:这里的“-”就没有“到”的意思了,匹配的是“a”或“-”或“z”这三个中的一个。
如果想表示“除了”的意思,则在第一个中括号后面加“!”或“^”。
(6)[!a-z]  [^a-z]:这个表示的是除字母外的其他字符。
(7)?:表示一个任意字符,这里强调是一个,不是0个也是多个,但是不能匹配表示隐藏文件的点。
(8)*:表示任意多个任意字符,可以是0个,可以是1个也可以是多个,也不能匹配表示隐藏文件的点。

练习:

先创建目录xx并在目录中创建如下几个测试文件,命令如下。

[root@server ~]# mkdir xx
[root@server ~]# cd xx
[root@server xx]# touch 1_aa  aa11  Aa11  _aaa  aa.txt  f1aa  u_12
[root@server xx]#

找出首字符是字母,第二个字符是数字的那些文件,命令如下。

[root@server xx]# ls [a-z][0-9]*
f1aa
[root@server xx]#

找出首字符是字母,第二个字符不是数字的那些文件,命令如下。

[root@server xx]# ls [a-z][^0-9]*
aa11  Aa11  aa.txt  u_12
[root@server xx]#

找出第一个字符不是字母,第二个字符不是数字的文件,命令如下。

[root@server xx]# ls [^a-z][^0-9]*
1_aa  _aaa
[root@server xx]#

以上可以看到,找出来的文件完全符合我们的需求。下面找出首字符为大写字母、第二个字符是非数字的文件,命令如下。

[root@server xx]# ls
1_aa  aa11  Aa11  _aaa  aa.txt  f1aa  u_12
[root@server xx]# ls [A-Z][!0-9]*
Aa11  u_12
[root@server xx]#

可以看到,首字母为大写的列出来了,首字符为小写字母的文件有的列出来有的没有列出来,所以[a-z]或[A-Z]这种有时并不精确。如果要更精确可以用如下元字符。

(1)[[:upper:]]:纯大写。
(2)[[:lower:]]:小写。
(3)[[:alpha:]]:字母。
(4)[[:alnum:]]:字母和数字。
(5)[[:digit:]]:数字。

列出首字符是小写的,第二个字符为数字的文件,命令如下。

[root@server xx]# ls [[:lower:]][0-9]*
f1aa
[root@server xx]#

列出首字符为大写字母,第二个字符为数字或字母的文件,命令如下。

[root@server xx]# ls [[:upper:]][[:alnum:]]*
Aa11
[root@server xx]#

如果想在yum源中列出所有以vsftpd开头的包,可以用如下命令。

[root@server xx]# yum list vsftpd*
    ...输出..
可安装的软件包
vsftpd.x86_64                    3.0.3-49.el9                            aa
[root@server xx]#

在当前目录中创建一个文件vsftpdxxx,命令如下。

[root@server xx]# touch vsftpdxxx
[root@server xx]#

然后再执行yum list vsftpd*,如下所示。

[root@server xx]# yum list vsftpd*
    ...输出...
错误:没有匹配的软件包可以列出
[root@server xx]# 

此处显示没有匹配的包,为什么呢?这是因为yum是bash的一个子进程,vsftpd在bash首先被解析成了vsftpdxxx,然后再经过yum。所以,本质上执行的是 yum list vsftpdxxx,而yum源中是没有这个vsftpdxxx这个包的,所以报错。
为了防止bash对这里的
进行解析,可以加上转移符"\",所以下面命令是正确的。

[root@server xx]# yum list vsftpd\*
    ...输出...
可安装的软件包
vsftpd.x86_64                   3.0.3-49.el9        aa
[root@server xx]#

21.2 变量

所谓的变量指的是可变的值,并非具体的值。例如,嘴中发出的“我”,指的是我自己,张三嘴中发出的“我”,指的是张三,那么这个“我”就是一个变量。
变量可以分本地变量、环境变量、位置变量、预定义变量。

21.2.1 本地变量

定义本地变量的格式如下。
变量名=值
定义变量有以下注意几点。

(1)变量名可以包含_、数字、大小写字母,但绝对不可以数字开头。
(2)=两边不能有空格。
(3)“值”如果含有空格,要使用单引号''或双引号""括起来。
(4)定义变量时,变量名前是不需要加$的,引用变量时在需要在变量名前加$。

本章的实验都放在 ~/yy中练习,命令如下。

[root@server xx]# cd 
[root@server ~]# mkdir yy ; cd yy
[root@server yy]#

下面开始练习定义变量,命令如下。

[root@server yy]# 1aa=123
bash: 1aa=123: 未找到命令...
[root@server yy]# 

这里定义变量不正确,因为变量名不能以数字开头,命令如下。

[root@server yy]# aa-1=123
bash: aa-1=123: 未找到命令...
[root@server yy]#

这里定义变量不对,因为变量名只能是字母、数字、下划线来组合,命令如下。

[root@server yy]# aa =123
bash: aa: 未找到命令...
[root@server yy]#

这里的错误是因为等号左边有空格。

[root@server yy]# aa=1 2
bash: 2: 未找到命令...
[root@server yy]#

这里的错误是因为"值"部分如果有空格话,要用引号引起来。

[root@server yy]# aa=123
[root@server yy]#

这是正确的定义了一个变量。
在使用本地变量时,变量名前需要加$符号,命令如下。

[root@server yy]# echo $aa
123
[root@server yy]#

本地变量的特点是:只能影响当前shell,不能影响子shell。

[root@server yy]# echo $aa
123
[root@server yy]# echo $$
620514
[root@server yy]# 

当前shell的pid是620514,下面打开一个子shell。

[root@server yy]# bash
[root@server yy]# echo $$
620956
[root@server yy]# 

这个子shell的PID是620956。

[root@server yy]# echo $aa

[root@server yy]# 

这里可以看到,没有aa变量。

[root@server yy]# exit
exit
[root@server yy]# echo $$
620514
[root@server yy]# echo $aa
123
[root@server yy]#

再次退回到原来的bash,又有了aa变量。情形如图21-1所示。
file
图21-1 本地变量不会作用到子shell

定义变量除刚才显式的定义外,还可以使用如下两种方法。
方法1:把一个命令的结果赋值给一个变量,这个变量要使用“$()”括起来,或者用反引号``引起来。这里是反引号,与波浪号“~”是同一个键,不是单引号。
例如,定义一个名称是ip的变量,对应的值是ens160的IP,命令如下。

[root@server yy]# ip=$(ifconfig ens160 | awk '/inet /{print $2}')
[root@server yy]# echo $ip
192.168.26.101
[root@server yy]#

方法2:通过read获取变量
read的用法如下。
read -p "提示信息" 变量
当遇到read命令时,系统会等待用户输入,用户所输入的值会赋值给read后面的变量,命令如下。

[root@server yy]# read -p "请输入您的名称: " aa 按【Enter】键
请输入您的名称: tom
[root@server yy]# echo $aa
tom
[root@server yy]#

当执行read这条命令时系统会提示用户输入一些内容,所输入的内容会赋值给aa变量。这里我们输入的是tom,所以打印aa变量时,看到的值是tom。
这样的用法比较适合写需要和用户交互的脚本。

21.2.2 环境变量

定义环境变量的注意点和本地变量是一样的。在定义环境时,前面加上export即可,命令如下。

[root@server yy]# export bb=123
[root@server yy]# 

或者先定义为本地变量,然后在通过export转变为环境变量,命令如下。

[root@server yy]# bb=123
[root@server yy]# export bb
[root@server yy]#

要想查看所有的环境变量可以执行env命令。
环境变量的特点是可以影响子shell,这里强调的是子shell,不能影响父shell。

[root@server yy]# echo $$
620514
[root@server yy]# echo $bb
123
[root@server yy]# 

当前shell的PID是620514,里面有一个环境变量aa。

[root@server yy]# bash
[root@server yy]# echo $$
621231
[root@server yy]# echo $bb
123
[root@server yy]# 

打开一个子shell,PID为621231,里面可以看到bb变量的值,说明环境变量已经影响到子shell了。

[root@server yy]# export bb=456
[root@server yy]# exit
exit
[root@server yy]# 

在子shell中重新给bb赋值为456,然后退回到父shell。

[root@server yy]# echo $$
620514
[root@server yy]# echo $bb
123
[root@server yy]# 

可以看到,在父shell中,bb的值仍然是123,说明在子shell中定义的变量不会影响到父shell,如图21-2所示。
file
图21-2 环境变量可以作用到子shell
系统中默认已经存在很多个变量,如下所示。

(1)UID:表示当前用户的UID。
(2)USER:表示当前用户名。
(3)HOME:表示当前用户的家目录。

分别显示这些变量的值,命令如下。

[root@server yy]# echo $UID
0
[root@server yy]# echo $USER
root
[root@server yy]# echo $HOME
/root
[root@server yy]#

有一个很重要的环境变量PATH,当我们执行命令时,一定要指定这个命令的路径,如果没有写路径,则会到PATH中所指定的路径中进行查询。先查看当前用户的PATH变量的值,命令如下。

[root@server yy]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
[root@server yy]#

PATH变量由多个目录组成,每个目录用冒号:分隔,我们把写好的脚本放在PATH变量中指定目录之后,运行此脚本时就不需要指明路径了。
查看tom用户的PATH变量,命令如下。

[tom@server ~]$ echo $PATH
/home/tom/.local/bin:/home/tom/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
[tom@server ~]$

这里展示了tom的PATH变量,如果tom写了一个脚本之后,就可以把这个脚本放在自己家目录下的.local/bin或bin目录下(~/.local/bin或~/bin),如果这两个目录不存在则创建出来即可。
以上定义的环境变量也只是在当前终端中生效,关闭终端之后这个变量也就消失了。如果想让定义的变量永久生效,可以写入家目录的.bash_profile中。因为打开终端时,首先会运行家目录下面的一个隐藏文件 .bash_profile。

21.2.3 位置变量和预定义变量

我们运行脚本时,有时后面是需要加上参数的。但是我们在写脚本时并不能预知后期在脚本后面跟上什么参数。这时就能用到位置变量了,位置变量如下。

$0:表示脚本的名称。
$1:表示第1个参数。
$2:表示第2个参数。
......
${10}:表示第10个参数。
......

这里$后面的数字如果不是个位数,则要用{}括起来。
系统中还内置了一些预定义变量。
$#:表示参数的个数
$*:表示所有的参数

例1:

写一个带参数的脚本,内容如下。

[root@server yy]# cat sc1.yaml
#!/bin/bash
echo "这是我的第一个脚本,脚本的名称是 $0"
echo "第1个参数是: $1"
echo "第2个参数是: $2"
echo "第3个参数是: $3"
echo "此脚本一共有 $# 个参数,它们分别是: $*"
[root@server yy]# 

给这个脚本加上可执行权限,并加参数运行,命令如下。

[root@server yy]# chmod +x sc1.yaml 
[root@server yy]# ./sc1.yaml tom bob mary
这是我的第一个脚本,脚本的名称是 ./sc1.yaml
第1个参数是: tom
第2个参数是: bob
第3个参数是: mary
此脚本一共有 3 个参数,它们分别是: tom bob mary
[root@server yy]# 

运行这个脚本时,共指定了3个参数: tom , bob, mary,他们分别赋值给了$1, $2, $3。这里$#被自动赋值为3,因为总共有3个参数,所有的参数被复制给$*。

例2:

运行如下命令。

[root@server yy]# set a b c d e f g h i j k 
[root@server yy]#

查看此命令的第1个和第3个参数,命令如下。

[root@server yy]# echo $1
a
[root@server yy]# echo $9
i
[root@server yy]#

第1个参数是a,第9个参数是i,下面查看第10个参数,命令如下。

[root@server yy]# echo $10
a0
[root@server yy]#

第9个参数是i,那么第10个参数应该是j才对,这里显示为a0,为什么呢?因为这里先把$10当成了$1+0了,$1的值是a,所以$10的值为a0。
所以,在位置变量中数字超过10,要用{}扩起来,下面的才是正确的。

[root@server yy]# echo ${10}
j
[root@server yy]#

另外在引用变量时,双引号和单引号的区别,直接看个例子。

[root@server yy]# xx=tom
[root@server yy]# echo "my name is $xx"
my name is tom
[root@server yy]# echo 'my name is $xx'
my name is $xx
[root@server yy]#

这里先定义一个变量xx=tom,如果变量在双引号中引用,则变量被解析成具体的值,如果变量出现在单引号中,则不被解析。

21.3 返回值

执行某命令之后,结果不是正确的就是错误的。命令正确执行了,返回值为0,如果没有正确执行则返回值为非零。返回值为非零,不一定是语法错误,执行结果如果有“否定”的意思,返回值也为非零。例如,ping 192.168.26.3,语法没有错误,但是没ping通,返回值也为非零。
返回值记录在$?中的,且$?只记录刚刚执行过命令的返回值。因为$?的值会被新执行命令的返回值覆盖。
练习:
先执行一个命令xxx,命令如下。

[root@server yy]# xxx
bash: xxx: 未找到命令...
[root@server yy]# echo $?
127
[root@server yy]# echo $?
0
[root@server yy]#

先执行一个命令xxx,这个命令是错误的命令,$?记录的是刚刚执行过命令xxx的返回值。所以,查看$?的值是127,是一个非零的值。再次查看$?的值时,却变成了0,因为这个$?记录的不再是xxx命令的返回值,而是它前面执行过的"echo $?" 这个命令的返回值。
逻辑上"否定"的意思也是可以体现出来的,例如,下面的例子。

[root@server yy]# grep ^root /etc/passwd 
root:x:0:0:root:/root:/bin/bash
[root@server yy]# echo $?
0
[root@server yy]# 

这里在/etc/passwd过滤行开头为root的行,结果找到了,所以返回值为0。

[root@server yy]# grep ^rootxxx /etc/passwd 
[root@server yy]# echo $?
1
[root@server yy]#

这里在/etc/passwd过滤行开头为rootxxx的行,结果没找到,这里即使语法没错误,但是逻辑上有"否定"的意思,所以返回值为非零。

21.4 数值运算

在写脚本时,有时我们经常要做一些数学运算。
数学运算的符号如下。

(1)+:表示加。
(2)-:表示减。
(3)*:表示乘。
(4)/:表示除。
(5)**:表示次方。

进行数学运算的表达式有 $(())、$[]、let等,命令如下。

[root@server yy]# echo $((2+3))
5
[root@server yy]# echo $((2*3))
6
[root@server yy]# echo $((2**3))
8
[root@server yy]# echo $[2**3]
8
[root@server yy]#

其中$(())和$[]的用法是一样的,如果不用这样的表达式,看如下所示的代码。

[root@server yy]# echo 2**3
2**3
[root@server yy]#

这里并不是计算的是2的3次方,直接把这4个字符打印出来了。
let也可以用于数学运算,命令如下。

[root@server yy]# let aa=1+2
[root@server yy]# echo $aa
3
[root@server yy]#

这里aa的值就是为3。
下面看下如果不使用let的情况,命令如下。

[root@server yy]# aa=1+2
[root@server yy]# echo $aa
1+2
[root@server yy]#

这里并没有把aa的值1+2当成数字,而是当成了3个字符:"1"、"+"、"2",所以结果显示的也是1+2。
可以实现定义aa为整数类型,然后再做数学运算,命令如下。

[root@server yy]# declare -i aa
[root@server yy]# aa=1+2
[root@server yy]# echo $aa
3
[root@server yy]# 

首先declare -i aa把aa定义为一个整数,所以1+2等于3,然后赋值给aa,所以aa的值为3。
以上的表达式不能求得小数,如果要得到小数需要使用bc命令,用法如下。
echo "scale=N ; 算法 " | bc
这里N是一个数字,表示小数点后面保留几位。
计算2/3,小数点后面保留3位,命令如下。

[root@server yy]# echo "scale=3 ; 2/3" | bc
.666
[root@server yy]# 

这里得到的结果是0.666,这里整数部分的0没有显示。
计算7/6,小数点后面保留3位,命令如下。

[root@server yy]# echo "scale=3 ; 7/6" | bc
1.166
[root@server yy]#

21.5 比较、对比、判断

在写脚本时,有时需要要做一些比较,例如,两个数字谁大谁小,两个字符是否相同等。做对比的表达式有[]、[[]]、test。其中[]和test这两种表达式的作用是相同的。[[]]和[]的不同在于,[[]]中能识别通配符和正则表达式中的元字符,[]却不能。
需要注意的是,在比较时,中括号两边,以及后续提及的比较符两边都要留有空格。

21.5.1 数字的比较

数字的比较,主要是比较两个数字谁大,谁小,或者是否相同。能用到的比较符有以下几种。

(1)-eq:相等。
(2)-ne:不等。
(3)-gt:大于。
(4)-ge:大于等于。
(5)-lt:小于。
(6)-le:小于等于。

做完比较之后,通过返回值来判断比较是否成立。

练习1:

判断1等于2,命令如下。

[root@server yy]# [ 1 -eq 2 ]
[root@server yy]# echo $?
1
[root@server yy]#

1是不能等于2的,所以判断不成立,所以返回值为非零。需要注意的是,[]两边及比较符两边的空格。

练习2:

判断1不等于2,命令如下。

[root@server yy]# [ 1 -ne 2 ]
[root@server yy]# echo $?
0
[root@server yy]#

1不等于2,所以判断成立,这里返回值为0。
21.5.2 字符的比较
字符串的比较,一般比较两个字符串是相同还是不相同,用得较多的比较符有以下两种。
(1)==:相同。
(2)!=:不相同。
做完比较之后,通过返回值来判断比较是否成立。

练习1:

定义一个变量aa=tom,然后做判断,命令如下。

[root@server yy]# aa=tom
[root@server yy]# [ $aa == tom ]
[root@server yy]# echo $?
0
[root@server yy]#

变量aa的值和tom完全相同,所以判断成立,所以返回值为0。

练习2:

在判断中匹配通配符,命令如下。

[root@server yy]# aa=tom
[root@server yy]# [ $aa == to? ]
[root@server yy]# echo $?
1
[root@server yy]#

这里定义aa=tom,按照前面讲过的通配符,to?匹配的应该是前两个字符为to,第三个可以是任一字符的,所以tom应该会被to?匹配到,为什么返回值为非零呢?
原因在于这一对中括号[]中是不能识别通配符的,aa的值是't、o、m'三个字符,而等号后面是't、o、?'这三个字符,并没有把问号当成通配符,所以判断失败。
如果想识别通配符,那么就要用双中括号[[]],看下面的判断。

[root@server yy]# aa=tom
[root@server yy]# [[ $aa == to? ]]
[root@server yy]# echo $?
0
[root@server yy]#

在[[]]中能识别通配符“?”,所以这里判断成立,返回值为0。
注意:

(1)==后面跟的是通配符,如果想跟正则,比较符就不能使用==了,要换成=~。
(2)一定要注意中括号两边的空格,以及比较符两边的空格。

21.5.3 属性的比较

属性的判断,用于判断一个文件是否具备某个属性,常见的属性包括以下7种。

(1)-r:具备读权限。
(2)-w:具备写权限。
(3)-x:具备可执行权限。
注意:以上三个属性,不管是出现在u、g、o上,只要有,就算是判断成立。
(4)-d:一个目录。
(5)-l:一个软连接。
(6)-f:一个普通文件,且要存在。
(7)-e:不管什么类型的文件,只要存在就算判断成立。

练习1:

判断/etc/hosts具备r权限,命令如下。

[root@server yy]# ls -l /etc/hosts
-rw-r--r--. 1 root root 158 9月  10 2018 /etc/hosts
[root@server yy]# [ -r /etc/hosts ]
[root@server yy]# echo $?
0
[root@server yy]#

通过第一条命令可以看到/etc/hosts是具备r权限的,判断/etc/hosts具备r权限,自然成立,所以返回值为0。

练习2:

判断/etc/hosts是否具备可执行权限,命令如下。

[root@server yy]# [ -x /etc/hosts ]
[root@server yy]# echo $?
1
[root@server yy]#

这里判断/etc/hosts具备x权限,但是/etc/hosts不管是u、g、o都不具备x权限,所以判断不成立,所以返回值为非零。
如果做一个否定判断,在前面加上叹号'!'即可。

练习3:

判断/etc/hosts没有x权限,命令如下。

[root@server yy]# [ ! -x /etc/hosts ]
[root@server yy]# echo $?
0
[root@server yy]# 

这里判断/etc/hosts没有x权限,判断是成立的,所以返回值为0。

练习4:

判断/etc/是一个普通文件,命令如下。

[root@server yy]# [ -f /etc ]
[root@server yy]# echo $?
1
[root@server yy]#

我们知道/etc是一个目录不是一个文件,所以这个判断是错误的,所以返回值为非零。

练习5:

判断/etc/不管是什么类型的,只判断存在还是不存在,命令如下。

[root@server yy]# [ -e /etc ]
[root@server yy]# echo $?
0
[root@server yy]#

这里/etc是存在的目录,-e用于判断存在不存在,不判断文件类型,所以这里返回值为0。

21.5.4 使用连接符

前面讲的判断只是单个判断,如果要同时做多个判断,那么就需要使用连接符了。能用的连接符包括"&&" 和"||"。
先看下使用&&作为连接符,用法如下。
判断1 && 判断2
只有两个判断都为真(返回值为0), 整体才是真,只要有一个假,则整体就为假。
判断1如果为假,判断2还有必要执行吗? 没有,因为整体已经确认为假了。
判断1为真,整体是真是假在于判断2,所以判断2肯定是要执行的。

[root@server yy]# [ 1 -le 2 ] && [ 2 -ge 3 ]
[root@server yy]# echo $?
1
[root@server yy]#

这里有两个判断,第一个判断1小于等于2,这个判断成立的,第二个判断是2大于等于3,这个不成立。使用&&做连接符,需要两边的判断都要成立,整体才成立,所以整个判断为假,所以返回值为非零。

[root@server yy]# [ 1 -le 2 ] && [ 2 -le 3 ]
[root@server yy]# echo $?
0
[root@server yy]#

这里有两个判断,第一个判断1小于等于2,这个判断成立的,第二个判断是2小于等于3,这个也成立。使用&&做连接符,需要两边的判断都要成立,整体才成立,所以整个判断为真,所以返回值为非零。
下面看使用||作为连接符,使用||做为连接符的语法如下。
判断1 || 判断2
两个判断只要有一个为真(返回值为0), 整体就是真,只要有全是假,则整体就为假。
判断1为真,整体已经确定为真,所以判断2没有必要要执行的。
判断1为假,整体是真是假在于判断2,所以判断2肯定是要执行的。

[root@server yy]# [ 1 -le 2 ] || [ 2 -ge 3 ]
[root@server yy]# echo $?
0
[root@server yy]#

这里有两个判断,第一个判断1小于等于2,这个判断成立的,整体已经确定为真,所以整个判断为真,所以返回值为非零。

[root@server yy]# [ 1 -ge 2 ] || [ 2 -ge 3 ]
[root@server yy]# echo $?
1
[root@server yy]#

这次做的两个判断,一个是1大于等于2,一个是2大于等于3,这两个判断都为假,所以整体为假,所以返回值为非零。

21.6 if判断语句

在脚本中执行某条命令需要满足一定的条件,如果不满足就不能执行。此时我们就要用到判断语句了。
先看if判断,if判断的语法如下。

if  条件1 ; then
    命令1
elif    条件2 ; then
    命令2
else
    命令3
fi

先判断if后面的判断是不是成立,
如果成立,则执行命令1,然后跳到fi后面,执行fi后面的命令。
如果不成立,则不执行命令1,然后判断elif后面的条件2是不是成立。
如果成立,则执行命令2,然后跳到fi后面,执行fi后面的命令。
如果不成立,则不执行命令2,进行下一轮的elif判断,以此类推。
如果所有if和elif都不成立,则执行else中的命令3。
练习:
写一个脚本/opt/sc1.sh,要求只有root用户才能执行此脚本,其他用户不能执行,命令如下。

[root@server yy]# cat /opt/sc1.sh
#!/bin/bash
if [ $UID -ne 0 ]; then
    echo "只有root才能执行此脚本."
    exit 1
fi
echo "hello root"
[root@server yy]# chmod +x /opt/sc1.sh
[root@server yy]# 

脚本分析:
root的UID是0,其他用户的UID不为0。第一条判断 如果UID不等于0,然后打印警告信息"只有root才能执行此脚本",然后exit退出脚本。

如果这里不加exit,判断之后仍然会继续执行echo "hello root",这样判断就失去了意义。只有加了exit之后,如果不是root,则到此结束,不要继续往下执行了。
如果是lduan执行此脚本,if判断成立,打印完报警信息之后,通过exit退出脚本。
如果是root执行此脚本,则判断不成立,直接执行fi后面的语句。
用root执行此脚本的结果如下。

[root@server yy]# /opt/sc1.sh 
hello root
[root@server yy]#

使用lduan用户执行的结果如下。

[lduan@server ~]$ /opt/sc1.sh 
只有root才能执行此脚本.
[lduan@server ~]$

写一个脚本:写一个脚本/opt/sc2.sh运行脚本时,后面必须要跟上一个参数,参数是系统中的一个文件。
如果这个文件不存在则显示此文件不存在,如果存在,则显示这个文件的行数,命令如下。

[root@server yy]# cat /opt/sc2.sh
#!/bin/bash
if [ $# -eq 0 ]; then
    echo "脚本后面必须要跟一个参数"
    exit 1
fi
if [ -f $1 ] ; then
    wc -l $1
else
    echo "$1不存在"
fi
[root@server yy]# chmod +x /opt/sc2.sh
[root@server yy]#

脚本分析:
$#表示参数的个数,第一个判断中,$# 的值如果等于0则说明脚本后面没有跟任何的参数,打印"脚本后面必须要跟一个参数",然后退出脚本。
如果后面跟了参数,则第一个判断不成立,然后进行下一个if判断。
第一个参数用$1来表示,[ -f $1 ]来判断所跟的参数是不是存在,如果存在则执行 wc -l $1, 如果不存在则执行else中的命令。
运行脚本,效果如下。

[root@server yy]# /opt/sc2.sh
脚本后面必须要跟一个参数
[root@server yy]# 

这次运行没有跟任何的参数,则提示必须要跟一个参数。

[root@server yy]# /opt/sc2.sh /etc/hostsxxx
/etc/hostsxxx不存在
[root@server yy]#

这里跟了一个不存在的文件/etc/hostsxxx,脚本提示这个文件不存在。

[root@server yy]# /opt/sc2.sh /etc/hosts
2 /etc/hosts
[root@server yy]#

这次脚本后面跟了一个存在的文件/etc/hosts,脚本会显示该文件的行数,为2行。

21.7 for循环

有时我们需要做多次重复的操作,例如,创建100个用户,创建一个用户需要两条命令:useradd和passwd。那么,创建100个用户就要重复执行100次,总共执行200条命令,那么此时我们就可以利用for循环简化操作,让系统自动帮我们重复运行即可。
for循环的语法如下。

for 变量 in  值-1  值-2  值-3  值-4 ; do
    命令 $变量
done

这里首先把值-1带给变量,执行do和done之间的命令,所有命令执行完毕之后,再把值-2赋值给变量,执行do和done之间的命令,执行完所有命令之后,再把值-3赋值给变量,以此类推,直到把所有的值都赋值给变量。
看一个简单的例子,如下所示。

[root@server yy]# for i in 1 2 3 4 ; do 此处按【Enter】键
> let i=$i+10
> echo $i
> done
11
12
13
14
[root@server yy]#

这里for后面定义了一个变量i,在in后面指定了4个值,分别是1,2,3,4。在do和done中定义了两个命令,第一个是在变量i的原有值的基础上加上10,然后打印i的值。
先把1赋值给i,此时i的值为1,执行do和done之间的命令。i加上10之后,i的值就变成了11,然后打印i,得到11,第一次循环结束。
然后把2赋值给i,此时i的值为2,执行do和done之间的命令,i加上10之后,i的值变为了12,然后打印i,得到12,第二次循环结束。

21.8 while循环

while也可以循环,while的语法如下。

while 判断 ; do
    命令1
    命令2
done

如果while后面的判断成立,则执行do和done之间的命令,在最后一个命令执行完毕之后,会回头再次判断一下while后面的判断是不是成立。如果不成立了,则跳出循环执行done后面的命令,如果成立则继续执行do和done之间的命令,就这样循环反复下去。
先看一个简单的例子,写一个脚本/opt/sc3.sh,命令如下。

[root@server yy]# cat /opt/sc3.sh
#!/bin/bash
declare -i n=1
while [ $n -le 4 ]  ; do
    echo $n
    let n=$n+1
done
[root@server yy]# chmod +x /opt/sc3.sh
[root@server yy]# 

脚本分析:
这里先通过declare -i n=1 定义了一个整数类型的变量n,初始值为1。然后进入while进行循环,先判断$n的值是不是小于等于4,如果成立,则执行do和done中的语句。
一开始$n的值为1,则[ $n -le 4 ]这个判断成立,则进入do和done之间执行命令。首先打印$n的值,然后在此基础上给n加上1,所以n的值变为了2,这样do和done之间的命令执行完毕了。然后再次到while后面进行判断,此时$n的值为2,依然满足小于等于4,再次执行do和done之间的命令。
如此反复,当$n的值最终能增长到4,然后打印,再然后加1,此时n变成了5。当$n的值变为5了之后,while后面的判断就不再成立了,此时会跳出while循环。
用while也可以用于循环一个文件的内容,用法如下。

while read aa ; do
    命令
done < file

这里read后面的变量aa是可以随意指定的,整体的意思是首先读取file的第一行内容赋值给aa,执行do和done之间的命令。然后读取file的第二行赋值给aa,执行do和done之间的命令,直到读取到file的最后一行。
有时while需要一直循环下去(死循环),语法如下。

while true ; do
    命令
done

或者

while ((1)) ; do
    命令
done
或者
while : ; do
    命令
done

下面写一个脚本,来实时判断vsftpd是否启动,如果没有启动,则将vsftpd启动,命令如下。

[root@server yy]# cat /opt/sc4.sh
#!/bin/bash
while : ; do
    systemctl is-active vsftpd &> /dev/null
    if [ $? -ne 0 ]; then
        systemctl start vsftpd
    fi
    sleep 1
done
[root@server yy]# chmod +x /opt/sc4.sh
[root@server yy]#

这里写了一个while循环,可以一直循环下去,循环中先判断vsftpd是否启动,如果启动了返回值为0,如果没有启动则返回值为非零。
下面开始根据返回值来进行判断,如果$?不等于0,说明vsftpd没有启动,则启动vsftpd服务。sleep 1的意思是暂停1s,这样就实现了每隔1s来判断一次vsftpd是否启动。
下面开始测试这个脚本,先把脚本放在后台运行,命令如下。
[root@server yy]# /opt/sc4.sh &
[1] 48786
[root@server yy]#
测试当前vsftpd的状态,命令如下。
[root@server yy]# systemctl is-active vsftpd
active
[root@server yy]#
关闭vsftpd服务之后,再次检测vsftpd的状态,命令如下。
[root@server yy]# systemctl stop vsftpd
[root@server yy]# systemctl is-active vsftpd
active
[root@server yy]#
可以看到,vsftpd仍然是启动的,说明我们的脚本生效了。

作业
1.请写一个脚本count.sh,用来统计文件的行数,要求:
(1)如果后面没有参数,则报错”必须要跟一个文件”,然后退出脚本
(2)如果跟的不是普通文件或不存在的文件,则提示”必须是一个普通文件”,并退出脚本
(3)如果脚本正常运行,输入格式为:
行数 文件名

2.写一个脚本/opt/ip.sh,用于获取本机ens160的IP地址,要求:
(1)只有root才能执行此脚本,其他用户执行脚本时提示“只有root才能执行此脚本”,并退出脚本。
(2)如果脚本正确执行,则在屏幕上显示一个IP地址。

相关新闻

发表回复

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

                                                                                                                                    RHCE9学习指南连载,点击阅读