3.2 Shell 命令(Shell Commands)

一个简单命令,如“echo a b c”,是由其命令本身和若干参数(arguments)组成的,并使用空格作为分隔符。

更复杂的 shell 命令是由若干简单命令以多种方式排列组合起来的:如一个命令的输出作为另一命令的输入的管道形式,或在一个循环和条件结构中,再或在其它形式的命令组合中。

3.2.1 保留字(Reserved Words)

保留字是 shell 中具有特殊意义的单词。它们用在 shell 复合命令的开头或结尾。

下列单词如果未被引起来并且是命令的第一个单词,则被认为是保留字(下面会说明一些例外):

if then elif else fi time

for in until while do done

case esac coproc select function

{ } [[ ]] !

例外:

“in”如果出现在“case”或“select”命令的第三个单词位置时,会被认为是保留字。

“in”和“do”出现在“for”命令的第三个单词位置时,才会被认为是保留字。

3.2.2 简单命令(Simple Commands)

简单命令是出现最多的一种命令。它只是由空白(blank)分隔的单词序列,并由一个 shell 控制操作符作(参见第 2 章《名词定义》)作为结束。简单命令中第一个单词一般指定的是用于执行的命令名,其后所有的单词作为这个命令的参数(arguments)。

简单命令的返回状态(参见章节 3.7.5《退出状态》)即是它的退出状态,是由 POSIX 1003.1 定义的 waitpid 函数提供的。或如果命令由信号“n”终止的,则为“n”+128 得到的值。

3.2.3 管道(Pipelines)

管道是由控制操作符“|”或“|&”将一个或多个命令连接起来的命令序列。其格式是:

[time [-p]] [!] command1 [ | or |& command2 ] …

每个命令的输出可通过管道连接到下一命令的输入中去。也就是说,每个命令会读取上一命令的输出。这个管道的连接会在该命令指定的任何重定向之前执行。

如果使用“|&”,command1 的标准报错和标准输出会通过管道连接到 command2 的标准输入中去;这是“2>&1 |”的简写方式。这种将标准报错合并到标准输出的隐式重定向会在这个命令的任意重定向之后执行。

保留字 time 会打印该管道完成后的时间统计信息。当前的统计信息是由这个命令执行时的使用时间(时钟时间)和用户及系统时间组成。“-p”选项将输出格式改为 POSIX 格式。当 shell 运行于 POSIX 模式(参见章节 6.11《Bash POSIX 模式》)时,如果 time 后面跟着标记“-”时是不会认为 time 是保留字。变量“TIMEFORMAT”用于指定时间统计信息的显示格式,可参见 5.2 章节《Bash 变量》关于可用格式的说明。time 作为保留字使用时,可以用于测量 shell 内建命令、函数及管道的运行时间。外部命令“time”不能很容易地实现命令运行时间的测量。

在 POSIX 模式时,time 可能直接跟随一个换行。在这种情况下,shell 会显示 shell 和子 shell 的用户和系统消耗时间的总合。变量“TIMEFORMAT”可用于指定时间信息格式。

如果管道未运行于异步模式(参见 3.2.4《列表》)下,shell 会等等管道中所有命令执行完成。

在管道中执行的每一个命令都会在其拥有的子 shell 中执行,也就是使用分开的处理进程(参见 3.7.3《命令执行环境》)。如果使用内建命令 shopt 开启了“lastpipe”选项,则管道中最后一个元素则运行于 shell 进程中,而不是子 shell 中。

管道中最后一条命令的退出状态是该管道的退出状态,除非开启了“pipefail”选项(参见 4.3.1《设置内建命令》)。如果开启了“pipefail”,管道的返回状态是最后(即最右边)一条命令的非零退出状态,或所有命令都成功退出才为零状态。如果保留字“!”出现在管道之前,退出状态则取为上面所描述的退出状态的逻辑反。shell 会等待管道中所有命令完成后才会返回退出状态值。

3.2.4 命令列表(Lists of Commands)

