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
语句。将每个命令放在自己的行上,可以让用户更容易地“浏览”脚本并大致了解其作用。
这些确切的约定并不特别重要,但遵循一致且易读的代码格式约定是好的。当其他程序员查看您的代码时——或者您在写完代码两个月后查看您的代码时——不一致或不合理的格式会导致难以理解代码的含义。