之前赶鸭子上架写过一个不算太复杂的bash脚本,被bash中的条件控制恶心到了,现在抽丝剥茧深入学习下,防止以后再掉坑里
基础用法
if commands; then
commands
[elif commands; then
commands...]
[else
commands]
fi
bash中的条件控制的基本形式如上, ;
可以与换行互相替换,这篇文章主要来研究下if后面接着的commands
退出状态
当命令执行完毕后,命令(包括我们编写的脚本和 shell 函数)会给系统发送一个值,叫做退出状态。 这个值是一个 0 到 255 之间的整数,说明命令执行成功或是失败。按照惯例,一个零值说明成功,其它所有值说明失败。 Shell 提供了一个参数 $?
,我们可以用它检查退出状态。
[me@linuxbox ~]$ ls -d /usr/bin
/usr/bin
[me@linuxbox ~]$ echo $?
0
[me@linuxbox ~]$ ls -d /bin/usr
ls: cannot access /bin/usr: No such file or directory
[me@linuxbox ~]$ echo $?
2
这是很重要的概念,意味着我们可以不仅仅使用test命令来进行条件控制,我们可以使用一个普通的命令来作为判断条件。
## 下载某个文件,成功则做一些事情,不成功就不做
if wget xxx ;then;commands;fi
true 和 false 是什么
在shell中true和false并不像其他语言一样是布尔值,而是两个内建的命令
它们不做任何事情,除了以一个0或1退出状态来终止执行。 True 命令总是执行成功,而 false 命令总是执行失败:
[me@linuxbox~]$ true
[me@linuxbox~]$ echo $?
0
[me@linuxbox~]$ false
[me@linuxbox~]$ echo $?
1
test命令
经常与 if 一块使用的命令是 test。它有两种等价形式
test expression
[ expression ] ## expression前后的空格必不可少
这里的 expression 是一个表达式,其执行结果是 true 或者是 false。当表达式为真时,这个 test 命令返回一个零 退出状态,当表达式为假时,test 命令退出状态为1。这句话比较重要,在这里踩了几个坑,后面会详细介绍。
test命令中的expression可以对文件、字符串和整数的状态进行判断
文件表达式
表达式 | 如果下列条件为真则返回True |
---|---|
file1 -ef file2 | file1 和 file2 拥有相同的索引号(通过硬链接两个文件名指向相同的文件)。 |
file1 -nt file2 | file1新于 file2。 |
file1 -ot file2 | file1早于 file2。 |
-b file | file 存在并且是一个块(设备)文件。 |
-c file | file 存在并且是一个字符(设备)文件。 |
-d file | file 存在并且是一个目录。 |
-e file | file 存在。 |
-f file | file 存在并且是一个普通文件。 |
-g file | file 存在并且设置了组 ID。 |
-G file | file 存在并且由有效组 ID 拥有。 |
-k file | file 存在并且设置了它的“sticky bit”。 |
-L file | file 存在并且是一个符号链接。 |
-O file | file 存在并且由有效用户 ID 拥有。 |
-p file | file 存在并且是一个命名管道。 |
-r file | file 存在并且可读(有效用户有可读权限)。 |
-s file | file 存在且其长度大于零。 |
-S file | file 存在且是一个网络 socket。 |
-t fd | fd 是一个定向到终端/从终端定向的文件描述符 。 |
这可以被用来决定是否重定向了标准输入/输出错误。 | |
-u file | file 存在并且设置了 setuid 位。 |
-w file | file 存在并且可写(有效用户拥有可写权限)。 |
-x file | file 存在并且可执行(有效用户有执行/搜索权限)。 |
一个简单的例子
FILE=~/.bashrc
if [ -e "$FILE" ]; then
if [ -f "$FILE" ]; then
echo "$FILE is a regular file."
fi
if [ -d "$FILE" ]; then
echo "$FILE is a directory."
fi
if [ -r "$FILE" ]; then
echo "$FILE is readable."
fi
if [ -w "$FILE" ]; then
echo "$FILE is writable."
fi
if [ -x "$FILE" ]; then
echo "$FILE is executable/searchable."
fi
else
echo "$FILE does not exist"
exit 1
fi
字符串表达式
表达式 | 如果下列条件为真则返回True |
---|---|
string | string 不为 null。 |
-n string | 字符串 string 的长度大于零。 |
-z string | 字符串 string 的长度为零。 |
string1 = string2 | |
string1 == string2 | string1 和 string2 相同。 单或双等号都可以,不过双等号更受欢迎。 |
string1 != string2 | string1 和 string2 不相同。 |
string1 > string2 | sting1 排列在 string2 之后。 |
string1 < string2 | string1 排列在 string2 之前。 |
一个例子
ANSWER=maybe
if [ -z "$ANSWER" ]; then
echo "There is no answer." >&2
exit 1
fi
if [ "$ANSWER" = "yes" ]; then
echo "The answer is YES."
elif [ "$ANSWER" = "no" ]; then
echo "The answer is NO."
elif [ "$ANSWER" = "maybe" ]; then
echo "The answer is MAYBE."
else
echo "The answer is UNKNOWN."
fi
整型表达式
表达式 | 如果为真... |
---|---|
integer1 -eq integer2 | integer1 等于 integer2。 |
integer1 -ne integer2 | integer1 不等于 integer2。 |
integer1 -le integer2 | integer1 小于或等于 integer2。 |
integer1 -lt integer2 | integer1 小于 integer2。 |
integer1 -ge integer2 | integer1 大于或等于 integer2。 |
integer1 -gt integer2 | integer1 大于 integer2。 |
例子
INT=-5
if [ -z "$INT" ]; then
echo "INT is empty." >&2
exit 1
fi
if [ $INT -eq 0 ]; then
echo "INT is zero."
else
if [ $INT -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi
结合表达式
AND -a
OR -o
NOT !
增强版的[]——[[ ]]
[[ expression ]]
类似于test命令,但是它的功能更为强大
支持正则表达式
string1 =~ regex
==
操作符支持模式匹配
[me@linuxbox ~]$ FILE=foo.bar
[me@linuxbox ~]$ if [[ $FILE == foo.* ]]; then
> echo "$FILE matches pattern 'foo.*'"
> fi
foo.bar matches pattern 'foo.*'
结合表达式支持&& ||
可以直接使用&& ||而不用-a -o
[[ ... && ... && ... ]] 和 [ ... -a ... -a ...] 不一样,[[ ]] 是逻辑短路操作,而 [ ] 不会进行逻辑短路
支持数字运算
test命令只支持数字的比较而不支持 + - * / %
,[[ ... ]]可以支持
ASCII比较
test命令中ASCII比较需要转义,而[[ ]]中不需要
[ aaa \> bbbb ]
[[ aaa > bbbb ]]
这里有个需要注意的点, >
或者 \>
比较的ASCII,在比较数字时很容易出错
例如
[[ "a" != "b" && 10 > 2 ]] ## 10的第一位是1,ASCII值小于2,所以这个表达式的值是false
如果需要进行数字的比较需要使用 -le
等命令选项,或者使用(( ))
let expr (( ))
除了[[ ]]可以进行整数运算之外还有几种其他的方式
## expr进行数学运算,注意空格,使用*需要转义\*
expr 2 + 2
## 将运算结果赋值,使用反引号或者$()
s=`expr 2 + 3`
echo $s
5
## 等价为
s=$[2+3] ## 不用考虑空格。*也不用转义
## 等价为
let s=2+3
## 等价为
s=$((2+3))
## ((expression))可以用来进行整数的比较
[[ "a" != "b" ]] && ((10 > 2)) ## 整数比较正确的写法
布尔型变量最佳实践
假设一个变量 ENABLE
被赋值为true/false,我们应该怎么去使用它呢
## 直接使用$ENABLE,一般情况下没有问题
## 但是如果ENABLE是个未定义的变量或者空字符串又或者是一个退出状态为true的命令,这个if都会判断为true
ENABLE=false
if $ENABLE
then
echo true
else
echo false
fi
## ENABLE是一个空字符串
➜ ~ ENABLE=""
➜ ~ if $ENABLE;then;echo true;else;echo false;fi
true
## ENABL为1
➜ ~ ENABLE=1
➜ ~ if $ENABLE;then;echo true;else;echo false;fi
zsh: command not found: 1
false
## ENABLE=echo $aaaa
➜ user-1678701-1561966146 ENABLE=echo $aaaa
➜ user-1678701-1561966146 echo $ENABLE
echo
➜ user-1678701-1561966146 if $ENABLE;then;echo true;else;echo false;fi
true
为了避免变量未被定义仍被当做true执行,即使有一个变量是true/false,我们仍需要将它当做字符串来处理
ENABLE=""
if [[ $ENABLE = "true" ]]
then
echo true
else
echo false
fi
如果不使用 [[ ]]
而是 [ ]
会发现当变量未定义时会发生异常
if [ $aaaaaaaa = "true" ]
then
echo true
else
echo false
fi
## 执行结果,这里虽然也打印出了false,但是是因为$aaaaaaaa = "true"执行失败,而不是test命令对表达式的判断
./hello_world.sh: line 5: [: =: unary operator expected
false
所以建议能用[[ ]] 的地方全部用[[ ]] ,而用 [ ]
时需要在引用变量时再套个双引号 if [ "$aaaaaaaa" = "true" ]
关于test命令一些理解
变量非空判断
之前说过在test命令这踩了几个坑,我碰到的场景是有个布尔值变量,然后根据其他条件,两个条件&&操作之后进行判断
ENABLE=false
TYPE=Debug
if [[ $ENABLE && $TYPE = "Debug" ]]
then
echo true
else
echo false
fi
这种写法无论 ENABLE
是true还是false最后都会打印true
尝试修改下 TYPE
的值,发现打印出了false,所以问题出在前一个语句中
将 [[ $ENABLE ]]
单独拿出来测试,发现只要ENABLE赋值为非空值,该条件都为true
在这里产生了一个误解,test命令并不能对true/false本身进行真值判断。
而 [[ $ENABLE ]] 的真正含义是对变量ENABLE进行非空判断
正确的用法应该是
## 将$ENABLE变为test命令可以正确支持的形式
## 这里$ENABLE被当做字符串
if [[ $ENABLE = true && $TYPE = "Debug" ]]
## 另一种方式是将$ENABLE单独作为一个命令
## 这里的$ENABLE是一个命令
if $ENABLE && [[ $TYPE = "Debug" ]]
要不要空格这是个问题
很多教程上都说在 [[ ]]
要舍得加空格,简单测试一下
[[ 1 == 2 ]] ## 结果为false
[[ 1==2 ]] ## 结果为真
这里简单谈下自己的理解,未经查证,有误欢迎指正
- 这里的
[ ]
[[ ]]
都可以看做test命令,而其中的内容都可以看做test命令的参数 - test命令没有参数时退出状态为1,表示false
- test命令有一个参数时退出状态为0,表示true
- 当参数大于一个时,test只支持字符串/文件/整数判断
- test不支持执行一个命令获取命令的退出状态(这是if语句本身的功能)
[[ 1==2 ]]为真是因为将1==2整体作为了命令参数
而[[ 1 == 2 ]]则是三个参数,其中==是tes支持的操作符
再联系上面的 [[ false ]]
为true,这里也是将true/false作为了一个参数,而不是执行true/false命令
所以建议在写条件判断的时候考虑成命令参数,分清楚比较对象和操作符
如何修改一个布尔值的变量
说回到之前ENABLE和其他条件结合的例子,当时我想将结合的真值直接重新赋值给ENABLE
类似下面的代码
ENABLE=true
TYPE=Debug
ENABLE= $($ENABLE && [[ $TYPE = "Debug" ]]) ## ENABLE为空
失败的原因我们来仔细探究一下
函数/命令的返回值
在shell中函数/命令实际是无法将一个值带回到调用方的,return的是函数执行的状态,而命令展开等展开的实际是标准输出的数据。而true/false/test命令都是没有标准输出的。这里我们期望的是$ENABLE
执行true命令,在将true值输出到标准输出再与[[ $TYPE = "Debug" ]]
的输出结合,实际情况并非如此
如何符合预期的修改
写了个函数
getBoolValue(){
## 将字符串当做命令执行
if eval $*
then
echo true
else
echo false
fi
}
ENABLE=true
TYPE=Debug
## &&需要转义才能作为getBoolValue函数的参数
## 否则会被看做getBoolValue $ENABLE作为一个整体[[ $TYPE = "Debug" ]]作为一个整体,然后&&操作
ENABLE=$(getBoolValue $ENABLE \&\& [[ $TYPE = "Debug" ]])
echo $ENABLE
当然这个函数只是写来试验下,没有太多的实际价值
总结
- if后面的条件可以是普通的命令
- true/false是内建的命令,不做任何事情,除了以一个0或1退出状态来终止执行。
- test命令只能处理它支持的字符串/文件/整数表达式,命令/函数作为参数只会当做字符串不会得到正确的结果
- 尽量使用[[ ]] 代替 [ ]
- [[ ]]中的空格需要注意
- 使用到数字运算和比较使用(( ))
参考资料
shell if [[ ]]和[ ]区别 || &&
流程控制:if 分支结构