命令列表是由一个或多个管道组成的命令序列,其是使用操作符“;”、“&”、“&&”或“||”作为分隔符,和使用“;”、“&”或换行(newline)作为终止符的。

在这些操作符中,“&&”和“||”具有相同的优先级,之后是“;”和“&”具有相同的优先级。

在列表中出现的一个或多个换行可以用于分隔命令,其作用等同于分号。

如果命令结束于控制符“&”时,shell 则在子 shell 中异步执行该命令。这也就是在后台(background)执行命令,并且这些命令也被称为异步命令。shell 不会等待这些命令完成,并且返回状态零(即真值)。当作业控制没有激活(参见第 7 章《作业控制》),在缺少显式重定向的用于异步命令的标准输入,会从“/dev/null”进行重定向输入。

使用分号“;”进行分隔的多条命令是按顺序执行的;shell 会轮流等待每一个命令执行完成。返回状态是最后一条命令的退出状态。

“与列表”和“或列表”分别使用控制操作符“&&”和“||”分隔的一个或多个管道命令所组成的。“与列表”和“或列表”列表执行时遵循左结合性。

“与列表”的格式是command1 && command2。只有当 command1 执行后返回状态为零(即成功)后,才能执行 command2。

“或列表”格式是command1 || command2。只有当 command1 返回状态为非零时,才会执行 command1。

“与列表”和“或列表”的返回状态是其最后一条命令的返回状态。

3.2.5 复合命令(Compound Commands)

复合命令是 shell 编程语言的结构体。每一个结构体是由一个保留字或控制操作符作为开始,并且结束于一个相应的保留字或操作符。任意关联于复合命令的重定向都应用到复合命令中的每一个命令,除非显式地重写它。

在大多数情况下,在复合命令中的命令列表都是使用一个或多个换行来与其它命令进行分隔的,并且使用换行代替末尾的分号。

Bash 提供循环结构体、条件判断命令和命令成组机制,并且将其作为一个单元执行。

3.2.5.1 循环结构体(Looping Constructs)

Bash 支持以下循环结构体。

注意,无论下面命令语法中在哪里出现的分号“;”,其都可以使用换行进行替代。

until

语法:

until test-commands; do consequent-commands; done

如果测试命令“test-commands”的退出状态不为零,则执行命令体“consequent-commands”里的命令。这个结构体的返回状态是其命令体里最后一条命令的退出状态,或如果什么都没执行则是零。

while

语法:

while test-commands; do consequent-commands; done

当测试命令“test-commands”退出状态为零时,才执行命令体“consequent-commands”里的命令。这个结构体的返回状态是其命令体里最后一条命令的退出状态,或如果什么都没执行则是零。

for

语法:

for name [ [ in [words ...] ] ; ] do commands; done

使用“name”所代表的变量轮流绑定到由“words”扩展生成的多个元素,并为每一个当前元素执行一遍“commands”执行体里的命令。如果“in words”没有给出,for 命令则将位置参数作为元素列表,就像使用了“in ”$@“”一样。(参见章节 3.4.2《特殊参数》)。

返回状态是其最后一条命令的退出状态。如果“words”扩展没有生成任何元素,则不会执行执行体里的命令,并且返回状态为零。

还有另外一种语法格式:

for (( expr1; expr2; expr3 )) ; do commands ; done

首先,算术表达式“expr1”按照算术规则(参见 6.5《Shell 算术运算》)进行运算。接着算术表达式“expr2”进行重复性运算,直到其运算得到返回值零。每一次“expr2”运算得到一个非零值时,都要执行命令体“commands”里的命令对“expr3”进行一次运算。如果省略了任意一个表达式,则其运算值就被视为 1。返回值是命令体最后一条命令的退出状态,或返回假值如果任意一个表达式是无效的。

内建命令“break”和“continue”(参见 4.1《Bourne Shell 内建命令》)可以用于控制循环的执行。

