跳转到内容

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 中,这也适用于算术表达式(我们将在后面看到)。但在命令级别,情况恰恰相反:退出状态为零表示“成功”或“真”,而非零退出状态表示“失败”或“假”。


注意

确保在 [[]] 前后包含空格,以便 Bash 将其识别为单独的单词。类似于 if[[[[-e 之类的用法将无法正常工作。

if 语句

[编辑 | 编辑源代码]

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.txtdestination.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.txtsource2.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 设置为 TRUEFALSE,我们可以将 $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放在ifelif行的末尾,所以我们在这里也遵循了这一约定。
  • thenelse之后使用换行符。这些换行符是可选的——它们不需要(也不能)用分号替换——但它们通过视觉上突出显示if语句的结构来提高可读性。
  • 常规命令之间用换行符分隔,而不是用分号。这是一个通用的约定,并不特定于if语句。将每个命令放在自己的行上,可以让用户更容易地“浏览”脚本并大致了解其作用。

这些确切的约定并不特别重要,但遵循一致且易读的代码格式约定是好的。当其他程序员查看您的代码时——或者您在写完代码两个月后查看您的代码时——不一致或不合理的格式会导致难以理解代码的含义。

    华夏公益教科书