Bash脚本编程学习笔记04:测试命令test、状态返回值、位置参数和特殊变量
我自己接触Linux主要是大学学习的Turbolinux --> 根据《鸟哥的Linux私房菜:基础篇》(第三版) --> 马哥的就业班课程。给我的感觉是这些课程对于bash的讲解,理论上是不够的,但是限于时间、篇幅和精力,确实无法讲解的足够深入。在接触了骏马金龙的博客以及bash官方站点后,就会理解骏马兄说的“平常我们学的只是bash的形,而不是bash的神”。最近在写这个系列的学习笔记,经常查阅bash官方手册,真的是有种醍醐灌顶的感觉,但是限于能力和进度问题,有些问题暂且无法做到深入理解,只能给出治标的方法。
学习test命令以及后续的选择语句if和case,需要注意的手册有这些:
组合命令之条件结构体:[[ ... ]]
字符串匹配时使用到的模式匹配
bash特性之条件表达式
Bourne Shell的内置命令test及其常用格式[ .. ]
字符串匹配时,使用扩展的glob或者忽略大小写匹配,需要了解的shell选项:extglob,nocaseglob,nocasematch等。
test简介
测试命令test用于形成一个表达式,结合条件判断语句if-else来判断。
例如可以判断某个文件是否存在,是否具备什么样的特性(可读吗?可写吗?可执行吗?块文件吗?)等等。
测试命令test有三种语法格式:
test EXPRESSION
[ EXPRESSION ]
[[ EXPRESSION ]]
前两种是等价的,应该是没有区别的。注意EXPRESSION两边与中括号之间需要有空格。
双中括号与前两者的区别,主要在于表达式中的操作符如果是这种情况的时候。
[[ string1 > string2 ]]
[[ string1 < string2 ]]
字符串之间比较比较大小,其实比的是在词典中字符的先后顺序。[]应该是基于ASCII来比较,而[[]]应该是基于shell当前的地理位置设置来比较(应该与locale相关的环境变量有关吧)。
更多的信息,可能需要大家去直接看bash的手册吧,我稍微看了下,是真的很难以理解,所以暂时放弃了,只找到了这些简单的区别,按照骏马兄的话,咱只能先学习bash的形了,直接跳着看bash手册会有一种被劝退的感觉。
测试表达式的结果为真或者假,真返回0,假返回1。这个返回值不会显示在终端上,而是保存在shell的特殊变量“$?”中(bash原文中提到的是特殊参数(parameter),变量是一个通过名称表示的参数)。
因此我们只需要在每次测试后echo这个特殊变量的值就可以验证了。
# [ EXPRESSION ] # echo $?
test实战
数值比较
-eq:equal,是否相等。
[root@c7-server ~]# age=28 [root@c7-server ~]# [ $age -eq 28 ] [root@c7-server ~]# echo $? 0
-ne:not equal,是否不相等。
[root@c7-server ~]# [ $age -ne 28 ] [root@c7-server ~]# echo $? 1
-gt:greater than,是否大于。
[root@c7-server ~]# [ $age -gt 28 ] [root@c7-server ~]# echo $? 1
-ge:greater equal,是否大于等于。
[root@c7-server ~]# [ $age -ge 28 ] [root@c7-server ~]# echo $? 0
-lt:less than,是否小于。
[root@c7-server ~]# [ $age -lt 28 ] [root@c7-server ~]# echo $? 1
-le:less equal,是否小于等于。
[root@c7-server ~]# [ $age -le 28 ] [root@c7-server ~]# echo $? 0
字符串比较
==:是否等于。字符串等值比较,使用一个=符号也是可以。
[root@c7-server ~]# name=alongdidi
[root@c7-server ~]# [ $name == alongdidi ] [root@c7-server ~]# echo $? 0
!=:是否不等于。
[root@c7-server ~]# [ $name != alongdidi ] [root@c7-server ~]# echo $? 1
=~:基于regex3的扩展正则匹配,并且使用这个操作符的时候必须使用[[]]。
[root@c7-server ~]# [[ $name =~ along(di){1} ]] [root@c7-server ~]# echo $? 0 [root@c7-server ~]# [ $name =~ along(di){1} ] -bash: syntax error near unexpected token `(' [root@c7-server ~]# [ $name =~ along\(di\){1} ] -bash: [: =~: binary operator expected
-z:判断字符串是否为空。
[root@c7-server ~]# [ -z $name ] [root@c7-server ~]# echo $? 1
-n:判断字符串是否不空。
[root@c7-server ~]# [ -n $name ] [root@c7-server ~]# echo $? 0
在做字符串的等值比较时,无论是==、!=或者=~,它们都是二元(binary)的运算符,也就是在这个符号的左右两边都必须存在字符串。
如果其中一边为空的话,就会报错。
[root@c7-server ~]# unset name [root@c7-server ~]# [ $name == tom ] -bash: [: ==: unary operator expected
因为$name为空,因此整个表达式就变成了。
[ == tom ]
所以bash会报错并告诉你我们期待的是一元(unary)运算符(因为只有tom这个字符串)。一元也可以叫单目,是一样的意思。
解决的办法有三个,对$name使用双引号或者单引号包裹或者将[]换成[[]]。
[root@c7-server ~]# [ "$name" == tom ] [root@c7-server ~]# echo $? 1 [root@c7-server ~]# [ '$name' == tom ] [root@c7-server ~]# echo $? 1 [root@c7-server ~]# [[ $name == tom ]] [root@c7-server ~]# echo $? 1
文件测试
存在性
-a FILE或者-e FILE:判断文件是否存在。
[root@c7-server ~]# [ -a /etc/passwd ] [root@c7-server ~]# echo $? 0 [root@c7-server ~]# [ -e /etc/passwd ] [root@c7-server ~]# echo $? 0
文件类型
-b FILE:文件是否存在且为块设备文件。
[root@c7-server ~]# ls -l /dev/sda1 brw-rw---- 1 root disk 8, 1 Jan 2 13:51 /dev/sda1 [root@c7-server ~]# [ -b /dev/sda1 ] [root@c7-server ~]# echo $? 0
-c FILE:文件是否存在且为字符设备文件。
[root@c7-server ~]# ls -l /dev/autofs crw------- 1 root root 10, 235 Jan 2 13:51 /dev/autofs [root@c7-server ~]# [ -c /dev/autofs ] [root@c7-server ~]# echo $? 0
-d FILE:文件是否存在且为目录文件。
[root@c7-server ~]# ls -ld /root dr-xr-x---. 16 root root 4096 Jan 7 09:54 /root [root@c7-server ~]# [ -d /root ] [root@c7-server ~]# echo $? 0
-f FILE:文件是否存在且为普通文件(即文本文件)。
[root@c7-server ~]# ls -l /etc/passwd -rw-r--r-- 1 root root 2296 Nov 11 14:28 /etc/passwd [root@c7-server ~]# file /etc/passwd /etc/passwd: ASCII text [root@c7-server ~]# [ -f /etc/passwd ] [root@c7-server ~]# echo $? 0
-h FILE:文件是否存在且为字符链接文件,即软连接、符号链接。
-L FILE:同上。
[root@c7-server ~]# ls -l /etc/rc.local lrwxrwxrwx. 1 root root 13 Oct 17 14:59 /etc/rc.local -> rc.d/rc.local [root@c7-server ~]# [ -h /etc/rc.local ] [root@c7-server ~]# echo $? 0 [root@c7-server ~]# [ -L /etc/rc.local ] [root@c7-server ~]# echo $? 0
-p FILE:文件是否存在且为命名管道文件。
[root@c7-server ~]# ls -l /run/dmeventd-client prw------- 1 root root 0 Jan 7 09:54 /run/dmeventd-client [root@c7-server ~]# [ -p /run/dmeventd-client ] [root@c7-server ~]# echo $? 0
-S FILE:文件是否存在且为套接字文件。
[root@c7-server ~]# ls -l /run/systemd/shutdownd srw------- 1 root root 0 Jan 7 09:54 /run/systemd/shutdownd [root@c7-server ~]# file /run/systemd/shutdownd /run/systemd/shutdownd: socket [root@c7-server ~]# [ -S /run/systemd/shutdownd ] [root@c7-server ~]# echo $? 0
文件权限
-r FILE:文件是否存在且对当前用户可读。
-w FILE:文件是否存在且对当前用户可写。
-x FILE:文件是否存在且对当前用户可执行。
[root@c7-server ~]# ls -l /etc/passwd -rw-r--r-- 1 root root 2296 Nov 11 14:28 /etc/passwd [root@c7-server ~]# [ -r /etc/passwd ] [root@c7-server ~]# echo $? 0 [root@c7-server ~]# [ -w /etc/passwd ] [root@c7-server ~]# echo $? 0 [root@c7-server ~]# [ -x /etc/passwd ] [root@c7-server ~]# echo $? 1
特殊文件权限
-u FILE:文件是否存在且具有SUID权限。
[root@c7-server ~]# ls -l /usr/bin/passwd -rwsr-xr-x. 1 root root 27832 Jun 10 2014 /usr/bin/passwd [root@c7-server ~]# [ -u /usr/bin/passwd ] [root@c7-server ~]# echo $? 0
-g FILE:文件是否存在且具有SGID权限。
[root@c7-server ~]# ls -l /usr/bin/wall -r-xr-sr-x. 1 root tty 15344 Jun 10 2014 /usr/bin/wall [root@c7-server ~]# [ -g /usr/bin/wall ] [root@c7-server ~]# echo $? 0
-k FILE:文件是否存在且具有STICKY权限。
[root@c7-server ~]# ls -ld /tmp/ drwxrwxrwt. 11 root root 4096 Jan 7 14:21 /tmp/ [root@c7-server ~]# [ -k /tmp/ ] [root@c7-server ~]# echo $? 0
文件是否有内容测试
-s FILE:文件是否存在且有内容。
[root@c7-server ~]# ls -l test.txt ls: cannot access test.txt: No such file or directory [root@c7-server ~]# touch test.txt [root@c7-server ~]# [ -s test.txt ] [root@c7-server ~]# echo $? 1 [root@c7-server ~]# [ -s /etc/passwd ] [root@c7-server ~]# echo $? 0
时间戳测试
-N FILE:文件自身从上一次读操作后是否被修改过。
[root@c7-server ~]# cat test.txt [root@c7-server ~]# [ -N test.txt ] [root@c7-server ~]# echo $? 1 [root@c7-server ~]# echo "alongdidi" > test.txt [root@c7-server ~]# [ -N test.txt ] [root@c7-server ~]# echo $? 0
从属关系测试
-O FILE:当前用户是否为文件的属主。
-G FILE:当前用户是否属于文件的属组。
[root@c7-server ~]# ls -ld /home/zwl/ drwx------. 3 zwl zwl 78 Apr 11 2018 /home/zwl/ [root@c7-server ~]# [ -O /home/zwl/ ] [root@c7-server ~]# echo $? 1 [root@c7-server ~]# [ -G /home/zwl/ ] [root@c7-server ~]# echo $? 1
双目测试
FILE1 -ef FILE2:FILE1和FILE2是否指向同一个文件系统的相同inode的硬链接。
[root@c7-server ~]# ln test.txt test.hard [root@c7-server ~]# ls -li test.txt test.hard 33731123 -rw-r--r-- 2 root root 10 Jan 7 14:31 test.hard 33731123 -rw-r--r-- 2 root root 10 Jan 7 14:31 test.txt [root@c7-server ~]# [ test.txt -ef test.hard ] [root@c7-server ~]# echo $? 0
FILE1 -nt FILE2:FILE1是否新于FILE2,根据文件的mtime。
FILE1 -ot FILE2:FILE1是否旧于FILE2,根据文件的mtime。
[root@c7-server ~]# [ test.txt -nt /etc/passwd ] [root@c7-server ~]# echo $? 0 [root@c7-server ~]# [ test.txt -ot /etc/passwd ] [root@c7-server ~]# echo $? 1
组合测试条件
即与或非。
[ EXP1 -a EXP2 ]:EXP1和EXP2必须都为true,结果才为true。
[ EXP1 -o EXP2 ]:只要EXP1和EXP2当中有一个为true,结果就为true。
[ ! EXP ]:当EXP为true的时候,结果为false;当EXP为false的时候,结果为true。
练习
如果主机名为空或者包含local字符串,则将主机名设置为www.alongdidi.com。
hostName=$(hostname) [ -z "${hostName}" -o ${hostName}=~"local" ] && hostname www.magedu.com
注:实际我在CentOS 7,Bash 4.2.46场景下执行该命令,得到的结果不太对。主要问题出在“=~”的判断上。暂时未知如何解决,这里大概知道下思路即可。
命令/脚本状态返回值
上文中我们介绍了特殊变量$?,它存储了测试表达式的测试结果。true=0,false=1。
命令执行的结果也会有这么一个返回值(也可以叫退出状态码),一般返回值0表示命令执行成功,返回值非0(多数情况下是1)则表示失败。
PS:这里也需要注意,大多数编程语言使用1来表示成功/true等。还有大家也要注意和命令执行后的标准输出或者标准错误输出区别开。一个表示命令执行成功与否的结果,另一个则是命令执行的输出结果。
这个返回值在我们执行脚本的时候,也会返回。默认脚本执行的返回值使用的是脚本中最后一条命令的返回值。
如果脚本中前几条命令的执行均成功了,但是最后一条执行失败了,那么整个脚本的$?也是非0的。
我们可以通过exit命令来手工配置退出状态码。bash遇到exit会立即退出当前的shell并将返回值存入父shell的$?变量中。因此可以用来立即退出bash脚本。
[root@c7-server ~]# bash [root@c7-server ~]# exit 10 exit [root@c7-server ~]# echo $? 10
PS:有的时候退出状态码会异常,可能和返回值的取值有关系。
[root@c7-server ~]# exit 1000 exit [root@c7-server ~]# echo $? 232 [root@c7-server ~]# bash [root@c7-server ~]# exit 1024 exit [root@c7-server ~]# echo $? 0
自定义返回值一般用于bash脚本中的判断。比如,当某个文件不存在的时候,立即执行exit 5。
脚本会立刻退出,5这个返回值会被返回。一般程序员会事先定义好不同的返回值表达的不同含义,并将其写入文档。
用户根据返回值和该文档来判断脚本为什么中断执行了。
像rsync的man手册中就有定义。
0 Success 1 Syntax or usage error 2 Protocol incompatibility 3 Errors selecting input/output files, dirs ...
脚本中的参数
在学习C语言的时候,我们可以向函数执行传递参数的操作。bash脚本编程也是可以的。
可以对脚本进行传参,也可以对函数。
在引用参数的时候,具体是引用脚本的参数还是函数的参数则取决于引用的位置。
按照马哥课程的进度,函数还未学习到,因此这里就不说了。(虽然已经有bash编程的基础)等到写bash函数的博文时,会在提及。
向脚本传参和脚本中引用参数十分简单,示例如下。
[root@c7-server ~]# cat test.sh #!/bin/bash echo $1 echo $2 [root@c7-server ~]# bash test.sh along didi along didi
在执行脚本时,脚本名称后面的字符串就是参数,多个参数之间以空格分离,根据参数出现在脚本名称后的位置,在脚本中使用$1、$2、$3...来引用,它们也被称作位置参数。
shift not shit
如果我们想要改变位置参数的位置,就需要使用到shift内置命令。
shift [n]
shift的本意是移动,我们可以理解为拿掉位置参数最左边的n个。默认是1个。
[root@c7-server ~]# cat test.sh #!/bin/bash echo $* shift 2 echo $* [root@c7-server ~]# bash test.sh a long di di a long di di di di
其他特殊变量
$#:获取脚本被传递的参数的个数。
[root@c7-server ~]# cat test.sh #!/bin/bash echo $# [root@c7-server ~]# bash test.sh a l on g did i 6
$0:获取脚本的名称,这个名称是执行时的名称。执行的方式不同,值也不同。
[root@c7-server ~]# cat test.sh #!/bin/bash echo $0 [root@c7-server ~]# bash test.sh test.sh [root@c7-server ~]# /root/test.sh /root/test.sh [root@c7-server ~]# ./test.sh ./test.sh
只想获取脚本的名称的话,可结合basename命令。
[root@c7-server ~]# basename $(bash test.sh) test.sh [root@c7-server ~]# basename $(/root/test.sh) test.sh [root@c7-server ~]# basename $(./test.sh) test.sh
$*:引用所有的参数。在双引号的情况下,所有参数整合作为一个整体。
$@:引用所有的参数。无论是否有双引号,每个参数自身都作为一个整体。
[root@c7-server ~]# cat test.sh #!/bin/bash echo $* echo $@ [root@c7-server ~]# bash test.sh a long di di a long di di a long di di
区别的话,可以看这个例子。
[root@c7-server ~]# cat test.sh #!/bin/bash for i in $*; do echo $i; done for i in $@; do echo $i; done echo "I am cut-off line" for i in "$*"; do echo $i; done for i in "$@"; do echo $i; done [root@c7-server ~]# bash test.sh a long di di a long di di a long di di I am cut-off line a long di di a long di di
更深度的解释,查阅官方文档的话,需要对bash的单词分割(word splitting)和IFS变量有理解才行。