开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第29天,点击查看活动详情
前言
在上一章中,我们遇到一个问题。怎样使我们的报告生成器脚本能适应运行此脚本的用户的权限?这个问题的解决方案要求我们能找到一种方法,在脚本中基于测试条件结果,来“改变方向”。用编程术语表达,就是我们需要程序可以分支。让我们考虑一个简单的用伪码表示的逻辑实例,伪码是一种模拟的计算机语言,为的是便于人们理解:
X=5
If X = 5, then:
Say
“X equals 5.”
Otherwise:
Say “X is not equal to 5.”
这就是一个分支的例子。根据条件,“Does X = 5?”做一件事情,“Say X equals 5,”否则,做另一件事情,“Say X is not equal to 5.” 使用 shell,我们可以编码上面的逻辑,如下所示:
x=5
if [ $x = 5 ]; then
echo "x equals 5."
else
echo "x does not equal 5."
fi
或者我们可以直接在命令行中输入以上代码(略有缩短):
[me@linuxbox ~]$ x=5
[me@linuxbox ~]$ if [ $x = 5 ]; then echo "equals 5"; else echo "does
not equal 5"; fi
equals 5
[me@linuxbox ~]$ x=0
[me@linuxbox ~]$ if [ $x = 5 ]; then echo "equals 5"; else echo "does
not equal 5"; fi
does not equal 5
在这个例子中,我们执行了两次这个命令。第一次是,把 x 的值设置为 5,从而导致输出字符串“equals 5”, 第二次是,把 x 的值设置为 0,从而导致输出字符串“does not equal 5”。
这个 if 语句语法如下:
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
这里的 commands 是指一系列命令。第一眼看到会有点儿困惑。但是在我们弄清楚这些语
句之前,我们必须看一下 shell 是如何评判一个命令的成功与失败的。
一、流程控制:if 分支结构
1.1 退出状态
当命令执行完毕后,命令(包括我们编写的脚本和 shell 函数)会给系统发送一个值,叫做退出状态。
这个值是一个 0 到 255 之间的整数,说明命令执行成功或是失败。按照惯例,一个零值说明成功,其它所有值说明失败。
Shell 提供了一个参数,我们可以用它检查退出状态。
用具体实例看一下:
[me@linuxbox ~]$ ls -d /usr/bin
/usr/bin
[me@linuxbox ~]$ echo $?
0
[me@linuxbox ~]$ ls -d /bin/usr
ls: cannot access /bin/usr: No such file or directory
[me@linuxbox ~]$ echo $?
2
在这个例子中,我们执行了两次 ls 命令。第一次,命令执行成功。如果我们显示参数 ?。
这次它包含一个数字 2,表明这个命令遇到了一个错误。有些命令使用不同的退出值,来诊断错误,而许多命令当它们执行失败的时候,会简单地退出并发送一个数字 1。手册页中经常会包含一章标题为“退出状态”的内容,描述了使用的代码。
然而,一个零总是表明成功。
这个 shell 提供了两个极其简单的内部命令,它们不做任何事情,除了以一个零或 1 退出状态来终止执行。
True 命令总是执行成功,而 false 命令总是执行失败:
[me@linuxbox~]$ true
[me@linuxbox~]$ echo $?
0
[me@linuxbox~]$ false
[me@linuxbox~]$ echo $?
1
我们能够使用这些命令,来看一下 if 语句是怎样工作的。
If 语句真正做的事情是计算命令执行成功或失败:
[me@linuxbox ~]$ if true; then echo "It's true."; fi
It's true.
[me@linuxbox ~]$ if false; then echo "It's true."; fi
[me@linuxbox ~]$
当 if 之后的命令执行成功的时候,命令 echo “It’s true.” 将会执行,否则此命令不执行。
如果 if 之后跟随一系列命令,则将计算列表中的最后一个命令:
[me@linuxbox ~]$ if false; true; then echo "It's true."; fi
It's true.
[me@linuxbox ~]$ if true; false; then echo "It's true."; fi
[me@linuxbox ~]$
3
1.2 测试
到目前为止,经常与 if 一块使用的命令是 test。这个 test 命令执行各种各样的检查与比较。
它有两种等价模式:
test expression
比较流行的格式是:
[ expression ]
这里的 expression 是一个表达式,其执行结果是 true 或者是 false。
当表达式为真时,这个test 命令返回一个零退出状态,当表达式为假时,test 命令退出状态为 1。
文件表达式
以下表达式被用来计算文件状态:
字符串表达式
以下表达式用来计算字符串:
警告:
这个 < 和 > 表达式操作符必须用引号引起来(或者是用反斜杠转义),当与 test 一块使用的时候。如果不这样,它们会被 shell 解释为重定向操作符,造成潜在地破坏结果。同时也要注意虽然 bash 文档声明排序遵从当前语系的排列规则,但并不这样。将来的 bash 版本,包含 4.0,使用 ASCII(POSIX)排序规则。
这是一个演示这些问题的脚本:
#!/bin/bash
# test-string: evaluate the value of a string
ANSWER=maybe
if [ -z "$ANSWER" ]; then
echo "There is no answer." >&2
exit 1
fi
if [ "$ANSWER" = "yes" ]; then
echo "The answer is YES."
elif [ "$ANSWER" = "no" ]; then
echo "The answer is NO."
elif [ "$ANSWER" = "maybe" ]; then
echo "The answer is MAYBE."
else
echo "The answer is UNKNOWN."
fi
在这个脚本中,我们计算常量 ANSWER。我们首先确定是否此字符串为空。
如果为空,我们就终止脚本,并把退出状态设为零。
注意这个应用于 echo 命令的重定向操作。
其把错误信息“There is no answer.”重定向到标准错误,这是处理错误信息的“合理”方法。
如果字符串不为空,我们就计算字符串的值,看看它是否等于“yes,”“no,”或者“maybe”。
为此使用了elif,它是“else if”的简写。通过使用 elif,我们能够构建更复杂的逻辑测试。
整型表达式
下面的表达式用于整数:
1.3 更现代的测试版本
目前的 bash 版本包括一个复合命令,作为加强的 test 命令替代物。它使用以下语法:
[[ expression ]]
这里,类似于 test,expression 是一个表达式,其计算结果为真或假。这个 [[ ]] 命令非
常相似于 test 命令(它支持所有的表达式),但是增加了一个重要的新的字符串表达式:
string1 =~ regex
其返回值为真,如果 string1 匹配扩展的正则表达式 regex。这就为执行比如数据验证等任务提供了许多可能性。在我们前面的整数表达式示例中,如果常量 INT 包含除了整数之外的任何数据,脚本就会运行失败。
1.4 (( )) - 为整数设计
除了 [[ ]] 复合命令之外,bash 也提供了 (( )) 复合命名,其有利于操作整数。
它支持一套完整的算术计算,我们将在第 35 章中讨论这个主题。(( )) 被用来执行算术真测试。
如果算术计算的结果是非零值,则一个算术真测试值为真。
[me@linuxbox ~]$ if ((1)); then echo "It is true."; fi
It is true.
[me@linuxbox ~]$ if ((0)); then echo "It is true."; fi
[me@linuxbox ~]$
使用 (( )),我们能够略微简化 test-integer2 脚本,像这样:
#!/bin/bash
# test-integer2a: evaluate the value of an integer.
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if ((INT == 0)); then
echo "INT is zero."
else
if ((INT < 0)); then
echo "INT is negative."
else
echo "INT is positive."
fi
if (( ((INT % 2)) == 0)); then
echo "INT is even."
else
echo "INT is odd."
fi
fi
else
echo "INT is not an integer." >&2
exit 1
fi
注意我们使用小于和大于符号,以及 == 用来测试是否相等。这是使用整数较为自然的语法了。
也要注意,因为复合命令 (( )) 是 shell 语法的一部分,而不是一个普通的命令,而且它只处理整数,所以它能够通过名字识别出变量,而不需要执行展开操作。我们将在第 35 中进一步讨论 (( )) 命令和相关的算术展开操作。
1.5 结合表达式
也有可能把表达式结合起来创建更复杂的计算。通过使用逻辑操作符来结合表达式。
我们在第 18 章中已经知道了这些,当我们学习 fifind 命令的时候。它们是用于 test 和 [[ ]] 三个逻辑操作。它们是 AND,OR,和 NOT。
test 和 [[ ]] 使用不同的操作符来表示这些操作:
1.6 控制操作符:分支的另一种方法
bash 支持两种可以执行分支任务的控制操作符。这个 &&(AND)和||(OR)操作符作用如同复合命令 [[ ]] 中的逻辑操作符。
这是语法:
command1 && command2
和
command1 || command2
理解这些操作很重要。对于 && 操作符,先执行 command1,如果并且只有如果 command1执行成功后,才会执行 command2。对于 || 操作符,先执行 command1,如果并且只有如果command1 执行失败后,才会执行 command2。
在实际中,它意味着我们可以做这样的事情:
[me@linuxbox ~]$ mkdir temp && cd temp
这会创建一个名为 temp 的目录,并且若它执行成功后,当前目录会更改为 temp。第二个命令会尝试执行只有当 mkdir 命令执行成功之后。同样地,一个像这样的命令:
[me@linuxbox ~]$ [ -d temp ] || mkdir temp
会测试目录 temp 是否存在,并且只有测试失败之后,才会创建这个目录。这种构造类型非常有助于在脚本中处理错误,这个主题我们将会在随后的章节中讨论更多。例如,我们在脚本中可以这样做:
[ -d temp ] || exit 1
如果这个脚本要求目录 temp,且目录不存在,然后脚本会终止,并返回退出状态 1。
总结
这一章开始于一个问题。
我们怎样使 sys info page 脚本来检测是否用户拥有权限来读取所有的主目录?
根据我们的 if 知识,我们可以解决这个问题,通过把这些代码添加到report home space 函数中:
report_home_space () {
if [[ $(id -u) -eq 0 ]]; then
cat <<- _EOF_
<H2>Home Space Utilization (All Users)</H2>
<PRE>$(du -sh /home/*)</PRE>
_EOF_
else
cat <<- _EOF_
<H2>Home Space Utilization ($USER)</H2>
<PRE>$(du -sh $HOME)</PRE>
_EOF_
fi
return
}
我们计算 id 命令的输出结果。通过带有 -u 选项的 id 命令,输出有效用户的数字用户 ID号。
超级用户总是零,其它每个用户是一个大于零的数字。知道了这点,我们能够构建两种不同的 here 文档,一个利用超级用户权限,另一个限制于用户拥有的主目录。
我们将暂别 sys info page 程序,但不要着急。它还会回来。同时,当我们继续工作的时候,将会讨论一些我们需要的话题。