Linux Shell核心编程指南
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2 字符串的判断与比较

下面的表达式使用test或[]测试的效果是一样,表达式中可以使用变量。

[root@centos7~]# test a == a                  #测试字符串是否相等

使用$?查看上一条命令的退出码,0代表正确(true),非0代表错误(false)。

[root@centos7~]# echo $?
0
[root@centos7~]# test a == b ; echo $?
1
[root@centos7~]# test a ! = b ; echo $?        #测试字符串是否不相等
0
[root@centos7~]# [ $USER == root ]; echo $?    #与使用test命令测试结果一致
0

下面的测试,因为当前用户是root,测试结果为真,所以会执行echo Y命令,而当echo Y命令执行并成功后,则不再执行echo N,结果屏幕仅显示Y。

[root@centos7~]# [ $USER == root ] && echo Y || echo N
Y

下面测试当前用户不是root,然而因为当前用户是root,所以结果为假,不会执行echo Y命令,而当echo Y命令未执行时,则执行echo N命令,最终屏幕仅输出显示N。

[root@centos7~]# [ $USER ! = root ] && echo Y || echo N
N

在表达式中使用-z可以测试一个字符串是否为空,下面测试一个未定义的变量TEST,如果变量值为空则屏幕显示Y,否则显示N。

[root@centos7~]# [ -z $TEST ] && echo Y || echo N
Y
[root@centos7~]# TEST=123456                      #定义变量并赋值
[root@centos7~]# [ -z $TEST ] && echo Y || echo N
N

在Shell中进行条件测试时一定要注意空格问题。使用[]测试时,左方括号右边和右方括号左边都必须有空格。而且测试的比较符号两边也必须都有空格。

[root@centos7~]# [test == beijing]            #前后括号都缺少空格,系统报错
bash: [test: command not found...

下面这个例子==符号两边没有空格,无论怎么测试结果都为真,编写脚本时这种Bug系统不会提示语法错误,但程序结果有可能是错误的。

[root@centos7~]# [ test==root ]; echo $?
0
[root@centos7~]# [ 1==2 ]; echo $?
0

我们还可以使用-z测试一个字符串是否非空(变量值不为空)。但是在实际应用时最好将测试对象使用双引号引起来。

[root@centos7~]# TEST=123456
[root@centos7~]# [ -n $TEST ] && echo Y || echo N  #TEST非空显示Y,否则显示N
Y

这样看起来没什么问题。但是,当测试一个未定义的变量时就会出故障。下面测试一个未定义的变量Jacob是否非空。为什么Jacob的度量值明明为空,但测试却说该变量值不为空呢?

[root@centos7~]# [ -n $Jacob ] && echo Y || echo N
Y

因为,当$Jacob为空时,等同于执行了下面的第一条命令,是在测试一个空格是否为空值。而计算机理解空格也是有值的,并非没有值(空值),所以这样的测试结果总为真;但程序的逻辑其实已经出错了。为了防止类似这种错误,可以将变量使用双引号.单引号会屏蔽特殊符号,会将$变成一个普通字符,所以这里不能使用单引号。引起来。

[root@centos7~]# [ -n ] && echo Y || echo N
Y
[root@centos7~]# [ -n "$Jacob" ] && echo Y || echo N
N

2.3 整数的判断与比较

比较两个数字可能的结果有等于、不等于、大于、大于或等于、小于、小于或等于这么几种情况,在Shell脚本中支持对整数的比较判断,可以使用如表2-1所示的符号进行比较运算。

表2-1 整数的比较运算符

[root@centos7~]# test 3 -eq 3 && echo Y || echo N         #3等于3吗
Y
[root@centos7~]# test 3 -ne 3 && echo Y || echo N         #3不等于3吗
N
[root@centos7~]# [ 6 -gt 4 ] && echo Y || echo N          #6大于4吗
Y
[root@centos7~]# [ 6 -gt 9 ] && echo Y || echo N          #6大于9吗
N
[root@centos7~]# [ 6 -ge 4 ] && echo Y || echo N          #6大于或等于4吗
Y
[root@centos7~]# [ 6 -ge 6 ] && echo Y || echo N          #6大于或等于6吗
Y
[root@centos7~]# [ 6 -lt 9 ] && echo Y || echo N          #6小于9吗
Y
[root@centos7~]# [ 6 -lt 3 ] && echo Y || echo N          #6小于3吗
N
[root@centos7~]# [ 6 -lt 6 ] && echo Y || echo N          #6小于6吗
N
[root@centos7~]# [ 6 -le 3 ] && echo Y || echo N          #6小于或等于3吗
N
[root@centos7~]# [ 6 -le 6 ] && echo Y || echo N          #6小于或等于6吗
Y
[root@centos7~]# [ $UID -eq 0 ] && echo Y || echo N      #当前用户是root吗
Y

下面这个案例使用grep命令结合正则表达式,从meminfo文件中过滤当前系统剩余可用的内存容量,剩余容量以KiB为单位,最后测试剩余可用容量是否小于或等于500MiB。

[root@centos7~]# grep Available /proc/meminfo  | egrep -o "[0-9]+"
814312

提示

grep命令使用-o选项可以仅显示匹配内容,而不显示全行所有内容。

[root@centos7~]# mem_free=$(grep Available /proc/meminfo  | egrep -o
"[0-9]+")
[root@centos7~]# [ $mem_free -le 512000 ] && echo Y || echo N
N