3.2.5.2 判断结构体(Conditional Constructs)

if

语法:

if test-commands; then
    consequent-commands;
[elif more-test-commands; then
    more-consequents;]
[else alternate-consequents;]
fi

如果执行测试命令“test-commands”后返回状态为零,则执行命令体“consequent-commands”。如果测试命令“test-commands”执行返回非零状态,则轮流执行“elif”语句体。如果该语句的测试命令“more-test-commands”返回状态零,则执行相应的“more-consequents”命令体并完成整个判断命令。如果“else alternate-consequents”出现,并且所有“if”和“elif”语句都是非零的退出状态,则执行“alternate-consequents”命令体。该结构体的返回状态是最后一条命令的退出状态,或如果条件测试没有成功则返回零。

case

语法:

case word in
    [ [(] pattern [| pattern]...) command-list ;;]...
esac

“case”语句将会有选择地执行第一个匹配到“word”的样式“pattern”相对应的命令列表“command-list”。执行的匹配规则请参见 3.5.8.1《样式匹配》。如果开启了 shell 选项“nocasematch”(详见 4.3.2《内建命令 shopt》命令 shopt 描述),将使用字母大小写不敏感的匹配。“|”用于分隔多个样式,并且“)”代表样式列表结束。一个样式列表和其相关联的命令列表“command-list”被称作一个子句(clause)。

每一个子句必须使用“;;”、“;&”或“;;&”表示结束。在进行匹配之前,“word”可以由波浪号扩展、参数扩展、命令替换、算术扩展和引用移除(以上扩展请参见 3.5.3《Shell 参数扩展》)运算生成。每一个样式“pattern”也可进行波浪号扩展、参数扩展、命令替换和算术扩展。

这里可以有任意多个“case”子句,每一个子句结束符都可以是“;;”、“;&”或“;;&”。第一个匹配到的样式来确定执行哪个相关的命令列表。有一种通用习惯,使用星号“*”作为“case”语句的默认匹配样式,也就是该样式总是会被匹配到。

下面的例子是使用“case”语句对感兴趣的动物进行选择的脚本:

echo -n "Enter the name of an animal: "
read ANIMAL
echo -n "The $ANIMAL has "
case $ANIMAL in
  horse | dog | cat) echo -n "four";;
  man | kangaroo ) echo -n "two";;
  *) echo -n "an unknown number of";;
esac
echo " legs."

如果使用了“;;”操作符,在第一个匹配到的样式之后,将不会再进行后继的样式匹配。如果使用“;&”替换了“;;”,则该子句执行完成后会继续直接执行下一个子句中的命令(如果有的话)而不需要进行后继的匹配。如果使用“;;&”替换了“;;”,则会对下一个子句(如果存在的话)进行匹配测试,如果匹配成功则执行其相应的命令;“case”语句会继续执行,就像样式列表没有匹配到一样。

如果没有任何样式匹配成功,则返回状态零。否则,返回执行命令列表的退出状态。

select

“select”结构体可以很容易地生成菜单。它的语法格式和“for”命令差不多。

语法:

select name [ in words ...]; do commands; done

在保留字“in”后面的“words”列表会先进行扩展,生成一个单词列表。扩展的“words”集合会打印到标准报错输出流中,并且每个单词前面会加入数字序号。如果在语法中省略了“in words”,则就像指定了“in ”$@“”一样,会将位置参数打印出来。之后会以变量“PS3”的值作为提示符进行从标准输入的读行操作。如果在输入行输入了显示列表中的一个条目所对应的数字,则相应的单词就被赋值给了变量“name”。如果输入行是空的,这个单词列表和提示符会重新显示。如果读到了“EOF”,则“select”命令完成。读取的任何其它值将使“name”设置为空。这个输入行读取的内容会被保存到变量“REPLY”中。

在每一次选择之后都会执行命令集“commands”,直到执行“break”命令才会完成“select”命令。

下面的例子是让用户从当前目录选择一个文件名,并打印该文件名和选择序号。

