条件语句格式

if 命令的一般格式为:

  1. if commandt
  2. then
  3. command
  4. command
  5. ...
  6. fi

其中,commandt是要执行的命令,命令的退出状态会被测试。如果退出状态为0,执行thenfi之间的命令。否则,跳过这些命令。

退出状态

要程序执行完成,就会向Shell返回一个退出状态码。这个状态码是一个数值,指明了程序运行是否成功。按照惯例,为0的退出状态码表示程序运行成功;非0的退出状态码表示程序运行失败,不同的值对应着不同的失败原因。
对于管道而言,退出状态对应的是管道中的最后一个命令。因此,在下列管道中:

  1. who | grep fred

grep的退出状态被Shell作为整个管道的退出状态。在这个例子中,为0的退出状态(成功)意味着在who的输出中找到了fred(也就是说,在命令执行的时候,fred处于已登录状态)

变量$?

Shell会将变量$?自动设置为最后一条命令的退出状态。你自然可以使用echo在终端中显示该变量的值。

  1. $ cp phonebook phone2
  2. $ echo $?
  3. 0 复制命令执行成功
  4. $ cp nosuch backup
  5. cp: cannot access nosuch
  6. $ echo $?
  7. 2 复制命令执行失败

注意,对于一些命令来说,其表示执行失败的数值在不同的UNIX版本中并不相同,但表示执行成功的退出状态码总是0。

  1. #
  2. # 确定用户是否已经登录
  3. #
  4. user="$1"
  5. if who | grep "$user"
  6. then
  7. echo "$user is logged on"
  8. fi

grep不仅会在管道中返回退出状态,而且还会将匹配的行写入到标准输出,哪怕你对此并不感兴趣。考虑到除了测试退出状态码之外,我们并不想看到命令结果,可以将grep的输出重定向到系统的垃圾桶:/dev/null。这是一个特殊的系统文件,任何人都可以读取(立刻会得到一个 EOF)或写入。向该文件写入的任何东西都会消失,就像是一个巨大的黑洞一样。

  1. who | grep "$user" > /dev/null

test命令

在测试单个或多个条件时,用得更多的是Shell内建的test命令。其一般格式为:

  1. test expression

其中,expression描述了待测试的条件。test会对expression求值,如果结果为真,返回为0的退出状态码;如果结果为假,返回非0的退出状态码。

字符串操作符

来看一个例子,如果Shell变量name的值为julio,下列命令会返回为0的退出状态码:

  1. test "$name" = julio

操作符=用来测试两个值是否一样。在本例中,我们要测试Shell变量 name 的内容是否和字符串julio一样。如果一样的话,test返回为 0的退出状态码;否则返回非0值。

注意,test命令所有的操作数($name和julio)和操作符(=)都必须是独立的参数,也就是说,它们彼此之间必须使用一个或多个空白字符分隔。
再来看if命令,如果name中包含的是字符串julio,则显示信息”Would you like to play a game?”,可以这样来写:

  1. if test "$name" = julio
  2. then
  3. echo "Would you like to play a game?"
  4. fi

if命令执行时,随后的命令也会被执行并会对其退出状态求值。test命令被传入了3个参数:$name(会被替换成相应的值)=julio。然后 test 会测试第一个参数是否等于第三个参数,如果相等,会返回为0的退出状态码,反之则返回非0值。
if语句会测试test返回的退出状态。如果为0,执行then和fi之间的命令,在本例中,执行的是echo命令。如果是非0的退出状态,则跳过echo命令。
如上例中所演示的,将test的参数放在双引号中(允许变量替换)是一种良好的编程实践。这确保了就算其值为空,test也能够将其视为参数。

  1. $ name= 将变量 name 设为空
  2. $ test $name = julio
  3. sh: test: argument expected

因为name的值为空,所以只有两个参数被传给test:=julio(Shell 会在将命令行解析为参数之前替换 name 的值)。实际上,当 Shell$name替换之后,就相当于输入了下列命令:

  1. test = julio

如果把变量放进双引号中,就能够确保 test 可以识别出这个参数,因为当参数值为空的时候,引号就相当于一个占位符

  1. $ test "$name" = julio
  2. $ echo $? 打印出退出状态码
  3. 1

即便name的值为空,Shell传入test的仍旧是3个参数,其中第一个参数是空值。

操作符 如果满足下列条件,则返回真(退出状态码为 0)
string1 = string2 string1 等于 string2
string1 != string2 string1 不等于 string2
string string 不为空
-n string string 不为空(test 必须能够识别出作为参数的 string)
-z string string 为空(test 必须能够识别出作为参数的 string)

