什么是 test 命令
说起 shell 中的 test 命令,你可能有点陌生,但是说到 if 中的条件测试命令,你可能就很熟悉了
#!/bin/bash
if [ -f ~/.bashrc ]; then
echo ...
fi
其中 [ -f ~/.bashrc ]
就是一个完整的 test 命令,其实这个脚本可以用 test 命令进行改写
#!/bin/bash
if test -f ~/.bashrc; then
echo ...
fi
通常我们更习惯使用 []
这种形式的 test 命令,但是要掌握 shell 的 test 命令不是一件容易的事情,至少对于初学者来说。
bash shell 对 shell 的 test 命令进行了功能增加,提供了一个更方便、更简洁、更符合现代的高级语言用法,并且我觉得最重要的一点,相对于 shell 的 test 命令,它就是可以避免很多语法错误。通过本文讲解,你将会爱上 bash shell 的 test 命令。
理解test命令
在深入学习 test 命令之前,我们需要理解 test 命令的括号形式语法,这是解开初学者所有困惑的关键所在。
以前面的[ -f ~/.bashrc ]
为例,请记住以下一句话 :
开括号[
表示 test 命令,括号中间的都是参数,闭括号 ]
也是 test 命令的参数。
当我们执行一个普通的命令时,命令和它的参数之间需要以一个空白字符分隔,因此 test 命令的 []
形式也是如此,前后括号的内侧都需要一个空白字符。
test 命令的表达式
test 命令的表达式有很多种,例如字符串表达式,整型表达式,文件表达式。下面我们来依次看看这些表达式如何使用,以及有哪些注意事项。
字符串表达式
关于字符串表达式总结如下
表达式 | 何时为真 |
---|---|
string | 字符中不为空 |
-n string | 字符串长度大于0 |
-z string | 字符串长为0 |
string1 = string2 或 string1 == string2 | 字符串相等 |
string != string2 | 字符串不相等 |
string1 > string2 | 以ASCII码的顺序,如果string1大于string2 |
string1 < string2 | 以ASCII码的顺序,如果string1小于string2 |
字符串表达式看似非常简单,其实也许多小细节需要注意,否则可能导致语法错误。我用一个脚本来解释这些细节
#!/bin/bash
str1=Z
str2=a
# 获取变量值要加引号,为了防止变量为空
if [ -z "$str1" ]; then
echo "str1 is empty."
exit 1;
fi
if [ -z "$str2" ]; then
echo "str2 is empty."
exit 1;
fi
# 大于号一定要转义,否则会被当作重定向符
if [ "$str1" \> "$str2" ]; then
echo "$str1 > $str2"
elif [ "$str1" == "$str2" ]; then
echo "$str1 == $str2"
else
echo "$str1 < $str2"
fi
从这个例子中,我们要注意的细节如下
- 括号内侧要有空格,前面讲过原理。
[ -z "$str1" ]
中,获取变量 str1 的值是,一定要加引号。因为如果 str1 为空,那么这个test命令是变成了[ -z ]
,这就变成了判断字符串(这里指-z)不为空的情况,很显然违背了原始的逻辑。[ $str1 \> $str2 ]
中,大于号一定要转义或者加括号(小于号也一样)。记住,[]
就是一个 test 命令,而在一个命令中,大于号、小于号,都会被当成重定向操作符,因此这里需要进行转义。
想不到吧,一个小小的 test 字符串表达式,居然有这么多需要注意的事项。但是只要我们记住一点就可以避免这些错误,这一点就是[]
是一个 test 命令,其余全部为参数。
判断字符串相等为何有两个操作符,= 是 shell 用的,== 是 bash 用的。
文件表达式
test 命令的文件表达式有很多,我把它们进行了分类讲解,这样方便理解与记忆。
判断文件存在性,以及长度
表达式 | 何时为真 |
---|---|
-a file 或 -e file | 文件存在 |
-s file | 文件存在,并且长度大于0 |
e 是 exist 的首字符,s 是 size 的首字符,这样就方便记忆了。
#!/bin/bash
if [ -e ~/.bashrc ]; then
echo "exist"
if [ -s ~/.bashrc ]; then
echo "size > 0"
fi
else
echo "no exist"
fi
这段脚本,很显示可以把~/.bashrc
提取为一个变量。我这里这样写,是为了说明一个问题,这也是很多新手困惑的问题,看下面的脚本
#!/bin/bash
# 这是一个错误脚本的写法,双引号阻止了波浪线的shell扩展
if [ -e "~/.bashrc" ] ; then
echo "exist"
else
echo "no exist"
fi
这个输出结果是no exist
!没错,我没有写错,你也没有看错。为什么呢?
所有的错误都是源于对 []
形式的 test 命令的不理解,无论是 ~/.bashrc
还是 "~/.bashrc"
,它们都是参数。而作为参数,双引号阻止了 shell 的波浪线展开。现在你能明白上面的错误了吗?
判断文件类型
表达式 | 何时为真 |
---|---|
-f file | 文件存在,且是普通文件 |
-d file | 文件存在,且是目录 |
-L file 或 -h file | 文件存在,且是符号链接 |
-c file | 文件存在,且是字符特殊文件 |
-b file | 文件存在,且是块特殊文件 |
-p file | 文件存在,且是一个命名管道文件 |
-S file | 文件存在,且是一个套接字文件 |
判断文件权限
表达式 | 何时为真 |
---|---|
-r file | 文件存在,且可读 |
-w file | 文件存在,且可写 |
-x file | 文件存在,且可执行 |
-u file | 文件存在,且设置用户ID被设置 |
-g file | 文件存在,且设置组ID被设置 |
-k file | 文件存在,且粘着位被设置 |
-G file | 文件存在,且被有效用户组ID拥有 |
-O file | 文件存在,且被有效用户ID拥有 |
文件之间的比较
表达式 | 何时为真 |
---|---|
file1 -ef file1 | file1和file2拥有相同的i节点号(硬链接创建的文件具有相同的i节点) |
file1 -nt file2 | file1的修改日期要新于file2的,或者file1存在,file2不存在 |
file1 -ot file2 | file1的修改日期要旧于file2的,或者file1存在,file2不存在 |
变量判断
表达式 | 何时为真 |
---|---|
-v variable | 如果变量已经被赋值 |
-R variable | 如果变量已经被赋值,并且是名称引用(name reference) |
#!/bin/bash
var=hello
if [ -v var ]; then
echo "var has been set"
fi
注意,这里是测试变量,不是测试变量的值,因此不需要加 var 前加美元符号。
整型表达式
test 命令的整型表达式的形式为int1 op int2
,其中op
为-eq
, -ne
, -lt
, -le
, -gt
, -ge
。其中 l
表示less
,g
表示greater
,e
表示equal
,n
表示not
,t
表示than
。
#!/bin/bash
int1=110
int2=911
if [ "$int1" -lt "$int2" ]; then
echo "$int1 less then $int2"
fi
整型比较的操作符为何这么复杂呢? 为何不能用大于号、小于号等等的操作符呢? 很可惜,shell 并不支持,而 bash 的复合命令 (())
支持这种写法,我们将在后面会看到。
test命令中的逻辑操作符
test 命令的表达式之间支持逻辑操作符,与或非分别由-a
, -o
, !
表示。
#!/bin/bash
a=5
b=11
FILE="test.txt"
if [ "$a" -ge 0 -a "$a" -le 10 ]; then
echo "$a is in range [0, 10]"
fi
if [ "$b" -lt 0 -o "$b" -gt 10 ]; then
echo "$b is not in range [0, 10]"
fi
# 注意,感叹号前后都要有空格,因为它是一个参数
if [ ! -e "$FILE" ]; then
echo "$FILE not exist"
fi
我们还可以使用括号把某些表达式结合到一起,但是使用起来要注意一些事项
#!/bin/bash
INT=5
if [ ! \( "$INT" -ge 0 -a "$INT" -le 10 \) ]; then
echo "INT not in range [0, 10]"
else
echo "INT in range [0, 10]"
fi
我们可以注意到,括号使用了转义,并且由于括号也是 test 命令的参数,因此需要用空白字符分隔。
说实话,这段代码写的真的是很累,在后面我们可以看到 bash 的复合命令 (())
会有更方便的写法。
bash 中的 test 命令
前面我们已经介绍了 shell 中的 test 命令的用法,我们可以发现使用起来还是有难度的,难度之一在于陷阱太多,难度之二在于语法晦涩。
bash 对 shell 的 test 命令进行了增强,避免了很多陷阱,也使语法更简单易用,只是它不兼容 POSIX 而已。
(())命令
bash 中的复合命令 (())
是针对算术表达式的,凡事能在高级语言( 例如 C,Java )中使用的算术表达式,都能在这个命令中使用,脚本代码如下
#!/bin/bash
INT=5
if ((!(INT >= 0 && INT <= 10))); then
echo "INT not in range [0, 10]"
else
echo "INT in range [0, 10]"
fi
可以注意到(())
有如下几点好处
- 获取变量时,不需要美元符号
- 括号内侧不需要空白字符。但是通常为了养成一个好习惯,在双括号内侧还是加上空白字符。
- 可以使用
&&
,|
,!
这样的逻辑操作符,而不需要再使用-a
,-o
,!
这样的复杂的逻辑操作符。
用这个脚本,对比下前面的 test 命令的整型表达式部分所使用的脚本,你会发现这简直是让人爽多了。
[[]]命令
凡是可以在 test 命令中使用的表达式,都可以在 bash 的 [[ expression ]]
中使用,但是 bash 的 [[]]
比[]
强在下面两点
==
和!=
支持通配符匹配。=~
支持 POSIX 正则表达式匹配。
#!/bin/bash
FILE=~/.bashrc
# == 或 != 使用模式进行匹配
if [[ "$FILE" == *rc ]]; then
echo "$FILE is rc file".
fi
INT=-10
# #~ 是使用正则表达式进行匹配的
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
echo "$INT is a number"
fi
通过这个例子,我们需要注意以下几点
- 由于
[[]]
是一个命令,因此与test的[]
形式一样,括号内侧需要括号。 - 由于
[[]]
命令不支持 shell 文件展开,因此模式*rc
中的星号,不会被展开。如果被加上引号或者被转义,那么将按原字符进行匹配。因此,上面的例子不能写成[["$FILE" == "*rc"]]
。同理,正则表达式^-?[0-9]+$
也下能加引号。 [[]]
中的逻辑操作符也高级语言一样,使用&&
,||
,!
。
关于兼容性的一些想法
在日常工作中,我都是使用 bash 的 test 命令,因为简洁而且不容易出错。但是它破坏了 POSIX 兼容。
其实事物都有两面性,例如,Linux 平台中默认的 shell 为 bash,而 Mac 默认为 zsh,Android 源码为了能在两个平台下编译,必须写出 POSIX 的代码,而不能使用 bash 独有的功能代码。
但是如果我们平时中不必考虑这样的 POSIX 问题,我们可以随意使用 bash shell 独有的高级功能,加快开发效率,殊不知,浪费时间就是谋财害命。
参考
[[]]
复合命令
https://www.gnu.org/software/bash/manual/bash.html#Conditional-Constructs
Linux命令行中对test命令的的说明
https://www.kancloud.cn/thinkphp/linux-command-line/39459
GNU Bash Reference Manual
https://www.gnu.org/software/bash/manual/bash.html#Invoking-Bash