select fname in *;
do
  echo you picked $fname \($REPLY\)
  break;
done

((…))

(( expression ))

算术表达式会依照其运算规则(参见 6.5《Shell 算术运算》)进行运算。如果表达式的值不为零,则返回状态为零;否则返回状态为一。该形式等同于“let “expression””。可参见 4.2《Bash 内建命令》关于 let 的完整描述。

[[…]]

[[ expression ]]

条件判断表达式返回状态为零或一,这完全取决于条件表达式“expression”的运算结果。该表达式的详细描述可参考 6.4《Bash 条件表达式》。不会将“[[”与“]]”之间的单词进行单词分割和文件名扩展;但会执行波浪号扩展、参数和变量扩展、算术扩展、命令替换、进程替换、和引用移除。条件操作符,如“-f”,只有在未被用引号引起来时才能被认为是操作符。当“<”和“>”号在“[[”后使用时,才会作为使用当前语言区域的按字典排序操作符使用。

当使用“==”和“!=”操作符时,在操作符右边的字符串被认为是一个样式,并且依照 3.5.8.1《样式匹配》中的规则进行匹配,就像开启了“extglob”选项一样。“=”与“==”操作符是一样的。如果开启了 shell 选项“nocasematch”(详见 4.3.2《内建命令 shopt》),匹配时则大小写不敏感。当字符串与样式匹配(使用“==”时)或不匹配(使用“!=”时)时返回状态为零,反之返回状态为一。在样式中任意部分如果被引号括起来,则代表这部分必须精确匹配该部分字符串。

还有一个二元操作符“=~”也是可用的,其与“==”和“!=”具有相同的优先级。当使用它时,操作符右边的字符串被认为是 POSIX 扩展的正则表达式并依据其规则进行匹配(可在 regex(3)中查看 POSIX 的 regcomp 和 regexec 接口的详细描述)。如果字符串匹配了样式则返回值为 0,否则为 1。如果正则表达式有语法错误,则判断表达式返回值是 2。如果开启了 shell 选项“nocasematch”(详见 4.3.2《内建命令 shopt》),匹配时则大小写不敏感。在样式中任意部分如果被引号括起来,则代表这部分必须精确匹配该部分字符串。在正则表达式中使用带有中括号表达式时要特别小心,毕竟在中括号之间的正常引用字符会丢失其意义。如果样式存储于一个 shell 变量中,将变量表达式引起来会将整个样式作为字符串进行强制匹配。

如果样式匹配到字符串的任意部分,就算其匹配成功。在样式中使用正则表达式的锚点操作符“^”和“$”会进行整个字符串的强制匹配。数组变量“BASH_REMATCH”记录了匹配到样式的字符串部分。索引为 0 的“BASH_REMATCH”元素记录的是匹配了整个正则表达式的字符串部分。在正则表达式中由小括号引起来的子表达式的匹配字符串存储于变量“BASH_REMATCH”的后面索引中。“BASH_REMATCH”的索引 n 位,存储着第 n 次小括号子表达式匹配的字符串部分。

例如,下面的表达式将存储于变量“line”中的字符串进行匹配。匹配的是一串包含“space”字符类中的任意个(包括零个)字符,和一个或零个字母“a”和一个字母“b”。

[[ $line =~ [[:space:]]*(a)?b ]]

这表示像“aab”和“aaaaaab”的字符串都会被匹配,如同一行中字母“b”出现在任何位置一样。

将正则表达式存储于变量中是一种常用手段。其可以避免引用 shell 特殊字母的问题。有时很难在注意使用 Shell 引用移除(Quote Removal)的同时,不使用引号而正确地指定正则表达式,或跟踪正则表达式引用。使用 shell 变量来存储样式就减少了这种问题的发生。例如,下面的语句等同于上面的语句:

pattern='[[:space:]]*(a)?b'

[[ $line =~ $pattern ]]

如果你想要匹配一个正则表达式语法中的一个特殊字符时,必须使用引用将其特殊含义去除。这就是说在样式“xxx.txt”中的点符号“.”在正则表达式中代表可匹配任意一个字符,但在被双引号引起来的样式““xxx.txt””中的点符号只匹配其本身。Shell 程序员要特别注意反斜线的使用,其可以在 shell 和正则表达式中用于移除后面字符的特殊意义。下面两组命令是不相等的:

pattern='\.'

[[ . =~ $pattern ]]
[[ . =~ \. ]]

[[ . =~ "$pattern" ]]
[[ . =~ '\.' ]]

第一组匹配是成功的,但第二组不会。因为在第二组的反斜线作为样式的一部分参加匹配。在第一组样例中,反斜线移除了点符号“.”的特殊含义,所以字母“.”匹配到了。如果在第一组中的字符串不是字母“.”,比如是字母“a”,样式是不会匹配到的。因为在样式中的“.”失去了其用于匹配任意字符的特殊含义。

表达式可以使用下列操作符进行混合使用,优先级按列出的顺序做降序:

( expression )

返回表达式的值。这可用于覆盖正常的操作符优先级。

! expression

如果表达式为假则返回真。

expression1 && expression2

如果两个表达式都为真则返回真。

expression1 || expression2

如果两个表达式中有一个为真,则返回真。

在使用“&&”和“||”操作符时,如果“expression1”的值可以充分确定整个条件表达式的返回值时,其不会对“expression2”再进行运算。

3.2.5.3 成组命令(Grouping Commands)

Bash 提供两种方法将要执行的命令列表组成一个单元。当命令被组成到一起,重定向可应用于整个命令列表。例如,可将所有命令列表中的命令输出重定向到一个流中。

( list )

将命令列表置于小括号中执行时,会先创建一个子shell环境(参考3.7.3《命令执行环境》),然后将列表中的每一个命令都执行于这个子shell中。在子shell执行期间所进行的变量赋值,会在子shell完成之后失效。

{ list; }

将命令列表置于大括号中,这些命令将在当前shell上下文中执行,而不会创建子shell。命令列表后面的分号(或换行)是必须出现的。

除了在这两种结构体中有创建子 shell 的不同,还有一个由于历史原因的微妙的不同。大括号属于保留字,所以其必须和命令列表用空白或其它 shell 元字符分开。小括号是操作符,是被 shell 认可的分隔标记,其可以不使用空格与命令列表隔开。

这两种结构体的退出状态即是命令列表的退出状态。

3.2.6 协同处理(Coprocesses)

协同处理是一个由保留字“coproc”开头的 shell 命令。一个协同处理是在一个子 shell 中异步执行的,就像在命令结尾使用“&”控制操作符一样。并且在执行 shell 和协同处理进程之间建立一个双向管道。

命令格式:

coproc [NAME] command [redirection]

这个命令会创建一个名为“NAME”的协同处理。如果没有给出“NAME”,则使用默认名“COPROC”。如果“command”是一个简单命令,就不能给出“NAME”;否则,会将其解释为简单命令的第一个单词。

当协同处理执行时,shell 会在执行 shell 上下文中创建一个名为“NAME”的数组变量(参见 6.7《数组》)。“command”的标准输出会通过管道连接到执行 shell 中的文件描述符,该描述符分配到“NAME[0]”上。“command”的标准输入会通过管道连接到执行 shell 中的文件描述符,该描述符分配到“NAME[1]”上。该管道会在由命令指定的任意重定向之前建立。文件描述符可以作为 Shell 命令的参数使用并且重定向使用标准单词扩展。不同于用于执行命令和进程替换创建的文件描述符,其在子 shell 中是不可用的。

shell 产生的用于执行协同处理的进程 ID 存储于变量“NAME_PID”中。内建命令“wait”可用于等待协同处理完成。

一旦创建出作为异步命令的协同处理,其“coproc”命令总会反回成功状态。协同处理的返回状态即是“command”的退出状态。

3.2.7 GNU 并行(GNU Parallel)

这里还有一些非 Bash 内建命令的方法来并行运行命令。GNU 并行就是其中之一。

GNU 并行,就像其名字一样,可以以并行方式构建和运行命令。你可以使用不同的参数,如不同的文件名、用户名、主机名或从文件读取的行,来运行一个相同的命令。GNU 并行提供一种快捷方式可引用大多数常规操作(如,多输入行,输入行的不同部分,用于指定输入源的不同方式等等)。GNU 并行可以替代“xargs”或从其输入源向多个不同的 Bash 实例提供命令。

请参考 GNU 并行文档对于其完整解释。一些示例可提供其使用的简单介绍。

例如,GNU 并行可以很容易地代替“xargs”运行“gzip”来将当前及其子目录中的所有“html”文件压缩。

find . -type f -name '*.html' -print | parallel gzip

如果你需要保护文件名中的特殊字符,如换行(newline),你可以使用“find”命令的“-print0”选项及“parallel”命令的“-0”选项。

当需要调用“mv”从当前目录来移动多个大文件时,你就可以使用并行来处理:

printf '%s\n' * | parallel mv {} destdir

正如你看到的,“{}”会替换为从标准输入读取的每一行。在大多数使用“ls”的实例时,其不足以处理所有的文件名。而“printf”是一个 shell 内建命令,并且其不会受限于内核对于程序的参数个数限制,所以你可以使用“*”(但请参考关于 shell 选项“dotglob”)。如果你的文件名中有特殊字符,可以使用上面略微提到的“-0”选项:

printf '%s\0' * | parallel -0 mv {} destdir

这样在当前目录有多少个文件就会运行多少个“mv”命令。你可以添加“-X”选项来模拟并行“xargs”命令:

printf '%s\0' * | parallel -0 -X mv {} destdir

(如果你开启了“dotglob”选项,则你需要修改“printf”的样式)

GNU 并行能将一些从一个文件读取的用于行操作的常用习惯(在这种情况下,每一行列出一个文件名):

while IFS= read -r x; do
do-something1 "$x" "config-$x"
do-something2 < "$x"
done < file | process-output

替换为更为紧凑的“lambdas”语法:

cat list | parallel "do-something1 {} config-{} ; do-something2 < {}" | process-output

并行提供一种可将文件名扩展移除的内建机制,将其自身提供给批处理文件转换或重命名:

ls *.gz | parallel -j+0 "zcat {} | bzip2 >{.}.bz2 && rm {}"

该命令将当前目录中的以“.gz”结尾的所有文件,使用“bzip2”命令进行重新压缩,每个 CPU(-j+0)运行一个作业的方式并行运行。(在这里我们简略地使用了“ls”命令;在上面如果使用“find”命令,会在文件名包含意外字符时更为健壮。)并行可以从命令行获取参数;上面的命令可写成:

parallel "zcat {} | bzip2 >{.}.bz2 && rm {}" ::: *.gz

如果一个命令产生了输出,你可能想要按命令的输入顺序进行输出。例如,下面的命令

{
    echo foss.org.my;
    echo debian.org;
    echo freenetproject.org;
} | parallel traceroute

将按“traceroute”命令先完成的顺序显示输出结果。加入“-k”选项

{
    echo foss.org.my;
    echo debian.org;
    echo freenetproject.org;
} | parallel -k traceroute

会确保第一个显示的是“traceroute foss.org.my”的输出结果。

最终,GNU 并行可同时运行类似于“cat file | bash”的 shell 命令序列。其不会以通常的顺序方式先获取文件列表,创建一系列用于操作的命令,并最后将命令列表提供给一个 shell 运行。并行会加速该运行过程。假设“file”文件包含一个 shell 命令列表,每行一条命令,

parallel -j 10 < file

将会一次以十个 shell 作业为单位来使用 shell 运行命令。(虽然没有显式地将命令作为参数提供。)