tags: [shell,bash]
categories: shell
top: false
toc: true
cover: false
img: /featureImg/bash.png
summary: shell中的布尔值有挺多的坑,来一块看看
之前赶鸭子上架写过一个不算太复杂的bash脚本,被bash中的条件控制恶心到了,现在抽丝剥茧深入学习下,防止以后再掉坑里
基础用法
if commands; thencommands[elif commands; thencommands...][elsecommands]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/usrls: 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=~/.bashrcif [ -e "$FILE" ]; thenif [ -f "$FILE" ]; thenecho "$FILE is a regular file."fiif [ -d "$FILE" ]; thenecho "$FILE is a directory."fiif [ -r "$FILE" ]; thenecho "$FILE is readable."fiif [ -w "$FILE" ]; thenecho "$FILE is writable."fiif [ -x "$FILE" ]; thenecho "$FILE is executable/searchable."fielseecho "$FILE does not exist"exit 1fi
字符串表达式
| 表达式 | 如果下列条件为真则返回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=maybeif [ -z "$ANSWER" ]; thenecho "There is no answer." >&2exit 1fiif [ "$ANSWER" = "yes" ]; thenecho "The answer is YES."elif [ "$ANSWER" = "no" ]; thenecho "The answer is NO."elif [ "$ANSWER" = "maybe" ]; thenecho "The answer is MAYBE."elseecho "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=-5if [ -z "$INT" ]; thenecho "INT is empty." >&2exit 1fiif [ $INT -eq 0 ]; thenecho "INT is zero."elseif [ $INT -lt 0 ]; thenecho "INT is negative."elseecho "INT is positive."fiif [ $((INT % 2)) -eq 0 ]; thenecho "INT is even."elseecho "INT is odd."fifi
结合表达式
AND -aOR -oNOT !
增强版的[]——[[ ]]
[[ expression ]] 类似于test命令,但是它的功能更为强大
支持正则表达式
string1 =~ regex
==操作符支持模式匹配
[me@linuxbox ~]$ FILE=foo.bar[me@linuxbox ~]$ if [[ $FILE == foo.* ]]; then> echo "$FILE matches pattern 'foo.*'"> fifoo.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 $s5## 等价为s=$[2+3] ## 不用考虑空格。*也不用转义## 等价为let s=2+3## 等价为s=$((2+3))## ((expression))可以用来进行整数的比较[[ "a" != "b" ]] && ((10 > 2)) ## 整数比较正确的写法
布尔型变量最佳实践
假设一个变量 ENABLE 被赋值为true/false,我们应该怎么去使用它呢
## 直接使用$ENABLE,一般情况下没有问题## 但是如果ENABLE是个未定义的变量或者空字符串又或者是一个退出状态为true的命令,这个if都会判断为trueENABLE=falseif $ENABLEthenecho trueelseecho falsefi## ENABLE是一个空字符串➜ ~ ENABLE=""➜ ~ if $ENABLE;then;echo true;else;echo false;fitrue## ENABL为1➜ ~ ENABLE=1➜ ~ if $ENABLE;then;echo true;else;echo false;fizsh: command not found: 1false## ENABLE=echo $aaaa➜ user-1678701-1561966146 ENABLE=echo $aaaa➜ user-1678701-1561966146 echo $ENABLEecho➜ user-1678701-1561966146 if $ENABLE;then;echo true;else;echo false;fitrue
为了避免变量未被定义仍被当做true执行,即使有一个变量是true/false,我们仍需要将它当做字符串来处理
ENABLE=""if [[ $ENABLE = "true" ]]thenecho trueelseecho falsefi
如果不使用 [[ ]] 而是 [ ] 会发现当变量未定义时会发生异常
if [ $aaaaaaaa = "true" ]thenecho trueelseecho falsefi## 执行结果,这里虽然也打印出了false,但是是因为$aaaaaaaa = "true"执行失败,而不是test命令对表达式的判断./hello_world.sh: line 5: [: =: unary operator expectedfalse
所以建议能用[[ ]] 的地方全部用[[ ]] ,而用 [ ] 时需要在引用变量时再套个双引号 if [ "$aaaaaaaa" = "true" ]
关于test命令一些理解
变量非空判断
之前说过在test命令这踩了几个坑,我碰到的场景是有个布尔值变量,然后根据其他条件,两个条件&&操作之后进行判断
ENABLE=falseTYPE=Debugif [[ $ENABLE && $TYPE = "Debug" ]]thenecho trueelseecho falsefi
这种写法无论 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=trueTYPE=DebugENABLE= $($ENABLE && [[ $TYPE = "Debug" ]]) ## ENABLE为空
函数/命令的返回值
在shell中函数/命令实际是无法将一个值带回到调用方的,return的是函数执行的状态,而命令展开等展开的实际是标准输出的数据。而true/false/test命令都是没有标准输出的。这里我们期望的是 $ENABLE 执行true命令,在将true值输出到标准输出再与 [[ $TYPE = "Debug" ]] 的输出结合,实际情况并非如此
如何符合预期的修改
写了个函数
getBoolValue(){## 将字符串当做命令执行if eval $*thenecho trueelseecho falsefi}ENABLE=trueTYPE=Debug## &&需要转义才能作为getBoolValue函数的参数## 否则会被看做getBoolValue $ENABLE作为一个整体[[ $TYPE = "Debug" ]]作为一个整体,然后&&操作ENABLE=$(getBoolValue $ENABLE \&\& [[ $TYPE = "Debug" ]])echo $ENABLE
当然这个函数只是写来试验下,没有太多的实际价值
总结
- if后面的条件可以是普通的命令
- true/false是内建的命令,不做任何事情,除了以一个0或1退出状态来终止执行。
- test命令只能处理它支持的字符串/文件/整数表达式,命令/函数作为参数只会当做字符串不会得到正确的结果
- 尽量使用[[ ]] 代替 [ ]
- [[ ]]中的空格需要注意
- 使用到数字运算和比较使用(( ))
参考资料
[shell if [[ ]]和 ]区别 || &&
流程控制:if 分支结构
