tags: [shell,bash]
categories: shell
top: false
toc: true
cover: false
img: /featureImg/bash.png
summary: shell中的布尔值有挺多的坑,来一块看看


之前赶鸭子上架写过一个不算太复杂的bash脚本,被bash中的条件控制恶心到了,现在抽丝剥茧深入学习下,防止以后再掉坑里

基础用法

  1. if commands; then
  2. commands
  3. [elif commands; then
  4. commands...]
  5. [else
  6. commands]
  7. fi

bash中的条件控制的基本形式如上, ; 可以与换行互相替换,这篇文章主要来研究下if后面接着的commands

退出状态

当命令执行完毕后,命令(包括我们编写的脚本和 shell 函数)会给系统发送一个值,叫做退出状态。 这个值是一个 0 到 255 之间的整数,说明命令执行成功或是失败。按照惯例,一个零值说明成功,其它所有值说明失败。 Shell 提供了一个参数 $? ,我们可以用它检查退出状态。

  1. [me@linuxbox ~]$ ls -d /usr/bin
  2. /usr/bin
  3. [me@linuxbox ~]$ echo $?
  4. 0
  5. [me@linuxbox ~]$ ls -d /bin/usr
  6. ls: cannot access /bin/usr: No such file or directory
  7. [me@linuxbox ~]$ echo $?
  8. 2

这是很重要的概念,意味着我们可以不仅仅使用test命令来进行条件控制,我们可以使用一个普通的命令来作为判断条件。

  1. ## 下载某个文件,成功则做一些事情,不成功就不做
  2. if wget xxx ;then;commands;fi

true 和 false 是什么

在shell中true和false并不像其他语言一样是布尔值,而是两个内建的命令
它们不做任何事情,除了以一个0或1退出状态来终止执行。 True 命令总是执行成功,而 false 命令总是执行失败:

  1. [me@linuxbox~]$ true
  2. [me@linuxbox~]$ echo $?
  3. 0
  4. [me@linuxbox~]$ false
  5. [me@linuxbox~]$ echo $?
  6. 1

test命令

