3.2 Shell指令

评论 0 浏览 0 2022-12-30

一个简单的shell命令,如echo a b c,由命令本身和参数组成,参数之间用空格分隔。

更复杂的shell命令是由简单的命令组成的,它们以不同的方式排列在一起:在一个管道中,一个命令的输出成为第二个命令的输入,在一个循环或条件结构中,或在其他一些分组中。

1 保留字

保留字是对shell有特殊意义的字,它们被用来开始和结束shell的复合命令。

以下词语在未加引号的情况下被确认为保留词,并且是命令的第一个词(例外情况见下文)。

ifthenelifelsefitime
forinuntilwhiledodone
caseesaccoprocselectfunction
{}[[]]!

如果incaseselect命令中的第三个字,则被认定为保留字。如果indofor命令中的第三个字,则被认定为保留字。

2 简单的命令

简单命令是最常遇到的一种命令,它只是一串由空格分隔的单词,以shell的一个控制操作符为结尾(见2 定义)。第一个词通常指定一个要执行的命令,其余的词是该命令的参数。

简单命令的返回状态(见5 退出状态)是它的退出状态,由POSIX1003.1waitpid函数提供,如果命令是由信号n终止的,则为128+n

3 管道

一个pipeline是由一个控制运算符‘|’或‘|&’隔开的一个或多个命令的序列。

一个管道的格式是

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

管道中每条命令的输出都通过管道连接到下一条命令的输入。 也就是说,每条命令都会读取前一条命令的输出。这种连接是在任何由command1指定的重定向之前进行的。

如果使用了‘|&’,command1的标准错误,除了其标准输出外,还通过管道连接到command2的标准输入;它是2>&1 |的缩写。 这种将标准错误重定向到标准输出的隐式操作是在command1指定的任何重定向后进行的。

保留字time会使管道完成后的时间统计被打印出来。 这些统计目前包括经过的(壁钟)时间和命令执行所消耗的用户和系统时间。 -p选项将输出格式改为POSIX指定的格式。当shell处于POSIX模式时(见6.11 Bash的POSIX模式),如果下一个标记以‘-’开头,它就不会将time作为一个保留字来识别。 TIMEFORMAT变量可以被设置为一个格式字符串,指定如何显示时间信息。参见5.2 Bash 变量,以了解可用格式的描述。 使用time作为保留字允许对shell内置程序、shell函数和管道进行计时。外部的time命令不能轻易地为这些东西计时。

当shell处于POSIX模式时(见6.11 Bash的POSIX模式),time后面可能有一个换行。在这种情况下,shell会显示shell及其子程序所消耗的用户和系统总时间。 TIMEFORMAT变量可以用来指定时间信息的格式。

如果流水线不是异步执行的(见4 命令列表),shell将等待流水线中的所有命令完成。

多命令流水线中的每条命令,在创建管道时,都在其自身的subshell中执行,这是一个独立的进程(见3 命令的执行环境)。 如果使用shopt内建程序启用了lastpipe选项(见2 Shopt 内置程序),当作业控制未激活时,流水线的最后元素可以由shell进程来运行。

管道的退出状态是管道中最后一条命令的退出状态,除非启用了pipefail选项(见1 Set 内置程序)。 如果启用了pipefail,管道的返回状态是最后一条(最右边)命令的值,以非零状态退出,如果所有命令都成功退出,则为零。如果保留字‘!’在管道之前,则退出状态是上述退出状态的逻辑否定。 shell会等待管道中的所有命令结束后再返回一个值。

4 命令列表

一个list是一个或多个管道的序列,由一个操作符‘;’、‘&’、‘&&’,或‘||’分割,并可选择以‘;’,‘&’,或newline中的一个来结束。

在这些列表操作符中,‘&&’和‘||’具有相同的优先权,其次是‘;’和‘&’,它们具有相同的优先权。

一个或多个换行符可以出现在list中,用来限定命令,相当于一个分号。

