Chapter12 - BASH 条件句和控制结构
使用条件名和控制结构优化 Bash Shell 脚本
使用Bash 特殊变量
尽管用户定义的变量为脚本作者提供了创建容器以以存储脚本所使用的值的方法,但Bash 还提供了一些预定义变量,在编写shell 脚本时很有用。其中类型的预定义变量是位置参数。
位置参数
位置参数是变量,可将命令行参数的值存储到脚本中。将以数字方式对变量进行命名。变量 0 指是的脚本名称自身。之后 ,预定义变量 1 时,以脚本的第一个参数作为其值,变量 2 包含第二个参数,以此类推。可以使用语法 $1 、$2 (以此类推)来引用值。
重要
当引用值超过第九个位置参数时,必须使用花括号括起的变量扩展形式,尽管这种情况很少见。例如,第10个参数的必须以语法 ${10} 而非 $10 来引用。否则 ,Bash 会将 $10 中的‘$1’扩展到脚本的第一个位置参数的值。
Bash 提供了特殊变量以引用位置参数 $* 和 $@ .这两个变量引用脚本中的所有参数,但是差别。使用 $*时,所有参数都将被视为一个单词。但是,当使用 $@ 时,每个参数都被视为单独单词。以下示例中演示了这种情况。
[student@server0 bin]$ cat showargs
#!/bin/bash
for ARG in "$*" ;do
echo $ARG
done
[student@server0 bin]$ ./showargs 1 2 3
1 2 3
[student@server0 bin]$ cat showargs
#!/bin/bash
for ARG in "$@";do
echo $ARG
done
[student@server0 bin]$ ./showargs 1 2 3
1
2
3
使用位置参数时,另一个可能有用的值是 $# ,该值表示传递给脚本的命令行参数的数量。该值可以用于验证是否任何参数或者正确数量的参数传递到脚本。
[student@server0 bin]$ cat countargs
#!/bin/bash
echo "There are $# arguments."
[student@server0 bin]$ ./countargs
There are 0 areumengs.
[student@server0 bin]$ ./countargs 1 2 3
There are 3 arguments.
评估和退出代码
每个命令返回一个状态,也通常称为返回状态或退出代码。如果命令成功,则退出时的退出状态为 0 。如果命令不成功,则退出时的退出状态不为零。完成后,命令的退出状态将传递到父进程并存储在 ? 变量中。因此,已执行命令的退出状态可通过显示 $? 值来检索。以下示例说明了几个常见命令在执行和退出状态检索。
[student@server0 bin]$ ls /etc/hosts
/etc/hosts
[student@server0 bin]$ echo $?
0
[student@server0 bin]$ ls /etc/nofile
ls:cannot access /etc/nofile:No such file or directory
[student@server0 bin]$ echo $?
2
[student@server0 bin]$ grep localhost /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
[student@server0 bin]$ echo $?
0
[student@server0 bin]$ grep random /etc/hosts
[student@server0 bin]$ echo $?
1
在脚本中使用退出代码
一旦执行,脚本将在其处理所有内容之后退出。但是,有时候可能需要中途退出脚本,比如在遇到错误条件时。可以通过在脚本中使用 exit 命令来实现这一目的。当脚本遇到 exit 命令时,脚本将立即退出并跳过对脚本其余内容的处理。
可以使用可选的整数参数(0 到 255 之间,表示退出代码)来执行 exit 命令。退出代码值 0 表示没有错误。所有其他非零值都表示存在错误的退出代码。脚本作者可以使用不同的非零值来区分遇到的不同类型错误。该退出代码传递回父进程,父进程将其存储在 ? 变量中,可以使用 $? 来访问,如以下示例中所示。
[student@server0 bin]$ cat hello
#!/bin/bash
echo "Hello world"
exit 0
[student@server0 bin]$ ./hello
Hello,world
[student@server0 bin]$ echo $?
0
[student@server0 bin]$cat hello
#!/bin/bash
echo "Hello,world"
exit 1
[student@server0 bin]$ ./hello
Hello,world
[student@server0 bin]$ echo $?
1
如果不使用任何参数调用 exit 命令,那么脚本将退出并且将最后执行的命令的退出状态传递给父进程。
测试脚本输入
为确保脚本不会由于意外情况轻易偏离轨道,对于脚本作者而言,一种良好的做法是不要进行与输入有关的假设,如命令参数、用户输入、命令退换、变量表达式、文件名扩展等等。可以使用Bash 的测试功能来执行完整性检查。可以使用 Bash 的测试命令语法 [<TESTEXPRESSION>
] 来执行测试。也可以使用 Bash 的最新扩展测试命令语法 [[<TESTEXPRESSION>
]](在Bash 版本 2.02 及更高版本中可用)执行这些测试。
与所有命令一样,test 命令会在完成后生成一个退出代码,该退出代码存储为值 $? 。要查看测试的结论,只需显示 $? 的值(在执行 test 命令之后立即显示)。再次说明,退出状态值 0 表示测试成功,而非零表示测试失败。
执行比较测试
比较测试表达式使用二进制比较运算符。这些运算符预期两个对象,运行符每侧一个,并对两个对象进行求值获得等式或不等式。Bash 使用另外一组运算符进行字符串和数字比较,并使用以下语法格式:
[<ITEM1><BINARY COMPARISON OPERATOR><ITEM2>]
Bash 的数字比较仅限于整数比较。以下二进制运算符列表在 Bash 中用于整数比较。
运算符 | 含义 | 示例 |
---|---|---|
-eq | 等于 | [ "b" ] |
-ne | 不等于 | [ "b" ] |
-gt | 大于 | [ "b" ] |
-ge | 大于等于 | [ "b" ] |
-lt | 小于 | [ "b" ] |
-le | 小于等于 | [ "b" ] |
以下示例演示了 Bash 的数字运算符的使用。
[root@promote ~]# [ 1 -eq 1 ];echo $?
0
[root@promote ~]# [ 1 -ne 1 ];echo $?
1
[root@promote ~]# [ 2 -ge 2 ];echo $?
0
[root@promote ~]# [ 8 -gt 2 ];echo $?
0
[root@promote ~]# [ 2 -lt 2 ];echo $?
1
[root@promote ~]# [ 1 -lt 2 ];echo $?
0
Bash 的字符串比较使用以下二进制运算符。
运算符 | 含义 | 示例 |
---|---|---|
= | 等于 | [ "b" ] |
== | 等于 | [ "b" ] |
!= | 不等于 | [ "b" ] |
以下示例演示了Bash 的字符串比较运算符的使用。
[root@promote ~]# [ abc = abc ];echo $?
0
[root@promote ~]# [ abc == abc ];echo $?
0
[root@promote ~]# [ abc != abc ];echo $?
1
[root@promote ~]# [ abc != efg ];echo $?
0
Bash 还有几个一元运算符可用于字符串求值。一元运算符使用以下格式仅对一个项目进行求值。
[ <UNARY OPERATOR><ITEM>
]
下表显示了Bash 的用于字符串求值的一元运算符。
运算符 | 含义 | 示例 |
---|---|---|
-z | 字符串的长度为零(空) | [ -z "$a" ] |
-n | 字符串不为空 | [ -n "$a" ] |
以下示例演示了 Bash 的字符串一元运算符的使用。
[root@promote ~]# STRING='';[ -z "$STRING" ];echo $?
0
[root@promote ~]# STRING=' ';[ -z "$STRING" ];echo $?
1
[root@promote ~]# STRING='abc';[ -z "$STRING" ];echo $?
1
[root@promote ~]# STRING='';[ -n "$STRING" ];echo $?
1
[root@promote ~]# STRING=' ';[ -n "$STRING" ];echo $?
0
[root@promote ~]# STRING='abc';[ -n "$STRING" ];echo $?
0
测试文件和目录
通过Bash 的字符串和二进制运算符,用户可贯彻不假定shell 脚本输入完整性的良好做法。当脚本与外部实体(如文件和目录)交互时,同样应谨慎。为此,Bash提供大量测试运算符。如下表中
运算符 | 含义 | 示例 |
---|---|---|
-b | 文件存在且为块文件 | [ -b<FILE> ] |
-c | 文件存在且为字符文件 | [ -c<FILE> ] |
-d | 文件存在并且是目录 | [ -d<DIRECTORY> ] |
-e | 文件存在 | [ -e<FILE> ] |
-f | 文件是常规文件 | [ -f<FILE> ] |
-L | 文件存在并且是符号链接 | [ -L<FILE> ] |
-r | 文件存在且有读权限 | [ -r<FILE> ] |
-s | 文件存在且大小大于零 | [ -s<FILE> ] |
-w | 文件存在且有写权限 | [ -w<FILE> ] |
-x | 文件存在且有执行(或搜索)权限 | [ -x<FILE> ] |
Bash 还提供了几个二进制运算符以执行文件比较。下表中定义了这些运算符。
运算符 | 含义 | 示例 |
---|---|---|
-ef | FILE1 与 FILE2 的设备和索引节点编号相同 | [<FILE1> -ef <FILE2> ] |
-nt | FILE1 的修改日期比 FILE2 早 | [<FILE1> -ef <FILE2> ] |
-ot | FILE1 的修改日期比 FILE2晚 | [<FILE1> -ot <FILE2> ] |
重要
测试表达式的括号中的空格字符串以及用于在测试表达式中分隔元素的空格字符串并非是为了可读性,而是对表达式进行正确的求值所必需的。如果缺少任何空格字符,那么测试将失败或者产生不准确或意外的结果。
逻辑 AND 、OR 运算符
有些情况下,测试多个条件可能会很有用。Bash 的逻辑 AND 运算符(即 &&)允许用于执行复合条件测试以了解两个条件是否均成立。另一方便,Bash 的逻辑 OR 运算符(即 || )允许用户测试两个条件中的一个是否成立。以下示例演示了 Bash 的逻辑 AND 和 OR 运算符的使用。
[root@promote ~]# [ 2 -gt 1 ] && [ 1 -gt 0 ];echo $?
0
[root@promote ~]# [ 2 -gt 1 ] && [ 1 -gt 2 ];echo $?
1
[root@promote ~]# [ 2 -gt 1 ] || [ 1 -gt 2 ];echo $?
0
[root@promote ~]# [ 0 -gt 1 ] || [ 1 -gt 2 ];echo $?
1
使用条件结构
简单的 shell 脚本表示从头到尾执行的命令的集合。条件结构允许用户在 shell 脚本中包含决策,以便当满足特定条件时才执行脚本的特定部分。
if/then 语句
Bash 中最简单的条件结构是 if/then 结构,其语法如下:
if <CONDITION>;then
<STATEMENT>
……
<STATEMENT>
fi
通过此结构,如果满足给定条件,将采取一个或多个操作。如果不满足给定条件,则不采取任何操作。前面演示的数字、字符串和文件测试经常用于在 if/then 语句中测试条件。以下代码演示了使用 if/then 语句启动 psacct 服务(如果其未处于活动状态)。
systemctl is-active psacct > /dev/null 2>&1
if [ $? -eq 0 ];then
systemctl start psacct
fi
if/then/else 语句
if/then 条件结构可以进上步扩展,以便能够根据是否满足条件来采取不同的操作集合。使用 if/then/else 条件结构可实现此目录。
if <CONDITION>;then
<STATEMENT>
……
else
<STATEMENT>
……
<STATEMENT>
fi
以下代码演示了使用 if/then/else 语句来启动 psacct 服务(如果其示处于活动状态)和停止该服务(如果其处于活动状态)。
systemctl is-acitve psacct >/dev/null 2>&1
if [ $? -ne 0 ];then
systemctl start psacct
else
systemctl stop psacct
fi
if/then/elif/else 语句
最后,if/then/elif/else 条件结构可以进一步扩展以测试多个条件:在满足某个条件时执行不同的操作集合。以下法例 中显示了其结构。在此结构中,Bash 将按照显示的顺序测试条件。在发现某个条件成立后,Bash 将执行与该条件相关联的操作,然后路过条件结构的其余部分。如果所有条件均不成立,Bash 将执行 else 子句中枚举的操作。
if <CONDITION>;then
<STATEMENT>
……
<STATEMENT>
elif <CONDITION>;then
<STATEMENT>
……
<STATEMENT>
else
<STATEMENT>
……
<STATEMENT>
fi
以下代码演示使用 if/then/elif/then/else 语句来运行 mysql 客户端(如果mariadb 服务处于活动状态),运行 psql 客户端(如果 postgresql 服务处于活动状态)或运行 sqllite3 客户端(如果mariadb 和 postgresql 服务未处于活动状态)
systemctl is-active mariadb >/dev/null 2>&1
MARIADB_ACTIVE=$?
systemctl is-active postgresql >/dev/null 2>&1
POSTGRESQL_ACTIVE=$?
if [ "${MARIADB_ACTIVE}" -eq 0 ];then
mysql
elif [ "${POSTGRESQL_ACTIVE}" -eq 0 ];then
psql
else
sqllite3
fi
Case 语句
用户可以在 if/then/elif/then/else 语句中添加其需要的任意数量的 elif 子句,以测试所需任意数量的条件。但是,添加的子句越多,语句及其逻辑就变得更加难以阅读和理解。对于这些更复杂的情况,Bash 提供了另一个条件结构,称为 case 语句。case 语句利用以下语法:
case <VALUE> in
<PATTERN1>)
<STATEMENT>
……
<STATEMENT>
;;
<PATTERN2>)
<STATEMENT>
……
<STATEMENT>
;;
esac
case 语句尝试按顺序逐个将 <VALUE>
与每个<PATTERN>
进行匹配。当某个模式匹配时,将执行与该模式相关联的代码段,以 ;; 语法指示块的结束。然后将跳过case 语句中其余的所有其他模式,并且 case 语句退出。可以根据需要添加任意数量的模式/语句块。
要在 if/then/elif/then/else 结构中模拟else 子句的行为,只需要在 case 语句中使用 * 作为最终模式。由于该表达式与任何内容匹配,因此,其效果是在任何其他模式均不匹配的情况下执行一组命令。
case 语句在 init 脚本中广泛使用。以下代码是一个示例,说明它们如何常用于根据传递给脚本的参数来执行不同操作。
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
reload)
reload
;;
status)
status
;;
*)
echo "Usage: $0 (start | stop | restart | reload | status)"
;;
esac
如果在 case 语句中将对多模式采取相同的操作,那么可以合并以共享同一个操作块,如以下示例所示。管道字符 | 用于分隔多个模式。
case "$1" in
……
reload|restart)
restart
;;
……
esac