经常与 if 一块使用的命令是 test。它有两种等价形式

  1. test expression
  2. [ 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 存在并且可执行(有效用户有执行/搜索权限)。 |

一个简单的例子

  1. FILE=~/.bashrc
  2. if [ -e "$FILE" ]; then
  3. if [ -f "$FILE" ]; then
  4. echo "$FILE is a regular file."
  5. fi
  6. if [ -d "$FILE" ]; then
  7. echo "$FILE is a directory."
  8. fi
  9. if [ -r "$FILE" ]; then
  10. echo "$FILE is readable."
  11. fi
  12. if [ -w "$FILE" ]; then
  13. echo "$FILE is writable."
  14. fi
  15. if [ -x "$FILE" ]; then
  16. echo "$FILE is executable/searchable."
  17. fi
  18. else
  19. echo "$FILE does not exist"
  20. exit 1
  21. 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 之前。

一个例子

  1. ANSWER=maybe
  2. if [ -z "$ANSWER" ]; then
  3. echo "There is no answer." >&2
  4. exit 1
  5. fi
  6. if [ "$ANSWER" = "yes" ]; then
  7. echo "The answer is YES."
  8. elif [ "$ANSWER" = "no" ]; then
  9. echo "The answer is NO."
  10. elif [ "$ANSWER" = "maybe" ]; then
  11. echo "The answer is MAYBE."
  12. else
  13. echo "The answer is UNKNOWN."
  14. 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。

例子

  1. INT=-5
  2. if [ -z "$INT" ]; then
  3. echo "INT is empty." >&2
  4. exit 1
  5. fi
  6. if [ $INT -eq 0 ]; then
  7. echo "INT is zero."
  8. else
  9. if [ $INT -lt 0 ]; then
  10. echo "INT is negative."
  11. else
  12. echo "INT is positive."
  13. fi
  14. if [ $((INT % 2)) -eq 0 ]; then
  15. echo "INT is even."
  16. else
  17. echo "INT is odd."
  18. fi
  19. fi

结合表达式

  1. AND -a
  2. OR -o
  3. NOT

增强版的[]——[[ ]]

[[ expression ]] 类似于test命令,但是它的功能更为强大

支持正则表达式

  1. string1 =~ regex

==操作符支持模式匹配

  1. [me@linuxbox ~]$ FILE=foo.bar
  2. [me@linuxbox ~]$ if [[ $FILE == foo.* ]]; then
  3. > echo "$FILE matches pattern 'foo.*'"
  4. > fi
  5. foo.bar matches pattern 'foo.*'

结合表达式支持&& ||

可以直接使用&& ||而不用-a -o
[[ … && … && … ]] 和 [ … -a … -a …] 不一样,[[ ]] 是逻辑短路操作,而 [ ] 不会进行逻辑短路

支持数字运算

test命令只支持数字的比较而不支持 + - * / % ,[[ … ]]可以支持

ASCII比较

test命令中ASCII比较需要转义,而[[ ]]中不需要

  1. [ aaa \> bbbb ]
  2. [[ aaa > bbbb ]]

这里有个需要注意的点, > 或者 \> 比较的ASCII,在比较数字时很容易出错
例如

  1. [[ "a" != "b" && 10 > 2 ]] ## 10的第一位是1,ASCII值小于2,所以这个表达式的值是false

如果需要进行数字的比较需要使用 -le 等命令选项,或者使用(( ))

let expr (( ))

除了[[ ]]可以进行整数运算之外还有几种其他的方式

  1. ## expr进行数学运算,注意空格,使用*需要转义\*
  2. expr 2 + 2
  3. ## 将运算结果赋值,使用反引号或者$()
  4. s=`expr 2 + 3`
  5. echo $s
  6. 5
  7. ## 等价为
  8. s=$[2+3] ## 不用考虑空格。*也不用转义
  9. ## 等价为
  10. let s=2+3
  11. ## 等价为
  12. s=$((2+3))
  13. ## ((expression))可以用来进行整数的比较
  14. [[ "a" != "b" ]] && ((10 > 2)) ## 整数比较正确的写法

布尔型变量最佳实践

假设一个变量 ENABLE 被赋值为true/false,我们应该怎么去使用它呢

  1. ## 直接使用$ENABLE,一般情况下没有问题
  2. ## 但是如果ENABLE是个未定义的变量或者空字符串又或者是一个退出状态为true的命令,这个if都会判断为true
  3. ENABLE=false
  4. if $ENABLE
  5. then
  6. echo true
  7. else
  8. echo false
  9. fi
  10. ## ENABLE是一个空字符串
  11. ~ ENABLE=""
  12. ~ if $ENABLE;then;echo true;else;echo false;fi
  13. true
  14. ## ENABL为1
  15. ~ ENABLE=1
  16. ~ if $ENABLE;then;echo true;else;echo false;fi
  17. zsh: command not found: 1
  18. false
  19. ## ENABLE=echo $aaaa
  20. user-1678701-1561966146 ENABLE=echo $aaaa
  21. user-1678701-1561966146 echo $ENABLE
  22. echo
  23. user-1678701-1561966146 if $ENABLE;then;echo true;else;echo false;fi
  24. true

为了避免变量未被定义仍被当做true执行,即使有一个变量是true/false,我们仍需要将它当做字符串来处理

  1. ENABLE=""
  2. if [[ $ENABLE = "true" ]]
  3. then
  4. echo true
  5. else
  6. echo false
  7. fi

如果不使用 [[ ]] 而是 [ ] 会发现当变量未定义时会发生异常

  1. if [ $aaaaaaaa = "true" ]
  2. then
  3. echo true
  4. else
  5. echo false
  6. fi
  7. ## 执行结果,这里虽然也打印出了false,但是是因为$aaaaaaaa = "true"执行失败,而不是test命令对表达式的判断
  8. ./hello_world.sh: line 5: [: =: unary operator expected
  9. false

所以建议能用[[ ]] 的地方全部用[[ ]] ,而用 [ ] 时需要在引用变量时再套个双引号 if [ "$aaaaaaaa" = "true" ]

关于test命令一些理解

变量非空判断

之前说过在test命令这踩了几个坑,我碰到的场景是有个布尔值变量,然后根据其他条件,两个条件&&操作之后进行判断

  1. ENABLE=false
  2. TYPE=Debug
  3. if [[ $ENABLE && $TYPE = "Debug" ]]
  4. then
  5. echo true
  6. else
  7. echo false
  8. fi

这种写法无论 ENABLE 是true还是false最后都会打印true
尝试修改下 TYPE 的值,发现打印出了false,所以问题出在前一个语句中
[[ $ENABLE ]] 单独拿出来测试,发现只要ENABLE赋值为非空值,该条件都为true
在这里产生了一个误解,test命令并不能对true/false本身进行真值判断。
而 [[ $ENABLE ]] 的真正含义是对变量ENABLE进行非空判断
正确的用法应该是

  1. ## 将$ENABLE变为test命令可以正确支持的形式
  2. ## 这里$ENABLE被当做字符串
  3. if [[ $ENABLE = true && $TYPE = "Debug" ]]
  4. ## 另一种方式是将$ENABLE单独作为一个命令
  5. ## 这里的$ENABLE是一个命令
  6. if $ENABLE && [[ $TYPE = "Debug" ]]

要不要空格这是个问题

很多教程上都说在 [[ ]] 要舍得加空格,简单测试一下

  1. [[ 1 == 2 ]] ## 结果为false
  2. [[ 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
类似下面的代码

  1. ENABLE=true
  2. TYPE=Debug
  3. ENABLE= $($ENABLE && [[ $TYPE = "Debug" ]]) ## ENABLE为空

失败的原因我们来仔细探究一下

函数/命令的返回值

在shell中函数/命令实际是无法将一个值带回到调用方的,return的是函数执行的状态,而命令展开等展开的实际是标准输出的数据。而true/false/test命令都是没有标准输出的。这里我们期望的是 $ENABLE 执行true命令,在将true值输出到标准输出再与 [[ $TYPE = "Debug" ]] 的输出结合,实际情况并非如此

如何符合预期的修改

写了个函数

  1. getBoolValue(){
  2. ## 将字符串当做命令执行
  3. if eval $*
  4. then
  5. echo true
  6. else
  7. echo false
  8. fi
  9. }
  10. ENABLE=true
  11. TYPE=Debug
  12. ## &&需要转义才能作为getBoolValue函数的参数
  13. ## 否则会被看做getBoolValue $ENABLE作为一个整体[[ $TYPE = "Debug" ]]作为一个整体,然后&&操作
  14. ENABLE=$(getBoolValue $ENABLE \&\& [[ $TYPE = "Debug" ]])
  15. echo $ENABLE

当然这个函数只是写来试验下,没有太多的实际价值

总结

  • if后面的条件可以是普通的命令
  • true/false是内建的命令,不做任何事情,除了以一个0或1退出状态来终止执行。
  • test命令只能处理它支持的字符串/文件/整数表达式,命令/函数作为参数只会当做字符串不会得到正确的结果
  • 尽量使用[[ ]] 代替 [ ]
  • [[ ]]中的空格需要注意
  • 使用到数字运算和比较使用(( ))

参考资料

[shell if [[ ]]和 ]区别 || &&
流程控制:if 分支结构