接下来使用ps命令,查看系统中所有启动的进程列表信息,结合wc命令还可以统计当前系统中已经启动的进程数量。这样,就可以判断是否启动了超过100个进程。

[root@centos7~]# ps aux | wc -l
118
[root@centos7~]# procs=$(ps aux | wc -l)
[root@centos7~]# [ $procs -gt 100 ] && echo Y || echo N
Y

最后,可以使用wc命令统计/etc/passwd文件的行数(有多少行就表示系统中有多少个账户),判断当前系统账户数量是否大于或等于30个。

[root@centos7~]# num=$(cat /etc/passwd | wc -l)
[root@centos7~]# [ $num -ge 30 ] && echo Y || echo N
Y

2.4 文件属性的判断与比较

Shell支持大量对文件属性的判断,常用的文件属性操作符很多,如表2-2所示。更多文件属性操作符可以参考命令帮助手册(man test)。

表2-2 文件属性操作符

首先,需要创建几个用于演示的文件和目录,注意创建文件时最好间隔一定的时间,让两个文件的最后修改时间有点间隔。

[root@centos7~]# touch ver1.txt
[root@centos7~]# touch ver2.txt
[root@centos7~]# mkdir test
[root@centos7~]# [ -e ver1.txt ] && echo|| echo 错   #判断文件是否存在

[root@centos7~]# [ -e test ] && echo|| echo 错       #判断目录是否存在

[root@centos7~]# [ ! -e ver1.txt ] && echo|| echo 错 #判断文件是否不存在

[root@centos7~]# [ -f ver1.txt ] && echo|| echo 错   #判断存在,且为文件

[root@centos7~]# [ ! -f ver1.txt ] && echo|| echo 错 #判断该文件不存在

[root@centos7~]# [ -f test/ ] && echo|| echo 错      #因为不是文件,结果错

[root@centos7~]# [ -d test/ ] && echo|| echo 错      #判断存在,且为目录

[root@centos7~]# [ -d ver1.txt ] && echo|| echo 错   #因为不是目录,结果错

下面这个测试,假设系统中有某个磁盘设备,使用-b测试该设备是否存在,且当该设备为块设备时返回值为真,否则返回值为假。

[root@centos7~]# [ -b /dev/sda ] && echo|| echo 不是

[root@centos7~]# [ -b /etc/passwd ] && echo|| echo 不是
不是