如果一个命令被控制操作符‘&’终止,shell会在一个子shell中异步地执行该命令。 这被称为在后台中执行命令,这些被称为异步命令。shell不等待命令的完成,返回状态为0(真)。 当作业控制没有激活时(见7 作业控制),异步命令的标准输入,在没有任何显式重定向的情况下,会从/dev/null重定向。

用‘;’隔开的命令是按顺序执行的;shell依次等待每个命令的终止。返回状态是最后执行的命令的退出状态。

ANDOR列表是一个或多个管道的序列,分别由控制运算符‘&&’和‘||’分开。ANDOR列表的执行具有左联想性。

一个and列表的形式是

command1 && command2 

当且仅当command1返回退出状态为0(成功)时,command2才会被执行。

一个OR列表的形式是

command1 || command2 

当且仅当command1返回一个非零的退出状态时,command2被执行。

ANDOR列表的返回状态是列表中最后一个执行的命令的退出状态。

5 复合命令

复合命令是shell编程语言的结构,每个结构以一个保留字或控制运算符开始,并以相应的保留字或运算符结束。 任何与复合命令相关的重定向(见重定向)都适用于该复合命令中的所有命令,除非明确地被覆盖。

在大多数情况下,复合命令的描述中的命令列表可以用一个或多个换行符与命令的其余部分隔开,并且可以用换行符代替分号。

Bash提供了循环结构、条件性命令,以及将命令分组并作为一个单元执行的机制。

5.1 循环结构

Bash支持以下的循环结构。

请注意,凡是在命令语法描述中出现的‘;’,都可以用一个或多个换行符来代替。

until

until命令的语法是:

until test-commands; do consequent-commands; done 

只要test-commands的退出状态不是0,就执行consequent-commands。 返回状态是consequent-commands中最后执行的命令的退出状态,如果没有执行任何命令,则返回0。

while

while命令的语法是:

while test-commands; do consequent-commands; done 

只要test-commands的退出状态为0,就执行consequent-commands。 返回状态是consequent-commands中执行的最后一条命令的退出状态,如果没有执行任何命令,则返回0。

for

for命令的语法是:

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

扩展(参见3.5 Shell的扩展),并对结果列表中的每个成员执行一次commandsname与当前成员绑定。如果“in words”不存在,for 命令会对设置的每个位置参数执行一次commands,就好像已经指定了‘in "$@"‘(参见2 特殊参数)。

返回状态是最后执行的命令的退出状态。 如果在words的扩展中没有任何项目,则不执行任何命令,返回状态为0。

也支持for命令的另一种形式。

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

首先,算术表达式expr1根据下面描述的规则进行评估(见6.5 shell算术)。 然后,算术表达式expr2被反复评估,直到它评估为零。每当expr2求值为非零时,command被执行,算术表达式expr3被评估。如果任何表达式被省略,它的行为就像它被评估为1一样。返回值是commands中最后一个被执行的命令的退出状态,如果任何表达式无效,则为false。

breakcontinue内置程序(见4.1 Bourne Shell内置程序)可用于控制循环的执行。

5.2 条件性结构

if

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-consequents,命令就完成了。如果‘else alternate-consequents’存在,并且最后一个ifelif子句中的最后一条命令的退出状态为非零,则执行alternate-consequents。 返回状态是最后一条命令的退出状态,如果没有条件测试为真,则返回零。

case

case命令的语法是:

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

case将有选择地执行command-list,对应于与word相匹配的第一个pattern。 匹配是根据下面8.1 模式匹配中描述的规则进行的。 如果nocasematch shell选项(见2 Shopt 内置程序中关于shopt的描述)被启用,匹配将不考虑字母字符的大小写。‘|’用于分隔多个模式,而‘)’操作符用于终止一个模式列表。 一个模式列表和一个相关的命令列表被称为clause

每个子句必须以‘;;’、‘;&’或‘;;&’结尾。 在尝试匹配之前,经历了tilde扩展、参数扩展、命令替换、算术扩展和引号删除(参见3 Shell参数扩展)。每个pattern都要经过倾斜符号扩展、参数扩展、命令替换、算术扩展、过程替换和引号删除。

