# 流程控制

# 语句块

  • 用括号 ( ) 或花括号 { } 可以声明一个语句块,如下:
    (
    echo Hello
    echo World
    ) &> /dev/null
    
    [root@CentOS ~]# ( b=42 ); echo $b    # 括号的语句块会创建一个子 Shell 去执行
    
    [root@CentOS ~]# { a=42; }; echo $a   # 花括号的语句块会放在当前 Shell 中执行
    42
    

# 逻辑运算符

  • ! :非运算符。
    • 将它放到一条命令之前,会对该命令的返回码进行逻辑非运算。
    • 例:
      [root@CentOS ~]# !
      [root@CentOS ~]# echo $?
      1
      [root@CentOS ~]# ! echo Hello
      Hello
      [root@CentOS ~]# echo $?         # echo 命令执行成功,返回码本来为 0 ,但被 ! 变成了 1
      1
      [root@CentOS ~]# ! ech Hello
      -bash: ech: command not found
      [root@CentOS ~]# echo $?         # ech 命令执行失败,返回码本来为非 0 ,但被 ! 变成了 0
      0
      
  • && :与运算符。
    • 用它连接两条命令,则前一条命令返回码为 0 时才会执行后一条命令。
  • || :或运算符。
    • 用它连接两条命令,则前一条命令返回码为非 0 时才会执行后一条命令。
    • 例:
      • ls f1 && touch f2 :若存在文件 f1 ,则创建文件 f2 。
      • ls f1 || touch f1 :若不存在文件 f1 ,则创建文件 f1 。
      • ls f1 && echo True || echo False :若第一条命令返回码为 0 ,则执行第二条命令,否则执行第三条命令。
      • ls f1 || touch f1 && touch f2 :若第一条命令返回码为 0 ,则执行第三条命令;若第一条命令返回码为非 0 ,则执行第二条命令,然后根据第二条命令的返回码,判断是否执行第三条命令。

# 条件表达式

  • Shell 中,任意命令都可以用作条件表达式。如果该命令的返回码为 0 ,则视作逻辑真;如果为非 0 ,则视作逻辑假。
  • 不过最常用作条件表达式的命令是 test[ ][[ ]]

# test

:用于检测一个表达式的真假。如果表达式为真,则 test 命令的返回码为 0 ,否则为 1 。

  • 命令:

    $ test   [expression]     # 判断表达式的真假
           ! [expression]     # 非运算
    

    expression 有多种:

    -n string   # nonzero ,检测字符串是否不为空,即长度不为 0 。可以省略 -n
    -z string   # zero ,检测字符串是否为空
    Hi == hi    # 检测两个字符串是否相同
    Hi != hi    # 检测两个字符串是否不同
    
    1 -eq 2     # 检测两个数是否相等
    1 -ne 2     # 不等于
    1 -gt 2     # 大于(greater than)
    1 -ge 2     # 大于等于
    1 -lt 2     # 小于(lower than)
    1 -le 2     # 小于等于
    
    -e <path>   # 检测该路径是否存在
    -f <path>   # 检测该路径是否存在,并且为普通文件
    -d <path>   # 检测该路径是否存在,并且为目录
    -r <path>   # 检测该路径是否存在,并且有可读权限。类似的还有-w、-x
    
  • 例:

    [root@CentOS ~]# test ; echo $?           # 字符串为空,因此表达式为假,返回码为 1
    1
    [root@CentOS ~]# test 0; echo $?          # 字符串内容为 0 ,不为空,因此表达式为真,返回码为 0
    0
    [root@CentOS ~]# test ! Hello; echo $?    # 字符串不为空,但进行了非运算
    1
    
    [root@CentOS ~]# test Hello     && echo True || echo False
    True
    [root@CentOS ~]# test Hi == hi  && echo True || echo False
    False
    [root@CentOS ~]# test 3 == 3.0  && echo True || echo False  # 注意 == 不能用于检测数值是否相等
    False
    
  • test 命令中可以用 -a、-o、! 三个符号构成与或非表达式。如下:

    test -d /tmp -a -r /tmp    # 检测 /tmp 目录是否存在,且拥有可读权限
    test -r /tmp -o -w /tmp    # 检测是否拥有可读或可写权限
    test ! ""                  # 检测字符串是否不为空
    
  • 当 test 命令不使用命令选项时:

    • 如果表达式为空,则检测结果为假。如下:
      [root@CentOS ~]# test        && echo True || echo False
      False
      
    • 如果表达式为数值,则检测结果总是为真。如下:
      [root@CentOS ~]# test 0      && echo True || echo False
      True
      
    • 如果表达式为字符串,则检测结果为真。但如果是空字符串,则检测结果为假。如下:
      [root@CentOS ~]# test hello  && echo True || echo False
      True
      [root@CentOS ~]# test ""     && echo True || echo False
      False
      
    • 注意表达式只能输入一个,不能输入多个。如下:
      [root@CentOS ~]# FILES=`ls`
      [root@CentOS ~]# test $FILES                                # 输入为一个数组,导致报错
      -bash: test: too many arguments
      [root@CentOS ~]# test "$FILES" && echo True || echo False   # 将输入从数组类型转换成字符串类型
      True
      