Linux系统中的文件链接分为软链接和硬链接两种。软链接创建后,如果源文件被删除,则软链接将无法继续使用,可以跨分区和磁盘创建软链接。硬链接创建后,如果源文件被删除,则硬链接依然可以正常使用、正常读写数据,但硬链接不可以跨分区或磁盘创建。另外,硬链接与源文件使用的是相同的设备、相同的inode编号。使用ls -l这里是小写字母L。命令查看硬链接文件的属性时,文件属性与普通文件是一样的,而软链接的文件属性则可以看到被l标记,表示该文件为软链接。

[root@centos7~]# ln -s /etc/hosts /root/soft              #创建软链接
[root@centos7~]# ln /etc/hosts /root/hard                 #创建硬链接
[root@centos7~]# ls -l /root/soft
lrwxrwxrwx. 1 root root 10 9月 16 20:52 /root/soft -> /etc/hosts
[root@centos7~]# ls -l /root/hard
-rw-r--r--. 3 root root 158 9月 15 15:24 /root/hard
[root@centos7~]# [ -L /root/soft ] && echo|| echo 不是 #判断是否为软链接

[root@centos7~]# [ ! -L /root/soft ] && echo|| echo 不是 #判断不是软链接

[root@centos7~]# [ -L /root/hard ] && echo|| echo 不是
不是
[root@centos7~]# [ /root/hard-ef /etc/hosts ] && echo Y || echo N
Y

在测试权限时需要注意,超级管理员root在没有rw权限的情况下,也是可以读写文件的,rw权限对超级管理员是无效的。但是如果文件没有x权限,哪怕是root也不可以执行该文件。

[root@centos7~]# ls -l ver1.txt
-rw-r--r--. 1 root root 0 9月 13 17:42 ver1.txt
[root@centos7~]# [ -r ver1.txt ] && echo Y || echo N
Y
[root@centos7~]# chmod -r ver1.txt                        #删除r权限
[root@centos7~]# [ -r ver1.txt ] && echo Y || echo N      #测试结果依然为真
Y
[root@centos7~]# [ ! -r ver1.txt ] && echo Y || echo N   #测试不可读
N
[root@centos7~]# chmod -w ver1.txt                        #删除w权限
[root@centos7~]# ls -l ver1.txt
----------. 1 root root 0 9月 16 20:31 ver1.txt
[root@centos7~]# [ -w ver1.txt ] && echo Y || echo N      #测试结果依然为真
Y
[root@centos7~]# [ -x ver1.txt ] && echo Y || echo N      #测试结果为假
N
[root@centos7~]# chmod +x ver1.txt                        #添加x权限
[root@centos7~]# ls -l ver1.txt
---x--x--x. 1 root root 0 9月 16 20:31 ver1.txt
[root@centos7~]# [ -x ver1.txt ] && echo Y || echo N
Y

默认touch命令创建的文件都是空文件,在使用-s测试文件是否为非空文件时,因为文件是空文件,所以测试结果为假。当文件中有内容时,测试文件是否为非空时,结果为真。

[root@centos7~]# [ -s ver1.txt ] && echo Y || echo N
N
[root@centos7~]# echo "hello" > ver1.txt
[root@centos7~]# [ -s ver1.txt ] && echo Y || echo N
Y

前面在创建ver1.txt和ver2.txt文件时,故意让两个文件创建的时间有所不同,现在可以使用测试条件判断两个文件的创建时间,看看哪个文件是新文件,哪个文件是旧文件。new than表示更新,old than表示更旧。根据下面的输出结果可知,ver2.txt文件比ver1.txt文件更新。

[root@centos7~]# ls -l ver*.txt
-rw-r--r--. 1 root root 0 9月 13 17:42 ver1.txt
-rw-r--r--. 1 root root 0 9月 13 17:43 ver2.txt
[root@centos7~]# [ ver1.txt -nt ver2.txt ] && echo Y || echo N
N
[root@centos7~]# [ ver2.txt -nt ver1.txt ] && echo Y || echo N
Y
[root@centos7~]# [ ver1.txt -ot ver2.txt ] && echo Y || echo N
Y
[root@centos7~]# [ ver2.txt -ot ver1.txt ] && echo Y || echo N
N