可以有任意数量的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."

如果使用了‘;;’操作符,那么在第一个模式匹配之后就不会再尝试后续的匹配。 使用‘;&’来代替‘;;’会使执行继续与下一个子句相关的command-list,如果有的话。使用‘;;&’代替‘;;’会使shell测试下一个子句中的模式(如果有的话),并在成功匹配后执行任何相关的command-list,继续执行case语句,就像模式列表没有匹配过一样。

如果没有模式被匹配,则返回状态为0。否则,返回状态是所执行的command-list的退出状态。

select

select结构允许轻松地生成菜单。 它的语法与for命令几乎相同。

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

in后面的单词列表被展开,生成一个项目列表,展开的单词集合被打印在标准错误输出流中,每个单词前面都有一个数字。如果省略了‘in words’,则打印位置参数,就像指定了‘in "$@"’一样。 select然后显示PS3提示,并从标准输入读取一行。如果该行由一个数字组成,对应于所显示的一个词,那么name的值将被设置为该词。 如果该行是空的,则再次显示该词和提示。 如果EOF被读取,则select命令完成并返回1。 任何其他读取的值将导致name被设置为空。 读取的行被保存在变量REPLY

命令在每次选择后都会被执行,直到break命令被执行,这时select命令就完成了。

下面是一个例子,它允许用户从当前目录中挑选一个文件名,并显示所选文件的名称和索引。

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

运算式表达式根据下面描述的规则进行评估(见6.5 shell算术),表达式经历了与双引号内相同的扩展,但表达式中的双引号字符不会被特别处理被删除。 如果表达式的值非零,返回状态为0;否则返回状态为1。

[[…]]
[[ expression ]] 

根据对条件表达式表达式的评估,返回0或1的状态。 表达式由下面6.4 Bash的条件表达式中描述的主要内容组成。 [[]]之间的字不进行分词和文件名扩展。shell对这些词进行tilde扩展、参数和变量扩展、算术扩展、命令替换、进程替换和去引号(如果这些词被括在双引号中会出现的扩展)。 条件性操作符,如‘-f’必须不加引号才能被识别为主语。

