Bash Shell 脚本/条件表达式
很多时候,我们希望只有在特定条件满足时才执行某个命令。例如,我们可能希望仅当 source.txt 存在时运行命令 cp source.txt destination.txt(“将文件 source.txt 复制到位置 destination.txt”)。我们可以像这样做到
#!/bin/bash
if [[ -e source.txt ]] ; then
cp source.txt destination.txt
fi
以上使用了两个内置命令
- 构造
[[ condition ]]如果condition为真,则返回退出状态为零(成功),如果condition为假,则返回非零退出状态(失败)。在我们的例子中,condition是-e source.txt,如果且仅当存在名为source.txt的文件时,此条件为真。 - 构造
if command1 ; then
command2
fi
command1;如果成功完成(即退出状态为零),则继续运行command2。
换句话说,上面等价于
#!/bin/bash
[[ -e source.txt ]] && cp source.txt destination.txt
只是它更清晰(并且更灵活,在我们将很快看到的方式中)。
一般来说,Bash 将成功的退出状态(零)视为“真”,将失败的退出状态(非零)视为“假”,反之亦然。例如,内置命令 true 始终“成功”(返回零),而内置命令 false 始终“失败”(返回一)。
| 注意 在许多常用的编程语言中,零被认为是“假”,非零值被认为是“真”。即使在 Bash 中,这也适用于算术表达式(我们将在后面看到)。但在命令级别,情况恰恰相反:退出状态为零表示“成功”或“真”,而非零退出状态表示“失败”或“假”。 |
| 注意 确保在 |
if 语句比我们上面看到的更灵活;我们实际上可以指定多个 命令在测试命令成功时运行,此外,我们还可以使用 else 子句来指定一个或多个命令在测试命令失败时运行
#!/bin/bash
if [[ -e source.txt ]] ; then
echo 'source.txt exists; copying to destination.txt.'
cp source.txt destination.txt
else
echo 'source.txt does not exist; exiting.'
exit 1 # terminate the script with a nonzero exit status (failure)
fi
这些命令甚至可以包含其他 if 语句;也就是说,一个 if 语句可以“嵌套”在另一个 if 语句中。在这个例子中,一个 if 语句嵌套在另一个 if 语句的 else 子句中
#!/bin/bash
if [[ -e source1.txt ]] ; then
echo 'source1.txt exists; copying to destination.txt.'
cp source1.txt destination.txt
else
if [[ -e source2.txt ]] ; then
echo 'source1.txt does not exist, but source2.txt does.'
echo 'Copying source2.txt to destination.txt.'
cp source2.txt destination.txt
else
echo 'Neither source1.txt nor source2.txt exists; exiting.'
exit 1 # terminate the script with a nonzero exit status (failure)
fi
fi
这种特殊的模式——一个 else 子句,它只包含一个 if 语句,代表一个回退测试——非常常见,以至于 Bash 为它提供了一个方便的简写符号,使用 elif(“else-if”)子句。上面的例子可以这样写
#!/bin/bash
if [[ -e source1.txt ]] ; then
echo 'source1.txt exists; copying to destination.txt.'
cp source1.txt destination.txt
elif [[ -e source2.txt ]] ; then
echo 'source1.txt does not exist, but source2.txt does.'
echo 'Copying source2.txt to destination.txt.'
cp source2.txt destination.txt
else
echo 'Neither source1.txt nor source2.txt exists; exiting.'
exit 1 # terminate the script with a nonzero exit status (failure)
fi
单个 if 语句可以有任意数量的 elif 子句,代表任意数量的回退条件。
最后,有时我们希望在条件为假时运行一个命令,而没有相应的命令在条件为真时运行。为此,我们可以使用内置的 ! 运算符,它位于命令之前;当命令返回零(成功或“真”)时,! 运算符会更改返回一个非零值(失败或“假”),反之亦然。例如,以下语句将复制 source.txt 到 destination.txt,除非 destination.txt 已经存在
#!/bin/bash
if ! [[ -e destination.txt ]] ; then
cp source.txt destination.txt
fi
以上所有示例都是使用 test 表达式的示例。实际上,if 只会在语句中的命令返回 0 时运行 then 中的所有内容
# First build a function that simply returns the code given
returns() { return $*; }
# Then use read to prompt user to try it out, read `help read' if you have forgotten this.
read -p "Exit code:" exit
if (returns $exit)
then echo "true, $?"
else echo "false, $?"
fi
因此,if 的行为在某些方面类似于逻辑“与”&& 和“或”||
# Let's reuse the returns function.
returns() { return $*; }
read -p "Exit code:" exit
# if ( and ) else fi
returns $exit && echo "true, $?" || echo "false, $?"
# The REAL equivalent, false is like `returns 1'
# Of course you can use the returns $exit instead of false.
# (returns $exit ||(echo "false, $?"; false)) && echo "true, $?"
始终注意,误用这些逻辑运算符可能会导致错误。在上面的例子中,一切正常,因为普通的 echo 几乎总是成功的。
除了上面使用的 -e file 条件(如果 file 存在则为真)之外,Bash 的 [[ … ]] 符号还支持相当多的条件类型。五个最常用的条件是
-d file- 如果
file存在且为目录,则为真。 -f file- 如果
file存在且为普通文件,则为真。 -e file- 如果
file存在,无论其是什么,都为真。 string == pattern- 如果
string匹配pattern,则为真。(pattern的形式与文件名扩展中的模式相同;例如,未引用的*表示“零个或多个字符”。) string != pattern- 如果
string不 匹配pattern,则为真。 string =~ regexp- 如果
string包含 Posix 扩展正则表达式regexp,则为真。有关更多信息,请参阅 正则表达式/POSIX 扩展正则表达式。
在最后三种类型的测试中,左侧的值通常是变量扩展;例如,[[ "$var" = 'value' ]] 如果名为 var 的变量包含值 value,则返回成功的退出状态。
以上条件只是触及了表面;还有许多其他条件可以检查文件,一些其他条件可以检查字符串,几个条件可以检查整数值,以及一些其他不属于这些组的条件。
平等测试的一个常见用途是查看脚本的第一个参数($1)是否是一个特殊选项。例如,考虑我们上面的 if 语句,它试图将 source1.txt 或 source2.txt 复制到 destination.txt。上面的版本非常“冗长”:它产生了大量的输出。通常我们不希望脚本生成太多输出;但我们可能希望用户能够请求输出,例如通过将 --verbose 作为第一个参数传递。以下脚本等价于上面的 if 语句,但它只在第一个参数是 --verbose 时打印输出
#!/bin/bash
if [[ "$1" == --verbose ]] ; then
verbose_mode=TRUE
shift # remove the option from $@
else
verbose_mode=FALSE
fi
if [[ -e source1.txt ]] ; then
if [[ "$verbose_mode" == TRUE ]] ; then
echo 'source1.txt exists; copying to destination.txt.'
fi
cp source1.txt destination.txt
elif [[ -e source2.txt ]] ; then
if [[ "$verbose_mode" == TRUE ]] ; then
echo 'source1.txt does not exist, but source2.txt does.'
echo 'Copying source2.txt to destination.txt.'
fi
cp source2.txt destination.txt
else
if [[ "$verbose_mode" == TRUE ]] ; then
echo 'Neither source1.txt nor source2.txt exists; exiting.'
fi
exit 1 # terminate the script with a nonzero exit status (failure)
fi
稍后,当我们学习 shell 函数时,我们将找到一种更紧凑的方式来表达这一点。(事实上,即使我们已经知道,也有一种更紧凑的方式来表达这一点:而不是将 $verbose_mode 设置为 TRUE 或 FALSE,我们可以将 $echo_if_verbose_mode 设置为 echo 或 :,其中冒号 : 是一个什么也不做的 Bash 内置命令。然后我们可以用 "$echo_if_verbose_mode" 替换所有 echo 的使用。然后,像 "$echo_if_verbose_mode" message 这样的命令将变成 echo message,打印 message,如果 verbose 模式开启,否则将变成 : message,什么也不做,如果 verbose 模式关闭。但是,这种方法可能比真正值得的更令人困惑,因为目的很简单。)
要将多个条件与“与”或“或”组合,或使用“非”反转条件,我们可以使用我们已经看到的通用 Bash 符号。考虑这个例子
#!/bin/bash
if [[ -e source.txt ]] && ! [[ -e destination.txt ]] ; then
# source.txt exists, destination.txt does not exist; perform the copy:
cp source.txt destination.txt
fi
测试命令 [[ -e source.txt ]] && ! [[ -e destination.txt ]] 使用了我们上面看到的基于退出状态的 && 和 ! 运算符。[[ condition ]] 如果 condition 为真,则“成功”,这意味着 [[ -e source.txt ]] && ! [[ -e destination.txt ]] 只有在 source.txt 存在时才会运行 ! [[ -e destination.txt ]]。此外,! 反转了 [[ -e destination.txt ]] 的退出状态,因此 ! [[ -e destination.txt ]] 如果且仅当 destination.txt 不存在 时才“成功”。最终结果是 [[ -e source.txt ]] && ! [[ -e destination.txt ]] 如果且仅当 source.txt 存在 且 destination.txt 不存在 时才“成功”——“真”。
构造 [[ … ]] 实际上对这些运算符有内置的内部支持,这样我们也可以这样写上面代码
#!/bin/bash
if [[ -e source.txt && ! -e destination.txt ]] ; then
# source.txt exists, destination.txt does not exist; perform the copy:
cp source.txt destination.txt
fi
但是,通用符号通常更清晰;当然,它们可以与任何测试命令一起使用,而不仅仅是 [[ … ]] 构造。
上面示例中的if语句经过格式化,以便人类易于阅读和理解。这不仅对书中的示例很重要,对现实世界中的脚本也很重要。具体而言,上述示例遵循以下约定
if语句中的命令以一致的量缩进(恰好缩进两个空格)。这种缩进对 Bash 来说无关紧要——它忽略行首的空白——但对人类程序员来说非常重要。如果没有它,就很难看清if语句的开始和结束位置,甚至很难看出有if语句。当if语句嵌套在if语句中(或其他各种类型的控制结构中,我们将在后面看到)时,一致的缩进就变得更加重要。- 分号字符
;用在then之前。这是一个用于分隔命令的特殊运算符;它在大多数情况下等同于换行符,尽管存在一些差异(例如,注释始终从#运行到行尾,而不是从#运行到;)。我们可以将then放在新行的开头,这完全没问题,但对单个脚本来说,保持一致的风格比较好;对普通结构使用单一一致的外观,可以更容易地注意到不寻常的结构。在现实世界中,程序员通常将; then放在if或elif行的末尾,所以我们在这里也遵循了这一约定。 - 在
then和else之后使用换行符。这些换行符是可选的——它们不需要(也不能)用分号替换——但它们通过视觉上突出显示if语句的结构来提高可读性。 - 常规命令之间用换行符分隔,而不是用分号。这是一个通用的约定,并不特定于
if语句。将每个命令放在自己的行上,可以让用户更容易地“浏览”脚本并大致了解其作用。
这些确切的约定并不特别重要,但遵循一致且易读的代码格式约定是好的。当其他程序员查看您的代码时——或者您在写完代码两个月后查看您的代码时——不一致或不合理的格式会导致难以理解代码的含义。