注意,test对于参数也是有讲究的。举例来说,如果Shell变量symbol的值是一个等号,来看看在测试其长度是否为0时会发生什么: sh $ echo $symbol = $ test -z "$symbol" sh: test: argument expected 操作符=的优先级要比操作符-z高,因此test会将命令作为等量关系测试来处理,希望在=之后找到另一个参数。为了避免这种问题,很多Shell程序员会将test命令写作: sh test X"$symbol" = X 如果symbol为空值的话,结果为真;否则结果为假。symbol前面的X避免了test将保存在symbol中的字符误认为是操作符。

test的另一种格式

Shell程序员经常用到test命令,该命令的另外一种格式,能够让你的程序看起来整洁得多:[。这种写法提高了if语句以及Shell脚本中其他条件测试的可读性。
test命令一般格式为:

  1. test expression

也可以使用另外一种格式来表示:

  1. [ expression ]

[之后以及]之前必须要有空格。你可以用新的格式重写之前例子中的 test 命令:

  1. $ [ -z "$nonnullvar" ]
  2. $ echo $?
  3. 1

if命令中使用的时候,格式如下:

  1. if [ "$name" = julio ]
  2. then
  3. echo "Would you like to play a game?"
  4. fi

整数操作符

test有不少可以用来执行整数比较的操作符。

操作符 如果满足下列条件,则返回真(退出状态码为 0)
int1 -eq int2 int1等于 int2
int1 -ge int2 int1大于或等于 int2
int1 -gt int2 int1大于 int2
int1 -le int2 int1小于或等于 int2
int1 -lt int2 int1小于 int2
int1 -ne int2 int1不等于 int2

有一点值得注意:在使用整数操作符时,将变量的值视为整数的是test命令,而非Shell。因此,无论Shell变量的类型是什么,都能够进行比较。 sh $ x1="005" $ x2=" 10" $ [ "$x1" = 5 ] 字符串比较 $ echo $? 1 假 $ [ "$x1" -eq 5 ] 整数比较 $ echo $? 0 真 $ [ "$x2" = 10 ] 字符串比较 $ echo $? 1 假 $ [ "$x2" -eq 10 ] 整数比较 $ echo $? 0 真

文件操作符

几乎所有的Shell程序都少不了要跟一个或多个文件打交道。因此,test提供了各种可以用来查询文件信息的操作符。这些操作符都是一元操作符,也就是说,它们只接受其后跟随单个参数。在所有情况下,第二个参数都是文件名(包括必要的目录名)。

操作符 如果满足下列条件,则返回真(退出状态码为 0)
-d file file 是一个目录
-e file file 存在
-f file file 是一个普通文件
-r file file 可由进程读取
-s file file 不是空文件
-w file file 可由进程写入
-x file file 是可执行的
-L file file 是一个符号链接

逻辑否定操作符!

一元逻辑否定操作符!可以放置在任意的test表达式之前,否定该表达式的求值结果。例如:

  1. [ ! -r /users/steve/phonebook ]

如果/user/steve/phonebook 不可读的话,会返回为 0 的退出状态码(真)

逻辑’与’操作符-a

操作符-a在两个表达式之间执行逻辑’与’运算,仅当这两个表达式均为真时,才返回真。因此,下列命令:

  1. [ -f "$mailfile" -a -r "$mailfile" ]

如果由$mailfile 指定的文件是一个普通文件且能够被当前用户读取,则返回真。

操作符-a两边多出的空格是为了提高表达式的可读性,不影响执行效果。

在有些情况下,重要的是要知道test只要发现条件不成立,立刻就会停止对逻辑’与’表达式求值,因此像下面的语句:

  1. [ ! -f "$file" -a $(who > $file) ]

如果! -f(文件不存在)测试没有通过的话,就不会在子Shell中执行who命令,因为test已经知道逻辑’与’(-a)表达式的结果为假了。

括号

你可以在 test 表达式中利用括号来根据需要改变求值顺序,不过记得把括号本身引用起来,因为它们对于Shell是有特殊含义的。

  1. [ \( "$count" -ge 0 \) -a \( "$count" -lt 10 \) ]

括号两边必须有空格,因为test要求条件语句中的每一个元素都是独立的参数。

逻辑’或’操作符-o

操作符-o-a类似,只是它在两个表达式之间形成的是逻辑’或’关系。也就是说,两个表达式只要其中一个为真,或者两个都为真,整个逻辑表达式的值就为真。

  1. [ -n "$mailopt" -o -r $HOME/mailfile ]

如果变量mailopt不为空或者$HOME/mailfile可由当前用户读取,则结果为真。
操作符-o的优先级要比操作符-a低,这就意味着表达式:

  1. "$a" -eq 0 -o "$b" -eq 2 -a "$c" -eq 10

会由test按照以下方式求值:

  1. "$a" -eq 0 -o ("$b" -eq 2 -a "$c" -eq 10)

else

if命令中可以加入else,其一般格式为:

  1. if commandt
  2. then
  3. command
  4. command
  5. . . .
  6. else
  7. command
  8. command
  9. . . .
  10. fi

其中,commandt会被执行并对其退出状态求值。如果为真(0),执行then代码块(then 和 else 之间的所有语句)并忽略else代码块。如果为假(非 0),则忽略then代码块,执行else代码块(else 与 fi 之间的所有语句)。无论在哪种情况下,都只执行一组语句:退出状态码为0,执行第一组语句;否则,执行第二组语句。

exit命令

Shell内建的exit命令可以立即终止Shell程序的执行。其一般格式为:

  1. exit n

其中,n是你想要返回的退出状态码。如果没有指定,则使用在exit之前最后执行的那条命令的退出状态(其实就是exit $?)。

elif

命令格式:

  1. if command1
  2. then
  3. command
  4. command
  5. ...
  6. elif command2
  7. then
  8. command
  9. command
  10. ...
  11. else
  12. command
  13. command
  14. ...
  15. fi

command1, command2, ..., commandn会依次执行并测试其退出状态。只要返回的退出状态为真(0),就会执行then之后的命令,直到碰上其他elifelsefi。如果所有的条件表达式都不为真,则执行可选的else之后的命令。

case命令

case命令可以将单个值与一组值或表达式进行比较,在有匹配的时候执行一个或多个命令。其一般格式如下:

  1. case value in
  2. pattern1)
  3. command
  4. command
  5. ...
  6. command;;
  7. pattern2)
  8. command
  9. command
  10. ...
  11. command;;
  12. ...
  13. patternn)
  14. command
  15. command
  16. ...
  17. command;;
  18. esac