2.5 探究[[]]和[]的区别

多数情况下[]和[[]]是可以通用的,两者的主要差异是:test或[]是符合POSIX标准的测试语句,兼容性更强,几乎可以运行在所有Shell解释器中,相比较而言[[]]仅可运行在特定的几个Shell解释器中(如Bash、Zsh等)。事实上,目前支持使用[[]]进行条件测试的解释器已经足够多了。使用[[]]进行测试判断时甚至可以使用正则表达式。

首先,测试两者的通用表达式(同样,一定要注意空格的问题,空格不可或缺)。

[root@centos7~]# [[ 5 -eq 5 ]] && echo Y || echo N
Y
[root@centos7~]# [[ 5 -ne 5 ]] && echo Y || echo N
N
[root@centos7~]# [[ 5 -gt 8 ]] && echo Y || echo N
N
[root@centos7~]# [[ 5 -lt 8 ]] && echo Y || echo N
Y
[root@centos7~]# [[ -r /etc/hosts ]] && echo Y || echo N
Y
[root@centos7~]# [[ ver1.txt -nt ver2.txt ]] && echo Y || echo N
N
[root@centos7~]# [[ ver1.txt -ot ver2.txt ]] && echo Y || echo N
Y

然后,看两者的差异点。其中,在[[]]中使用<和>符号时,系统进行的是排序操作,而且支持在测试表达式内使用&&和||符号。在test或[]测试语句中不可以使用&&和||符号。

注意

[[ ]]中的表达式如果使用<或>进行排序比较,使用的是本地的locale语言顺序。

可以使用LANG=C设置在排序时使用标准的ASCII码顺序。

在ASCII码的顺序中,小写字母顺序码>大写字母顺序码>数字顺序码。

[root@centos7~]# LANG=C                            #防止其他语系导致排序混乱
小写字母顺序码大于大写字母顺序码,对
[root@centos7~]# [[ b > A ]] && echo Y || echo N#
Y
#大写字母顺序码大于数字顺序码,对
[root@centos7~]# [[ A > 6 ]] && echo Y || echo N
Y
#大写字母顺序码大于小写字母顺序码,错
[root@centos7~]# [[ A > b ]] && echo Y || echo N
N
#数字顺序码大于大写字母顺序码,错
[root@centos7~]# [[ 8 > C ]] && echo Y || echo N
N
[root@centos7~]# [[ 8 > 2 ]] && echo Y || echo N      #8大于2,对
Y
[root@centos7~]# [[ 2 > 8 ]] && echo Y || echo N      #2大于8,错
N
[root@centos7~]# [[ T > F ]] && echo Y || echo N      #T大于F,对
Y
[root@centos7~]# [[ m > c ]] && echo Y || echo N      #m大于c,对
Y

虽然[]也支持同时进行多个条件的逻辑测试,但是在[]中需要使用-a和-o进行逻辑与和逻辑或的比较操作,而[[]]中可以直接使用&&和||进行逻辑比较操作,更直观,可读性更好。

A && B或者A -a B,意思是仅当A和B两个条件测试都成功时,整体测试结果才为真。

A || B或者A -o B,意思是只要A或B中的任意一个条件测试成功,则整体测试结果为真。

