Shell测试和比较函数(test)
2010-07-13 05:15:31 阿炯

每一种条件语句的基础都是判断什么是真什么是假,是否了解其工作原理将决定您编写的是质量一般的脚本还是您将引以为荣的脚本,每一种条件语句的基础都是判断什么是真什么是假。Shell脚本的能力时常被低估,但实际上其能力的发挥受制于脚本撰写者的能力。您了解得越多就越能像变戏法似地编写一个文件来使任务自动化和简化您的管理工作。在 shell 脚本中进行的每一种操作(除最简单的命令编组之外)都需要检查条件。所有的 shell 脚本“逻辑” - 广义意义下的“逻辑” - 通常都可以分为以下三大类:
if {condition exists} then ...
while {condition exists} do ...
until {condition exists} do ...


《Advanced Bash-Scripting Guide》Chapter 7. Tests中有对其的专门章节讲解。

无论随后的操作是什么,这些基于逻辑的命令都依靠判断一种条件是否真实存在来决定后续的操作。test 命令是使得在每一种情况下都能够确定要判断的条件是否存在的实用工具。因此,彻底了解这个命令对于撰写成功的 shell 脚本至关重要。
运算符描述示例
文件比较运算符
-e filename如果 filename 存在,则为真[ -e /var/log/syslog ]
-d filename如果 filename 为目录,则为真[ -d /tmp/mydir ]
-f filename如果 filename 为常规文件,则为真[ -f /usr/bin/grep ]
-L filename如果 filename 为符号链接,则为真[ -L /usr/bin/grep ]
-r filename如果 filename 可读,则为真[ -r /var/log/syslog ]
-w filename如果 filename 可写,则为真[ -w /var/mytmp.txt ]
-x filename如果 filename 可执行,则为真[ -x /usr/bin/grep ]
filename1 -nt filename2如果 filename1 比 filename2 新,则为真[ /tmp/install/etc/services -nt /etc/services ]
filename1 -ot filename2如果 filename1 比 filename2 旧,则为真[ /boot/bzImage -ot arch/i386/boot/bzImage ]
字符串比较运算符(请注意引号的使用,这是防止空格扰乱代码的好方法)
-z string如果 string 长度为零,则为真[ -z "$myvar" ]
-n string如果 string 长度非零,则为真[ -n "$myvar" ]
string1 = string2如果 string1 与 string2 相同,则为真[ "$myvar" = "one two three" ]
string1 != string2如果 string1 与 string2 不同,则为真[ "$myvar" != "one two three" ]
算术比较运算符
num1 -eq num2等于[ 3 -eq $mynum ]
num1 -ne num2不等于[ 3 -ne $mynum ]
num1 -lt num2小于[ 3 -lt $mynum ]
num1 -le num2小于或等于[ 3 -le $mynum ]
num1 -gt num2大于[ 3 -gt $mynum ]
num1 -ge num2大于或等于[ 3 -ge $mynum ]

测试命令
test命令用于检查某个条件是否成立,它可以进行数值、字符和文件3个方面的测试,其测试符和相应的功能分别如下。

(1)数值测试:
-eq 等于则为真。
-ne 不等于则为真。
-gt 大于则为真。
-ge 大于等于则为真。
-lt 小于则为真。
-le 小于等于则为真。

(2)字串测试:
= 等于则为真。
!= 不相等则为真。
-z字串 字串长度为零则为真。
-n字串 字串长度非零则为真。

(3)文件测试:
-e文件名 如果文件存在则为真。
-r文件名 如果文件存在且可读则为真。
-w文件名 如果文件存在且可写则为真。
-x文件名 如果文件存在且可执行则为真。
-s文件名 如果文件存在且至少有一个字符则为真。
-z文件名 文件存在且长度为0返回真。
-d文件名 如果文件存在且为目录则为真。
-f文件名 如果文件存在且为普通文件则为真。
-c文件名 如果文件存在且为字符型特殊文件则为真。
-b文件名 如果文件存在且为块特殊文件则为真
-o文件名 如果文件属于用户本人返回真。



工作原理
test 命令最短的定义可能是评估一个表达式;如果条件为真,则返回一个 0 值。如果表达式不为真,则返回一个大于 0 的值 - 也可以将其称为假值。检查最后所执行命令的状态的最简便方法是使用 $? 值。出于演示的目的,本文中的例子全部使用了这个参数。其期望在命令行中找到一个参数,当 shell 没有为变量赋值时,则将该变量视为空。这意味着在处理脚本时,一旦脚本寻找的参数不存在,则 test 将报告该错误。当试图保护脚本时,您可以通过将所有参数包含在双引号中来解决这个问题。然后 shell 将变量展开,如果变量没有值,那么将传递一个空值给 test。另一种方法是在脚本内增加一个额外检查过程来判断是否设置了命令行参数。如果没有设置命令行参数,那么脚本会告诉用户缺少参数,然后退出。会通过一些例子来更具体地说明所有这些内容。

