聊一聊shell中的条件控制

3,391 阅读3分钟

之前赶鸭子上架写过一个不算太复杂的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 分支结构