[root@centos7~]# [ yes == yes -a no == no ] && echo Y || echo N
Y
[root@centos7~]# [ yes == y -a no == no ] && echo Y || echo N
N
[root@centos7~]# [ yes == y -o no == no ] && echo Y || echo N
Y
[root@centos7~]# [ yes == yes -o no == no ] && echo Y || echo N
Y
[root@centos7~]# [[ yes == yes && no == no ]] && echo Y || echo N
Y
[root@centos7~]# [[ yes == y && no == no ]] && echo Y || echo N
N
[root@centos7~]# [[ yes == yes || no == no ]] && echo Y || echo N
Y
[root@centos7~]# [[ yes == y || no == no ]] && echo Y || echo N
Y
[root@centos7~]# [[ A == A && 6 -eq 6 && C == C ]]
[root@centos7~]# echo $?                               #返回0表示正确
0
[root@centos7~]# [[ A == A && 6 -eq 3 && C == C ]]
[root@centos7~]# echo $?                               #返回非0表示错误
1

需要注意的还有==比较符,在[[]]中==是模式匹配,模式匹配允许使用通配符。例如,Bash常用的通配符有*、? 、[…]等。而==在test语句中仅代表字符串的精确比较,判断字符串是否一模一样。

下面的例子,测试变量name的值是否以字母J开头,后面可以是任意长度的任意字符,测试结果为真。

[root@centos7~]# name=Jacob
[root@centos7~]# [[ $name == J* ]] && echo Y || echo N
Y

接着,测试变量name的值是否以字母A开头,后面可以是任意长度的任意字符,测试结果为假。

[root@centos7~]# [[ $name == A* ]] && echo Y || echo N
N

测试变量name的值是否是J和cob中间有任意单个字符?结果为真。

[root@centos7~]# [[ $name == J? cob ]] && echo Y || echo N
Y

测试字符a,是否是小写字母?结果为真。

[root@centos7~]# [[ a == [a-z] ]] && echo Y || echo N
Y

测试字符a,是否是数字?结果为假。

[root@centos7~]# [[ a == [0-9] ]] && echo Y || echo N
N

同样是使用==进行比较操作,但在[]中系统进行的是字符串的比较操作,判断两个字符串是否绝对相同。

[root@centos7~]# [ $name == J? ] && echo Y || echo N
N
[root@centos7~]# [ $name == J* ] && echo Y || echo N
N
[root@centos7~]# name='J*'
[root@centos7~]# [ $name == 'J*' ] && echo Y || echo N
Y
[root@centos7~]# [[ a == a ]] && echo Y || echo N
Y
[root@centos7~]# [ a == b ] && echo Y || echo N
Y

另外,在[[]]中还支持使用=~进行正则匹配,而在[]中则完全不支持正则匹配。

[root@centos7~]# name="welcome to beijing"

对变量name的值进行正则匹配,判断name的值是否包含字母w。

[root@centos7~]# [[ $name =~ w ]] && echo Y || echo N
Y

对变量name的值进行正则匹配,判断name的值是否包含数字。

[root@centos7~]# [[ $name =~ [0-9] ]] && echo Y || echo N
N

对变量name的值进行正则匹配,判断name的值是否包含小写字母。

[root@centos7~]# [[ $name =~ [a-z] ]] && echo Y || echo N
Y

对变量name的值进行正则匹配,判断name的值是否包含大写字母。

[root@centos7~]# [[ $name =~ [A-Z] ]] && echo Y || echo N
N

最后,看看分组测试。使用()进行分组,效果类似于虽然在数学上默认先算乘除法再算加减法,但使用()后可以先算加减法再算乘除法。

下面这条命令,在a==a为真的情况下,b==b和c==d两个测试中只要有一个测试为真,则整体测试为真。

[root@centos7~]# [[ a == a && (b == b || c == d) ]] && echo Y || echo N
Y

下面这条命令,在a==a为真的情况下,只有b==b和c==d两个测试都为假,整体测试才为假。

[root@centos7~]# [[ a == a && (b == g || c == d) ]] && echo Y || echo N
N

表2-3中列出了[[]]和[]的差异汇总信息,相同点这里不再赘述。为了熟练掌握这些语法,本书后面的案例中将混合应用多种不同的条件测试语句。

表2-3 [[]]和[]的对比