# 中括号

[ ]

  • [ 是一个内置命令,必须与 ] 成对使用,用法相当于 test 命令。
  • 例:
    [root@CentOS ~]# [ -d /tmp ] && echo True || echo False
    True
    [root@CentOS ~]# [ Hi == hi ] && echo True || echo False
    False
    [root@CentOS ~]# [ Hi != hi ] && echo True || echo False
    True
    
  • 注意运算符、中括号的前后要用空格分隔,以免被 Shell 解释器视作同一个字符串。如下:
    [root@CentOS ~]# [ Hi==hi ] && echo True || echo False    # 此时将 Hi==hi 看作一个字符串,所以结果为 True
    True
    [root@CentOS ~]# [Hi == hi ] && echo True || echo False   # 此时将 [Hi 看作一个命令名
    -bash: [Hi: command not found
    False
    

# 双中括号

[[ ]]

  • [[ 是一个 Shell 关键字,必须与 ]] 成对使用。
  • [[ ]] 的功能比 [ ] 更多,包括:
    • 双中括号内支持使用运算符 &&||
    • 双中括号内支持使用运算符 =~ 进行字符串的正则匹配。
  • 例:
    [root@CentOS ~]# [[ 1 != 2 && 2 != 3 ]] && echo True || echo False
    True
    [root@CentOS ~]# [[ Hi =~ H.* ]] && echo True || echo False   # =~ 用于判断右侧的 pattern 是否正则匹配左侧的字符串
    True
    

# 选择结构

# if

  • 单重 if :

    if <条件表达式>
    then
       <语句块>
    fi
    
    • 加上分号作为分隔符,就可以写成单行:
      if ...; then ...; fi
      
    • 可以用任意命令作为条件表达式,如下:
      [root@CentOS ~]# if echo Hello  ; then echo True; else echo False; fi
      Hello
      True
      [root@CentOS ~]# if test Hello  ; then echo True; else echo False; fi
      True
      [root@CentOS ~]# if [ Hello ]   ; then echo True; else echo False; fi
      True
      [root@CentOS ~]# if [ -d /tmp ] ; then echo True; else echo False; fi
      True
      
    • 注意:条件表达式执行出错时,会被 if 视作逻辑假,然后执行 else 语句块。如下:
      [root@CentOS ~]# if ech Hello   ; then echo True; else echo False; fi
      -bash: ech: command not found
      False
      
    • 可以通过逻辑运算符组合多个条件,它们的最终结果,决定了 if 的逻辑真假。例:
      if [ "${FLAG_1,,}" = true ] && [ "${FLAG_2,,}" = true ] || [ "${FLAG_3,,}" = true ]
      then
          ...
      else
          ...
      fi
      
  • 双重 if :

    if  ...; then
        ...
    else
        ...
    fi
    
  • 多重 if :

    if  ...; then
        ...
    elif ...; then
        ...
    else
        ...
    fi
    
  • if 语句以 fi 结尾,case 语句以 esac 结尾(都是将名字反过来写表示结束),不过 while 语句以 done 结尾。

# case

  • 格式:
    case $name in
        "hello")      # 每种情况的值以右括号 ) 结尾。这里是当 name 等于 hello 时执行该语句块
            语句块
            ;;        # 每个语句块以两个分号 ;; 结尾
        "")           # 当 name 为空字符串时执行该语句块
            语句块
            ;;
        *)            # 匹配剩下的所有情况
            语句块
            ;;
    esac
    