value会连续地和pattern1、pattern2...patternn比较,直到找到匹配项。接着,执行所匹配值之后的命令,碰到双分号后停止,双分号在这里起到一个break语句的作用,表明已经完成了特定条件下指定的语句。在这之后,case语句就结束了。如果没有发现匹配项,case中的命令一个都不执行。

  1. #
  2. # 将数字转换成对应的英语
  3. #
  4. if [ "$#" -ne 1 ]
  5. then
  6. echo "Usage: number digit"
  7. exit 1
  8. fi
  9. case "$1"
  10. in
  11. 0) echo zero;;
  12. 1) echo one;;
  13. 2) echo two;;
  14. 3) echo three;;
  15. 4) echo four;;
  16. 5) echo five;;
  17. 6) echo six;;
  18. 7) echo seven;;
  19. 8) echo eight;;
  20. 9) echo nine;;
  21. esac

当符号|用于两个模式之间时,其效果等同于逻辑’或’。也就是说,模式:

  1. pat1 | pat2

表明匹配pat1或pat2。例如:

  1. -l | -list

能够匹配-l-list

空命令:

每个匹配的case语句分支都需要对应的命令,每个if-then条件也是一样,但有时候你并不打算执行什么命令,只想把结果丢掉。那该怎么做?可以利用 Shell 内建的空命令来实现。该命令的格式非常简单:

  1. :

在大多数情况下,它用来满足必须有命令存在的要求,尤其是在if语句中。

  1. if grep "^$system" /users/steve/mail/systems > /dev/null
  2. then
  3. :
  4. else
  5. echo "$system is not a valid system"
  6. exit 1
  7. fi

&&||

Shell有两个特殊的操作符,可以根据之前命令成功与否来执行后续的命令。如果你觉得这听起来似乎有些像if命令,从某种程度上来说,的确是的。它是if命令的一种简写形式。

  1. command1 && command2

command1会在Shell需要命令的地方被执行,如果该命令返回的退出状态码为0(成功),则接着执行command2。如果command1返回的退出态码不为0(失败),command2会被忽略。

  1. command1 || command2

command1会在Shell需要命令的地方被执行,如果该命令返回的退出状态码为非0(失败),则接着执行command2。如果command1返回的退出态码为0(成功),command2会被忽略。

  1. sort bigdata > /tmp/sortout && mv /tmp/sortout bigdata

mv 命令仅会在 sort 命令执行成功的情况下被执行。
操作符||的工作方式类似,除了第二个命令仅在第一个命令的退出状态码非0的时候才执行。

  1. grep "$name" phonebook || echo "Couldn't find $name"

那么仅当grep失败的时候(也就是说,无法在phonebook中找到$name,或打不开文件phonebook)才会执行echo命令。