test 和 [ 命令
虽然 Linux 和 UNIX 的每个版本中都包含 test 命令,但该命令有一个更常用的别名 – 左方括号:[ test 及其别名通常都可以在 /usr/bin 或 /bin (取决于操作系统版本和供应商)中找到。当您使用左方括号而非 test 时,其后必须始终跟着一个空格、要评估的条件、一个空格和右方括号。右方括号不是任何东西的别名,而是表示所需评估参数的结束。条件两边的空格是必需的,这表示要调用 test,以区别于同样经常使用方括号的字符/模式匹配操作。test 和 [ 的语法如下:
test expression
[ expression ]
[[ EXPRESSION ]]


在这两种情况下,test 都评估一个表达式,然后返回真或假。如果它和 if、while 或 until 命令结合使用,则您可以对程序流进行广泛的控制。不过,您无需将 test 命令与任何其它结构一起使用;您可以从命令行直接运行它来检查几乎任何东西的状态。因为它们彼此互为别名,所以使用 test 或 [ 均需要一个表达式。表达式一般是文本、数字或文件和目录属性的比较,并且可以包含变量、常量和运算符。运算符可以是字符串运算符、整数运算符、文件运算符或布尔运算符 - 我们将在以下各部分依次介绍每一种运算符。

如果你希望脚本是可移植的,那么应该更喜欢使用旧的 test [ 命令,它在所有POSIX shell上都可用;大多数使用Bash、Zsh和Ksh作为默认Shell的现代系统都支持新升级版本的test [[(双中括号)。

test 文件运算符
利用这些运算符,您可以在程序中根据对文件类型的评估结果执行不同的操作:
-b file    如果文件为一个块特殊文件,则为真
-c file    如果文件为一个字符特殊文件,则为真
-d file    如果文件为一个目录,则为真
-e file    如果文件存在,则为真
-f file    如果文件为一个普通文件,则为真
-g file    如果设置了文件的 SGID 位,则为真
-G file    如果文件存在且归该组所有,则为真
-h        如果文件为链接文件,则为真
示例:[ -h /etc/grub2.cfg ] && echo "Y" || echo "N"

-k file    如果设置了文件的粘着位,则为真
-O file    如果文件存在并且归该用户所有,则为真
-p file    如果文件为一个命名管道,则为真
-r file    如果文件可读,则为真
-s file    如果文件的长度不为零,则为真
-S file    如果文件为一个套接字特殊文件,则为真
-t fd    如果 fd 是一个与终端相连的打开的文件描述符(fd 默认为 1),则为真
-u file    如果设置了文件的 SUID 位,则为真
-w file    如果文件可写,则为真
-x file    如果文件可执行,则为真

Table - test command file and other operators list
OperatorReturns
-a FILETrue if file exists.
-b FILETrue if file is block special.
-c FILETrue if file is character special.
-d FILETrue if file is a directory.
-e FILETrue if file exists.
-f FILETrue if file exists and is a regular file.
-g FILETrue if file is set-group-id(SGID).
-h FILETrue if file is a symbolic link.
-L FILETrue if file is a symbolic link.
-k FILETrue if file has its sticky bit set.
-p FILETrue if file is a named pipe.
-r FILETrue if file is readable by you.
-s FILETrue if file exists and is not empty.
-S FILETrue if file is a socket.
-t FDTrue if FD is opened on a terminal.
-u FILETrue if the file is set-user-id(SUID).
-w FILETrue if the file is writable by you.
-x FILETrue if the file is executable by you.
-O FILETrue if the file is effectively owned by you.
-G FILETrue if the file is effectively owned by your group(both group id of the file and the login user is the same).
-N FILETrue if the file has been modified since it was last read.
! EXPRLogical not.
EXPR1 && EXPR2Perform the and operation.
EXPR1 || EXPR2Perform the or operation.


以下示例显示了此简单操作的运行情况
$ ls -l
total 33
drwxr-xr-w 2 root       root      1024  Dec 5  05:05  LST
-rw-rw-rw- 1 emmett        users      27360 Feb 6  07:30  evan
-rwsrwsrwx 1 root        root      152   Feb 6  07:32  hannah
drwxr-xr-x 2 emmett        users      1024  Feb 6  07:31  karen
-rw------- 1 emmett        users      152   Feb 6  07:29  kristin
-rw-r--r-- 1 emmett     users     152   Feb 6  07:29  spencer

$ test -r evan
$ echo $?
0
$ test -r walter
$ echo $?
1


由于第一次评估为真 - 文件存在且可读 - 返回值为真,或 0。由于第二次评估的文件不存在,该值为假,返回值不为零。将值指定为零或非零很重要,因为在失败时不会始终返回 1(虽然这是通常返回的值),可能返回一个非零值。 正如开头所提到的,除了使用 test 外,您还可以用方括号 [ ] 将命令括住来向 shell 发出同样的命令 - 如下所示:

$ [ -w evan ]
$ echo $?
0
$ [ -x evan ]
$ echo $?
1

同样,第一个表达式为真,第二个表达式为假 - 正如返回值所指示的那样。文件新旧与本身比较见如下:

-nt operator is used to check that any file is newer than the other file or not.
if [ $1 -nt $2 ]
then
  echo "$1 is newer than $2"
else
  echo "$2 is newer than $1"
fi

-ot operator is used to check any file is older than the other file or not.
if [ $1 -ot $2 ]
then
  echo "$1 is older than $2"
else
   echo "$2 is older than $1"
fi

-ef operator is used to check that two hard links are pointing the same file or not.
if [ $1 -ef $2 ]
then
  echo "$1 and $2 are hard links of the same file."
else
  echo "$1 and $2 are not hard links of the same file."
fi

还可以使用以下命令将两个文件彼此进行比较:
file1 -ef file2    测试以判断两个文件是否与同一个设备相连,是否拥有相同的 inode 编号
file1 -nt file2    测试以判断第一个文件是否比第二个文件更新(由修改日期决定)
file1 -ot file2    测试以判断第一个文件是否比第二个文件更旧

以下示例显示了使用这些运算符比较文件的结果:
$ [ evan -nt spencer ]
$ echo $?
0
$ [ karen -ot spencer ]
$ echo $?
1

名为 evan 的文件比名为 spencer 的文件更新,因而评估为真。类似地,名为 karen 的文件比名为 spencer 的文件更新,因此该评估为假。

字符串比较运算符
如标题所示,这组函数比较字符串的值。您可以检查它们是否存在、是否相同或者是否不同。
String    测试以判断字符串是否不为空(通过长度来判断)
-n string    测试以判断字符串是否不为空;字符串必须为 test 所识别
-z string    测试以判断字符串是否为空;字符串必须为 test 所识别
string1 = string2    测试以判断 string1 是否与 string2 相同
string1 != string2    测试以判断 string1 是否与 string2 不同

对任何变量进行的最有用的测试之一是判断它的值是否不为空,可以简单地将其放在 test 命令行中执行这种测试,如下例所示:
$ test "$variable"

强烈建议进行此种测试时用双引号将变量括住,以让 shell 识别变量(即使变量为空)。默认情况下执行的基本字符串评估和 -n 测试从功能上讲是相同的,如以下示例所示:
#example1
if test -n "$1"
then
echo "$1"
fi
执行以上例子中的代码将根据 $1 是否存在给出以下结果:
$ example1 friday
friday
$ example1
如果将代码更改为以下形式,则结果将相同:
#example2
if test "$1"  
then
echo "$1"     
fi
如下所示:
$ example2 friday
friday
$ example2
所有这些表明,通常不需要 -n,它代表默认操作。 要从一个不同的角度来查看各种可能性,您可以用另一个选项来替换 -n,并检查该值是否为空(相对于非空)。这可以用 -z 选项来实现,代码为:
#example3
if test -z "$1"
then
echo "no values were specified"
fi
运行如下:
$ example3
no values were specified
$ example3 friday
如果在没有命令行参数的情况下运行该程序,而表达式评估为真,那么将执行程序块中的文本。如果在命令行中有值,则脚本退出,不执行任何操作。将评估操作放在脚本的开头非常有用,这可以在可能产生错误的进一步处理之前预先检查变量值。
其余的字符串运算符对两个变量/字符串之间的精确匹配或其中的差异(您也可以称之为等价性和“不等价性”)进行评估。第一个例子对匹配进行测试:
$ env
LOGNAME=emmett
PAGER=less
SHELL=/bin/bash
TERM=linux
$ [ "$LOGNAME" = "emmett"  ]
$ echo $?
0
$ [ "$LOGNAME" = "kristin"  ]
$ echo $?
1
或者,该评估可以以脚本的形式用于决定是否运行脚本:
#example4
if [ "$LOGNAME" = "emmett" ]
then
echo "processing beginning"
else
echo "incorrect user"
fi
这种方法可以用来寻找任意的值(如终端类型或 shell 类型),在允许脚本运行之前这些值必须匹配。请注意,= 或 != 运算符的优先级高于其它大多数可指定选项,且要求必须伴有表达式。因此,除了比较字符串的选项之外,= 或 != 都不能和检查某种东西(如可读文件、可执行文件或目录)的存在性的选项一起使用。

整数比较运算符
正如字符串比较运算符验证字符串相等或不同一样,整数比较运算符对数字执行相同的功能。如果变量的值匹配则表达式测试为真,如果不匹配,则为假。整数比较运算符不处理字符串(正如字符串运算符不处理数字一样):
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,则为真
以下示例显示了一个代码段,其中在命令行中给出的值必须等于 7:
#example5
if [ $1 -eq 7 ]
then
echo "You've entered the magic number."
else
echo "You've entered the wrong number."
fi
运行中:
$ example5 6
You've entered the wrong number.
$ example5 7
You've entered the magic number.
和字符串一样,比较的值可以是在脚本外为变量赋的值,而不必总是在命令行中提供。以下示例演示了实现这一点的一种方法:
#example6
if [ $1 -gt $number ]
then
echo "Sorry, but $1 is too high."
else
echo "$1 will work."
fi

$ set number=7
$ export number
$ example6 8
Sorry, but 8 is too high.
$ example6 7
7 will work.
整数比较运算符最佳的用途之一是评估指定的命令行变量的数目,并判断它是否符合所要求的标准。例如,如果某个特定的命令只能在有三个或更少变量的情况下运行,
#example7 - display variables, up to three
if [ "$#" -gt 3 ]
then
echo "You have given too many variables."
exit $#
fi
只要指定三个或更少的变量,该示例脚本将正常运行(并返回值 0)。如果指定了三个以上的变量,则将显示错误消息,且例程将退出 - 同时返回与命令行中给定的变量数相等的退出代码。对这个过程进行修改可以用来在允许运行报表之前判断当天是否是本月的最后几天:
#example8 - to see if it is near the end of the month#
set `date`  # use backward quotes
if [ "$3" -ge 21 ]
then
echo "It is close enough to the end of the month to proceed"
else
echo "This report cannot be run until after the 21st of the month"
exit $3
fi
在这个例子中,设置了六个变量(通过空格彼此分开):
$1 = Fri
$2 = Feb
$3 = 6
$4 = 08:56:30
$5 = EST
$6 = 2004
这些值可以在脚本中使用,就像它们是在命令行中输入的一样。请注意,退出命令再次返回一个值 - 在这种情况下,返回的值是从 $3 的值中得到的日期。这一技巧在故障诊断时会非常有用 - 如果您认为脚本应该运行而没有运行,那么请查看 $? 的值。
一种类似的想法可能是撰写一个只在每个月的第三个星期三运行的脚本。第三个星期三一定在该月的 15 日到 21 日之间。使用 cron,您可以调用脚本在 15 日到 21 日之间每天的一个指定时间运行,然后使用脚本的第一行检查 $1(在设置日期之后)的值是否为 Thu。如果为 Thu,那么执行剩下的脚本,如果不是,则退出。
而另一个想法可能是,只允许脚本在超过 6:00 p.m. (18:00),所有用户都回家之后运行。只要撰写脚本,使其在值低于 18 时退出,并通过使用以下命令来获取时间(将其设为 $1)
set `date +%H`

布尔运算符
布尔运算符在几乎每种语言中的工作方式都相同 - 包括 shell 脚本。在 nutshell 中,它们检查多个条件为真或为假,或者针对假的条件而不是真的条件采取操作。与 test 搭配使用的运算符有:
! expr    如果表达式评估为假,则为真
expr1 -a expr2    如果 expr1 和 expr2 评估为真,则为真
expr1 -o expr2     如果 expr1 或 expr2 评估为真,则为真
可以用 != 运算符代替 = 进行字符串评估。这是最简单的布尔运算符之一,对 test 的正常结果取非。其余两个运算符中的第一个是 -a(即 AND)运算符。要使测试最终为真,两个表达式都必须评估为真。如果任何一个评估为假,则整个测试将评估为假。例如:
$ env
HOME=/
LOGNAME=emmett
MAIL=/usr/mail/emmett
PATH=:/bin:/usr/bin:/usr/lbin
TERM=linux
TZ=EST5:0EDT
$ [ "$LOGNAME" = "emmett" -a "$TERM" = "linux" ]
$ echo $?
0
$ [ "LOGNAME" = "karen" -a "$TERM" = "linux" ]
$ echo $?
1
在第一个评估中,两个条件都测试为真(在一个 linux 终端上登录的是 emmett),因此整个评估为真。在第二个评估中,终端检查正确但用户不正确,因此整个评估为假。
简而言之,AND 运算符可以确保代码只在两个条件都满足时才执行。相反,只要任何一个表达式测试为真,OR (-o) 运算符即为真。我们来修改先前的例子,并将其放到一个脚本中来说明这一点:
#example9
if [ "$LOGNAME" = "emmett" -o "$TERM" = "linux" ]
then
echo "Ready to begin."
else
echo "Incorrect user and terminal."
fi

$ env
HOME=/
LOGNAME=emmett
MAIL=/usr/mail/emmett
PATH=:/bin:/usr/bin:/usr/lbin
TERM=linux
TZ=EST5:0EDT
$ example9
Ready to begin.
$ LOGNAME=karen
$ example9
Ready to begin.
在脚本第一次运行时,评估判断用户是否等于 emmett。如果发现用户等于 emmett,则脚本转至 echo 语句,并跳过其余的检查。它从不检查终端是否等于 linux,因为它只需要找到一条为真的语句就可以使整个运算为真。在脚本第二次运行时,它判断用户不是 emmett,因此它将检查并发现终端确实是 linux。由于一个条件为真,脚本现在转至 echo 命令。为了引出第二条消息,两个条件都必须为假。在先前确定时间是否为月末的例子中,可以执行类似的检查来防止用户试图在周末运行脚本:
#example10 - Do not let the script run over the weekend#
set `date`  # use backward quotes
if [ "$1" = "Sat" -o "$1" = "Sun" ]
then
echo "This report cannot be run over the weekend."
fi

一些有用的示例
示例1:在脚本文件中出现的“逻辑”的最简单的形式(如本文所有示例中所示)是“if ... then”语句。先前的一个代码段检查是否存在一定数量的变量,然后将这些变量回显。假设我们对此稍微做一些修改,比如我们想回显变量,并且每次回显均减去最左边的变量,以显示一个倒的三角形。 虽然这听起来很简单,但实际并非如此;这是您在执行大规模处理时想实现的方式:处理第一个变量、转移、处理下一个变量……出于演示的目的,可以按以下方式撰写脚本中的重要行:
#example11 - display declining variables, up to three
if [ "$#" -gt 3 ] # see if more than three variables are given
then
echo "You have given more than three variables."
exit
fi
echo $*
if test -n "$2"
then
shift
echo $*
fi
if test -n "$2"
then
shift
echo $*
fi
它将按以下方式执行:
$ example11 one
one

$ example11 one two
one two
two

$ example11 one two three
one two three
two three
three

$ example11 one two three four
You have given more than three variables.
出于检查的目的将数量限制为三个变量的原因是减少在例子中要检查的行数。一切都按部就班地进行,虽然它令人难以置信地混乱;用户因使用了超过程序依设计所能处理的变量数而得到警告,且脚本退出。如果变量数为 3 或更少,则运算的核心部分开始执行。
回显变量,执行测试以查看另一个变量是否存在。如果另一个变量存在,则执行一次转移,回显该变量,执行另一测试,等等。总共使用了16个有效行,而程序仅能处理不超过三个变量 - 非常混乱。假设消除变量数的限制,程序可以处理任意数量的变量。经过一些修改,脚本被缩短(美化)了,并能处理任意数量的变量:
#example12 - display declining variables, any number
while [ "$#" -gt 0 ]
do
echo $*
shift
done

$ example12 1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0
2 3 4 5 6 7 8 9 0
3 4 5 6 7 8 9 0
4 5 6 7 8 9 0
5 6 7 8 9 0
6 7 8 9 0
7 8 9 0
8 9 0
9 0
0
现在减少到只有 5 个有效行,且消除了第一个脚本三个变量的限制,并在运行时要更高效。

示例2:无论何时当在脚本内执行与处理相关的操作时,下一个操作将始终检查上一操作的状态,以确认它已成功完成。您可以通过检查 $? 的状态并验证它等于 0 来实现这一目的。例如,如果一个数据目录是否能访问非常重要。
#example13
TEMP=LST
cd $TEMP
if [ $?-ne 0 ]
then
echo "Data directory could not be found."
Exit
Fi

示例2:综合用例
可以在不使用if语句的情况下使用test命令。只有当测试命令的退出状态为true时,才会执行&&运算符之后的命令:

>FILE=/etc/freeoa.conf

test -f $FILE && echo "$FILE exists."

[ -f $FILE ] && echo "$FILE exists."

[[ -f $FILE ]] && echo "$FILE exists."

如果想在&&运算符之后运行一系列命令,只需将命令括在用';'分隔的大括号中或&&:
[ -f $FILE ] && { echo "$FILE exist."; cp -v "$FILE" /tmp/; }

与&&相反,只有当测试命令的退出状态为false时,才会执行||运算符后面的语句:
[ -f $FILE ] && echo "$FILE exist." || echo "$FILE does not exist."

[ -d /etc/dir ] && echo "$DIR is a directory."

[ -f /etc/resolv.conf -a -f /etc/hosts ] && echo "Both files exist."

[[ -f /etc/resolv.conf && -f /etc/hosts ]] && echo "Both files exist."


处理错误
test命令常常出现的错误事实上只有两种类型。第一种是未使用正确的评估类型,例如将字符串变量与整型变量进行比较或者将带填充的字符串与不带填充的字符串进行比较。仔细评估您使用的变量将使您最终找到错误的根源,并让您能够解决这些问题。
第二种错误类型包括将方括号误认为别名之外的某个东西。方括号与其内容之间必须有一个空格;否则,它们将不能解释其中的对象。例如,
$ [ "$LOGNAME" -gt 9]
test:] missing
请注意,错误消息指示 test 存在问题,即使使用了别名 ]。这些问题很容易发现,因为错误消息准确地将这些问题显示出来,然后您可以增加必要的空格。

结论
要在 shell 脚本中构建逻辑,您必须添加条件语句。每一条这种语句的核心都是对条件的评估,以判断它是否存在 - 通过使用 test 命令完成评估。了解它和它的别名(左方括号 ([)的工作原理将使您能够撰写可以完成一些复杂操作的 shell 脚本。

------------------------------------------
是否为 Bash shell 中大量的测试和比较选项而困惑呢?这个技巧可以帮助您解密不同类型的文件、算术和字符串测试,这样您就能够知道什么时候使用 test、 [ ]、 [[ ]]、 (( )) 或 if-then-else 了。
Bash shell 在当今的许多 Linux 和 UNIX 系统上都可使用,是 Linux 上常见的默认 shell。其包含强大的编程功能,其中包括丰富的可测试文件类型和属性的函数,以及在多数编程语言中可以使用的算术和字符串比较函数。理解不同的测试并认识到 shell 还能把一些操作符解释成 shell 元字符,是成为高级 shell 用户的重要一步。理解 shell 测试和比较函数并向 shell 添加编程功能。可能已经看到过使用 && 和 || 操作符的简单 shell 逻辑,它允许您根据前一条命令的退出状态(正确退出或伴随错误退出)而执行后一条命令。在这些技巧中将看到如何把这些基本的技术扩展成更复杂的 shell 编程。

测试
在任何一种编程语言中,学习了如何给变量分配值和传递参数之后,都需要测试这些值和参数。在 shell 中,测试会设置返回的状态,这与其他命令执行的功能相同。实际上 test 是个内置命令!
test 和 [
内置命令 test 根据表达式expr 求值的结果返回 0(真)或 1(假)。也可以使用方括号:test expr 和 [ expr ] 是等价的。 可以用 $? 检查返回值;可以使用 && 和 || 操作返回值;也可以用本技巧后面介绍的各种条件结构测试返回值。

清单1. 一些简单测试
[jhon@freeoa ~]$ test 3 -gt 4 && echo True || echo false
false
[jhon@freeoa ~]$ [ "abc" != "def" ];echo $?
0
[jhon@freeoa ~]$ test -d "$HOME" ;echo $?
0

在清单1 的第一个示例中,-gt 操作符对两个字符值之间执行算术比较。在第二个示例中,用 [ ] 的形式比较两个字符串不相等。在最后一个示例中,测试 HOME 变量的值,用单目操作符 -d 检查它是不是目录。
可以用 -eq、 -ne、-lt、 -le、 -gt 或 -ge 比较算术值,它们分别表示等于、不等于、小于、小于等于、大于、大于等于。
可以分别用操作符 =、 !=、 比较字符串是否相等、不相等或者第一个字符串的排序在第二个字符串的前面或后面。单目操作符 -z 测试 null 字符串,如果字符串非空 -n 返回 True(或者根本没有操作符)。
说明:shell 也用 操作符进行重定向,所以必须用 \ 加以转义。清单2 显示了字符串测试的更多示例。检查它们是否如您预期的一样。

清单2. 一些字符串测试
[jhon@freeoa ~]$ test "abc" = "def" ;echo $?
1
[jhon@freeoa ~]$ [ "abc" != "def" ];echo $?
0
[jhon@freeoa ~]$ [ "abc" \< "def" ];echo $?
0
[jhon@freeoa ~]$ [ "abc" \> "def" ];echo $?
1
[jhon@freeoa ~]$ [ "abc" \<"abc" ];echo $?
1
[jhon@freeoa ~]$ [ "abc" \> "abc" ];echo $?
1

表1 显示了一些更常见的文件测试。如果被测试的文件存在,而且有指定的特征,则结果为 True。
表1. 一些常见的文件测试 操作符     特征
-d     目录
-e     存在(也可以用 -a)
-f     普通文件
-h     符号连接(也可以用 -L)
-p     命名管道
-r     可读
-s     非空
-S     套接字
-w     可写
-N     从上次读取之后已经做过修改

除了上面的单目测试,还可以使用表2 所示的双目操作符比较两个文件:
表2. 测试一对文件 操作符 为 True 的情况
-nt     测试 file1 是否比 file2 更新。修改日期将用于这次和下次比较。
-ot     测试 file1 是否比 file2 旧。
-ef     测试 file1 是不是 file2 的硬链接。
其他一些测试可以用来测试文件许可之类的内容。请参阅 bash 手册获得更多细节或使用 help test 查看内置测试的简要信息。也可以用 help 命令了解其他内置命令。
-o 操作符允许测试利用 set -o 选项 设置的各种 shell 选项,如果设置了该选项,则返回 True (0),否则返回 False (1),如清单3 所示。

清单3. 测试 shell 选项
[jhon@freeoa ~]$ set +o nounset
[jhon@freeoa ~]$ [ -o nounset ];echo $?
1
[jhon@freeoa ~]$ set -u
[jhon@freeoa ~]$ test   -o nounset; echo $?
0
最后,-a 和 -o 选项允许使用逻辑运算符 AND 和 OR 将表达式组合在一起。单目操作符 ! 可以使测试的意义相反。可以用括号把表达式分组,覆盖默认的优先级。请记住 shell 通常要在子 shell 中运行括号中的表达式,所以需要用 \( 和 \) 转义括号,或者把这些操作符括在单引号或双引号内。清单4 演示了摩根法则在表达式上的应用。

清单4. 组合和分组测试
[jhon@freeoa ~]$ test "a" != "$HOME" -a 3 -ge 4 ; echo $?
1
[jhon@freeoa ~]$ [ ! \( "a" = "$HOME" -o 3 -lt 4 \) ]; echo $?
1
[jhon@freeoa ~]$ [ ! \( "a" = "$HOME" -o '(' 3 -lt 4 ')' ")" ]; echo $?
1
(( 和 [[
test 命令非常强大,但是很难满足其转义需求以及字符串和算术比较之间的区别。幸运的是,bash 提供了其他两种测试方式,这两种方式对熟悉 C、C++ 或 Java? 语法的人来说会更自然些。
(( )) 复合命令 计算算术表达式,如果表达式求值为 0,则设置退出状态为 1;如果求值为非 0 值,则设置为 0。不需要对 (( 和 )) 之间的操作符转义。算术只对整数进行。除 0 会产生错误,但不会产生溢出。可以执行 C 语言中常见的算术、逻辑和位操作。 let 命令也能执行一个或多个算术表达式。它通常用来为算术变量分配值。

清单5. 分配和测试算术表达式
[jhon@freeoa ~]$ let x=2 y=2**3 z=y*3;echo $? $x $y $z
0 2 8 24
[jhon@freeoa ~]$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w
0 3 8 16
[jhon@freeoa ~]$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w
0 4 8 13
同使用 (( )) 一样,利用复合命令 [[ ]] 可以对文件名和字符串使用更自然的语法。可以用括号和逻辑操作符把 test 命令支持的测试组合起来。

清单6. 使用 [[ 复合命令
[jhon@freeoa ~]$ [[ ( -d "$HOME" ) && ( -w "$HOME" ) ]] &&  echo "home is a writable directory"
home is a writable directory
在使用 = 或 != 操作符时,复合命令 [[ 还能在字符串上进行模式匹配。匹配的方式就像清单7 所示的通配符匹配。

清单7. 用 [[ 进行通配符测试
[jhon@freeoa ~]$ [[ "abc def .d,x--" == a[abc]*\ ?d* ]]; echo $?
0
[jhon@freeoa ~]$ [[ "abc def c" == a[abc]*\ ?d* ]]; echo $?
1
[jhon@freeoa ~]$ [[ "abc def d,x" == a[abc]*\ ?d* ]]; echo $?
1

甚至还可以在 [[ 复合命令内执行算术测试,但是千万要小心。除非在 (( 复合命令内,否则 操作符会把操作数当成字符串比较并在当前排序序列中测试它们的顺序。清单8 用一些示例演示了这一点。

清单8. 用 [[ 包含算术测试
[jhon@freeoa ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || (( 3 > 2 )) ]]; echo $?
0
[jhon@freeoa ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 -gt 2 ]]; echo $?
0
[jhon@freeoa ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 > 2 ]]; echo $?
0
[jhon@freeoa ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || a > 2 ]]; echo $?
0
[jhon@freeoa ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || a -gt 2 ]]; echo $?
-bash: a: unbound variable

条件测试
虽然使用以上的测试和 &&、|| 控制操作符能实现许多编程,但 bash 还包含了更熟悉的 “if, then, else” 和 case 结构。学习完这些之后,将学习循环结构,这样您的工具箱将真正得到扩展。

If、then、else 语句
bash 的 if 命令是个复合命令,它测试一个测试或命令($?)的返回值,并根据返回值为 True(0)或 False(不为 0)进行分支。虽然上面的测试只返回 0 或 1 值,但命令可能返回其他值。请参阅 LPI exam 102 prep: Shells, scripting, programming, and compiling 教程学习这方面的更多内容。
bash 中的 if 命令有一个 then 子句,子句中包含测试或命令返回 0 时要执行的命令列表,可以有一个或多个可选的 elif 子句,每个子句可执行附加的测试和一个 then 子句,子句中又带有相关的命令列表,最后是可选的 else 子句及命令列表,在前面的测试或 elif 子句中的所有测试都不为真的时候执行,最后使用 fi 标记表示该结构结束。使用迄今为止学到的东西,现在能够构建简单的计算器来计算算术表达式,如清单9 所示:

清单9. 用 if、then、else 计算表达式
[jhon@freeoa ~]$ function mycalc ()
> {
>    local x
>    if [ $# -lt 1 ]; then
>      echo "This function evaluates arithmetic for you if you give it some"
>    elif (( $* )); then
>      let x="$*"
>      echo "$* = $x"
>    else
>      echo "$* = 0 or is not an arithmetic expression"
>    fi
> }
[jhon@freeoa ~]$ mycalc 3 + 4
3 + 4 = 7
[jhon@freeoa ~]$ mycalc 3 + 4**3
3 + 4**3 = 67
[jhon@freeoa ~]$ mycalc 3 + (4**3 /2)
-bash: syntax error near unexpected token `('
[jhon@freeoa ~]$ mycalc 3 + "(4**3 /2)"
3 + (4**3 /2) = 35
[jhon@freeoa ~]$ mycalc xyz
xyz = 0 or is not an arithmetic expression
[jhon@freeoa ~]$ mycalc xyz + 3 + "(4**3 /2)" + abc
xyz + 3 + (4**3 /2) + abc = 35

这个计算器利用 local 语句将 x 声明为局部变量,只能在 mycalc 函数的范围内使用。let 函数具有几个可用的选项,可以执行与它密切关联的 declare 函数。请参考 bash 手册或使用 help let 获得更多信息。如清单 9 所示,需要确保在表达式使用 shell 元字符 -- 例如(、)、*、> 和 < 时 -- 正确地对表达式转义。无论如何,现在有了一个非常方便的小计算器,可以像 shell 那样进行算术计算。

在清单9 中可能注意到 else 子句和最后的两个示例。可以看到,把 xyz 传递给 mycalc 并没有错误,但计算结果为 0。这个函数还不够灵巧,不能区分最后使用的示例中的字符值,所以不能警告用户。可以使用字符串模式匹配测试(例如[[ ! ("$*" == *[a-zA-Z]* ]],或使用适合自己范围的形式)消除包含字母表字符的表达式,但是这会妨碍在输入中使用 16 进制标记,因为使用 16 进制标记时可能要用 0x0f 表示 15。实际上,shell 允许的基数最高为 64(使用 base#value 标记),所以可以在输入中加入 _ 和 @ 合法地使用任何字母表字符。8 进制和 16 进制使用常用的标记方式,开头为 0 表示八进制,开头为 0x 或 0X 表示 16 进制。清单10 显示了一些示例。

清单10. 用不同的基数进行计算
[jhon@freeoa ~]$ mycalc 015
015 = 13
[jhon@freeoa ~]$ mycalc 0xff
0xff = 255
[jhon@freeoa ~]$ mycalc 29#37
29#37 = 94
[jhon@freeoa ~]$ mycalc 64#1az
64#1az = 4771
[jhon@freeoa ~]$ mycalc 64#1azA
64#1azA = 305380
[jhon@freeoa ~]$ mycalc 64#1azA_@
64#1azA_@ = 1250840574
[jhon@freeoa ~]$ mycalc 64#1az*64**3 + 64#A_@
64#1az*64**3 + 64#A_@ = 1250840574
对输入进行的额外处理超出了本技巧的范围,所以请小心使用这个计算器。elif 语句非常方便。它允许简化缩进,从而有助于脚本编写。在清单11 中可能会对 type 命令在 mycalc 函数中的输出感到惊讶。

清单11. Type mycalc
[jhon@freeoa ~]$ type mycalc
mycalc is a function
mycalc (){
local x;
if [ $# -lt 1 ]; then
echo "This function evaluates arithmetic for you if you give it some";
else
if (( $* )); then
let x="$*";
echo "$* = $x";
else
echo "$* = 0 or is not an arithmetic expression";
fi;
fi
}

当然也可以只用 $(( 表达式 )) 和 echo 命令进行 shell 算术运算,如清单12 所示。这样就不必学习关于函数或测试的任何内容,但是请注意 shell 不会解释元字符,例如 *,因此元字符不能在 (( 表达式 )) 或 [[ 表达式 ]] 中那样正常发挥作用。

清单12. 在 shell 中用 echo 和 $(( )) 直接进行计算
[jhon@freeoa ~]$   echo $((3 + (4**3 /2)))
35