# 循环结构

  • for、while、until 循环体内都支持使用 breakcontinue

# for

  • C 风格的写法:

    for (( i=0 ; i<10 ; i++ ))    # 条件循环
    do
        echo hello
    done
    
  • 遍历风格的写法:

    [root@CentOS ~]# for i in one two three                       # 遍历多个字符串
    >   do
    >       echo $i,
    >   done
    one,
    two,
    three,
    [root@CentOS ~]# for i in one two   three; do echo $i,; done  # 遍历的多个字符串之间,可以用任意个空格分隔
    one,
    two,
    three,
    [root@CentOS ~]# for i in "one two three"; do echo $i,; done  # 这是只遍历一个字符串
    one two three,
    [root@CentOS ~]# for i in "one"; do echo $i,; done            # 遍历一个字符串时,并不会像 Python 那样遍历单个字符
    one,
    
    for i in {1..5}       # 遍历一个等差数列
    for i in `ls /root`   # 遍历一组文件名
    
  • 例:执行一个命令,如果执行失败则最多重试 3 次

    for i in {1..3}
    do
        <command> && break
    done
    

# while

  • 格式:
    while [ ... ]     # 当条件表达式为真时进入循环
    do
        ...
    done
    
  • 例:逐行遍历一个文件
    cat /etc/os-release | while IFS= read -r line
    do
        echo "$line"
    done
    
    • IFS 是 shell 的内部变量,称为内部字段分隔符(Internal Field Separator),默认值为 \x0b\t\n
      • 将它赋值为空,用于保留每行开头、末尾的空字符。

# until

  • 格式:
    until [ ... ]     # 当条件表达式为真时退出循环
    do
        ...
    done
    

# 函数

  • 定义函数的一般格式:
    name() { block }
    
  • 有些 Shell 解释器使用关键字 function 定义函数:
    function name() { block }
    
    function name { block }
    
  • 例:创建局部变量
    fun1() {
        a=1             # 在函数内创建的变量默认为全局变量
        local b=2       # 可用 local 创建局部变量
        # return 0      # 可用 return 返回一个整数。如果没有 return ,则默认将函数执行的最后一个命令的返回值作为函数的返回值
    }
    
    [root@CentOS ~]# fun1     # 定义函数之后,便可调用函数名
    [root@CentOS ~]# echo $a
    [root@CentOS ~]# echo $b
    
    • 当函数运行结束时,会自动销毁所有局部变量。
    • 读取一个变量时,如果同时存在同名的局部变量与全局变量,则优先读取局部变量。
  • 例:给函数输入参数,并检查输入参数的数量
    log() {
        [ $# -ge 2 ] || {
            echo "Usage: ${FUNCNAME[0]} <level> <message>"
            return 1
        }
        local level=$1
        local message=${@:2}
        echo `date +"%Y/%m/%d %H:%M:%S"` $level $message
    }
    
    [root@CentOS ~]# log
    Usage: log <level> <message>
    [root@CentOS ~]# log INFO testing
    2022/01/01 12:01:31 INFO testing
    

# 相关命令

# sleep

$ sleep <n>[suffix]...    # 让终端睡眠一定时间,这会阻塞等待
  • suffix 可以是 s、m、h、d ,分别代表秒、分钟、小时、天。
  • 例:
    sleep 1
    sleep 1d
    sleep 1m 10s
    

# true、false

  • 执行 true、false 命令时,其后的内容会被忽略,相当于注释。
    $ true [line]     # 该命令的返回码总是 0
    
    $ false [line]    # 该命令的返回码总是 1
    
    • true 命令的别名为 : 。
  • 例:
    [root@CentOS ~]# true touch f1 && ls f1
    ls: cannot access 'f1': No such file or directory
    [root@CentOS ~]# true `touch f1` && ls f1   # 嵌套的命令依然会被执行
    f1
    [root@CentOS ~]# true ; echo $?
    0
    [root@CentOS ~]# ! true ; echo $?
    1