当与[[一起使用时,‘<’和‘>’操作符使用当前的locale进行词法排序。

当使用‘==’和‘!=’操作符时,操作符右边的字符串被视为模式,并根据下面8.1 模式匹配中描述的规则进行匹配,就像启用extglob壳选项一样。 ‘=’操作符与‘==’相同。如果启用了nocasematch shell选项(见2 Shopt 内置程序中对shopt的描述),则在进行匹配时不考虑字母字符的大小写。 如果字符串匹配(‘==’)或不匹配(‘!=’)该模式,则返回值为0,否则为1。

如果你使用shell的任何引用机制来引用模式的任何部分,被引用的部分就会按字面意思进行匹配。 这意味着被引用部分的每个字符都会自我匹配,而不是有任何特殊的模式匹配意义。

还有一个二进制运算符,‘=~’,与‘==’和‘!=’的优先级相同。当你使用‘=~’时,操作符右边的字符串被认为是POSIX扩展的正则表达式模式,并进行相应的匹配(使用通常在regex(3)中描述的POSIX regcompregexec接口)。如果字符串与模式匹配,返回值为0,如果不匹配,返回值为1。 如果正则表达式在语法上不正确,则条件表达式返回2。如果nocasematch shell选项(见2 Shopt 内置程序中对shopt的描述)被启用,匹配时不考虑字母字符的大小写。

你可以引用模式的任何部分,以强制对被引用的部分进行字面匹配,而不是作为一个正则表达式(见上文)。 如果模式存储在一个shell变量中,引用变量的扩展部分可以强制对整个模式进行字面匹配。

如果该模式与字符串的任何部分相匹配,它就会匹配。 如果你想强制该模式匹配整个字符串,请使用‘^’和‘$’正则表达式操作符来锚定该模式。

例如,下面将匹配一行(存储在shell变量line中),如果在该值的任何地方有一个由任何数量(包括零)的space字符类的字符组成的序列,紧接着是零或一个‘a’的实例,然后是一个‘b’的实例。

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

这意味着line的值如‘aab’、‘ aaaaaab’、‘xaby’和‘ ab’都会被匹配,正如其值中任何地方含有‘b’的行一样。

如果你想匹配一个对正则表达式语法来说比较特殊的字符(‘^$|[]()\.*+?’),就必须对其进行引号处理以消除其特殊含义。 这意味着在模式‘xxx.txt’中,‘.’可以匹配字符串中的任何字符(其通常的正则表达式含义),但在模式‘"xxx.txt"’中,它只能匹配一个字面的‘.’。

同样,如果你想在你的模式中包含一个对正则表达式语法有特殊意义的字符,你必须确保它没有被引用。 例如,如果你想在字符串的开头或结尾锚定一个模式,你不能使用任何形式的shell引号来引用‘^’或‘$’字符。

如果你想在一行的开头匹配‘initial string’,下面的方法就可以了。

[[ $line =~ ^"initial string" ]] 

但这不会:

[[ $line =~ "^initial string" ]] 

因为在第二个例子中,‘^’是被引用的,并没有其通常的特殊含义。

有时很难在不使用引号的情况下正确指定正则表达式,或者在注意shell引号和shell’引号移除 "的情况下跟踪正则表达式使用的引号。 将正则表达式存储在shell变量中通常是一种有用的方法,可以避免shell中特殊的引号字符带来的问题。 例如,下面的内容相当于上面使用的模式。

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

Shell程序员应该特别注意反斜线,因为反斜线被shell和正则表达式用来删除后面字符的特殊含义。这意味着在shell的单词扩展完成后(见3.5 Shell的扩展),模式中最初未引用的部分中剩余的任何反斜杠都可以删除模式字符的特殊含义。如果模式的任何部分被引用,shell 会尽最大努力确保正则表达式将那些剩余的反斜杠视为文字,如果它们出现在引用的部分中。

以下两组命令是等价的:

pattern='\.'

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

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

前两个匹配会成功,但后两个不会,因为在后两个中反斜杠将成为要匹配的模式的一部分。 在前两个示例中,传递给正则表达式的模式解析器是“\.”。反斜杠删除了特殊含义‘.’,所以文字‘.’匹配。在后两个示例中,传递给正则表达式的模式解析器引用了反斜杠(例如,‘\\\.’),这将不匹配字符串,因为它不包含反斜杠。如果第一个例子中的字符串不是‘.’,比如说‘a’,模式将不匹配,因为模式中引用的‘.’失去了匹配任何单个字符的特殊含义。

正则表达式中的括号表达式也可能是错误的来源,因为通常在正则表达式中特殊的字符在括号之间失去了它们的特殊含义。 但是,您可以使用方括号表达式来匹配特殊模式字符而无需引用它们,因此它们有时可用于此目的。

虽然这看起来是一种奇怪的写法,但下面的模式将匹配字符串中的‘.’。

[[ . =~ [.] ]] 

在将模式传递给正则表达式函数之前,shell会执行任何单词扩展,所以你可以假设shell的引用优先。 如上所述,正则表达式分析器将根据自己的规则解释shell扩展后模式中剩余的任何未引用的反斜线。 目的是尽可能避免让shell程序员引用东西两次,所以shell引用应该足以在必要时引用特殊模式的字符。

数组变量BASH_REMATCH记录了字符串中哪些部分与模式相匹配。 索引为0的BASH_REMATCH元素包含了与整个正则表达式相匹配的字符串部分。 由正则表达式中的括号子表达式相匹配的子字符串被保存在其余的BASH_REMATCH索引中。 索引为nBASH_REMATCH元素是与第n个括号子表达式相匹配的字符串部分。

Bash在全局范围内设置了BASH_REMATCH,将其声明为局部变量会导致意想不到的结果。

表达式可以使用以下运算符进行组合,按优先级递减的顺序排列。

( expression )

返回expression的值。 这可以用来覆盖运算符的正常优先级。

! expression

如果expression是假的,则为 "真"。

expression1 && expression2

如果expression1expression2都为真,则为真。

expression1 || expression2

如果expression1expression2为真,则为真。

如果expression1的值足以确定整个条件表达式的返回值,那么&&||运算符就不会对expression2进行评估。

5.3 分组命令

Bash提供了两种方法来对命令列表进行分组,以便作为一个单元来执行。当命令被分组时, 重定向可以被应用到整个命令列表中。例如,列表中所有命令的输出都可以被重定向到一个流中。

()
( list ) 

将命令列表放在括号中,迫使shell创建一个子shell(见3 命令的执行环境),list中的每个命令都在该子shell环境中执行。 由于list在子shell中执行,变量赋值在子shell完成后不会保持有效。

{}
{ list; } 

将命令列表放在大括号之间会导致列表在当前shell上下文中被执行。list后面的分号(或换行)是必须的。

除了创建子shell之外,由于历史原因,这两个结构之间还有一个微妙的区别。大括号是保留字,所以它们必须用空格或其他shell元字符与list分开。 小括号是运算符,即使它们没有用空格与list分开,也会被shell识别为独立标记。

这两个结构体的退出状态都是list的退出状态。

6 协进程

coprocess是一个以coproc保留字开头的 shell 命令。 协进程在子shell中被异步执行,就像命令已使用‘&’控制运算符终止一样,在执行 shell 和协进程之间建立了双向管道。

协进程的语法是:

coproc [NAME] command [redirections] 

这将创建一个名为NAME的协进程。 command可以是一个简单的命令(见2 简单的命令)或一个复合命令(见5 复合命令)。 NAME是一个shell变量名称。 如果没有提供NAME,默认名称是COPROC

推荐用于协进程的形式是

coproc NAME { command; } 

推荐使用这种形式,因为简单的命令会导致协进程总是被命名为COPROC,而且它比其他复合命令使用起来更简单,更完整。

还有其他形式的协进程:

coproc NAME compound-command
coproc compound-command
coproc simple-command

如果command是一个复合命令,NAME是可选的。在coproc后面的词决定了该词是否被解释为变量名:如果它不是引入复合命令的保留词,则被解释为NAME。 如果command是一个简单命令,则不允许NAME;这是为了避免NAME和简单命令的第一个词之间发生混淆。

执行coprocess时,shell 在执行 shell 的上下文中创建一个名为NAME的数组变量(参见6.7 数组)。命令的标准输出通过管道连接到正在执行的 shell 中的文件描述符,并且该文件描述符被分配给NAME[0]。command 的标准输入通过管道连接到正在执行的 shell 中的文件描述符,并且该文件描述符被分配给 NAME[1]。此管道在命令指定的任何重定向之前建立(参见3.6 重定向)。文件描述符可以用作 shell 命令和使用标准字扩展的重定向的参数。除了那些为执行命令和进程替换而创建的文件描述符外,文件描述符在子 shell 中不可用。

为执行协同进程而产生的shell的进程ID可以作为变量NAME_PID的值。 wait内置命令可以用来等待协同进程的终止。

由于协进程是作为一个异步命令创建的,coproc命令总是返回成功。 协处理的返回状态是command的退出状态。

7 GNU Parallel

有一些方法可以并行运行Bash以外的命令,GNU Parallel就是一个这样的工具。

GNU Parallel,正如它的名字所示,可以用来建立和运行并行的命令。你可以用不同的参数来运行同一个命令,不管这些参数是文件名、用户名、主机名还是从文件中读取的行。GNU Parallel为许多最常见的操作提供了速记参考(输入行、输入行的各个部分、指定输入源的不同方式,等等)。Parallel可以替换xargs或将其输入源的命令送入几个不同的Bash实例中。

关于完整的描述,请参考GNU Parallel文档,该文档可在https://www.gnu.org/software/parallel/parallel_tutorial.html上获得。

最后更新2023-03-02
0 个评论
上一篇: 3.1 Shell的语法
下一篇: 3.3 Shell函数