美文网首页Shell
Shell进阶脚本-循环与分支

Shell进阶脚本-循环与分支

作者: Chris0Yang | 来源:发表于2021-11-26 21:40 被阅读0次

    循环

    循环是当循环控制条件为真时,一系列命令迭代执行的代码块

    for 循环

    for arg in [list]

    这是 shell 中最基本的循环结构,它与C语言形式的循环有着明显的不同

    for arg in [list]
    do
      command(s)...
    done
    

    在循环的过程中,arg 会从 list 中连续获得每一个变量的值

    for arg in "$var1" "$var2" "$var3" ... "$varN"
    # 第一次循环中,arg = $var1
    # 第二次循环中,arg = $var2
    # 第三次循环中,arg = $var3
    # ...
    # 第 N 次循环中,arg = $varN
    
    # 为了防止可能的字符分割问题,[list] 中的参数都需要被引用。
    

    参数 list 中允许含有通配符

    如果 dofor 写在同一行时,需要在 list 之后加上一个分号

    for arg in [list] ; do

    样例-1. 简单的 for 循环

    #!/bin/bash
    # 列出太阳系的所有行星。
    
    for planet in Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto
    do
      echo $planet  # 每一行输出一个行星。
    done
    
    echo; echo
    
    for planet in "Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune Pluto"
        # 所有的行星都输出在一行上。
        # 整个 'list' 被包裹在引号中时是作为一个单一的变量。
        # 为什么?因为空格也是变量的一部分。
    do
      echo $planet
    done
    
    echo; echo "Whoops! Pluto is no longer a planet!"
    
    exit 0
    

    list中的每一个元素中都可能含有多个参数。这在处理参数组中非常有用。在这种情况下,使用set命令强制解析 list 中的每一个元素,并将元素的每一个部分分配给位置参数

    样例-2. for 循环 [list] 中的每一个变量有两个参数的情况

    #!/bin/bash
    # 让行星再躺次枪。
    
    # 将每个行星与其到太阳的距离放在一起。
    
    for planet in "Mercury 36" "Venus 67" "Earth 93" "Mars 142" "Jupiter 483"
    do
      set -- $planet  #  解析变量 "planet"
                      #+ 并将其每个部分赋值给位置参数。
      # "--" 防止一些极端情况,比如 $planet 为空或者以破折号开头。
    
      # 因为位置参数会被覆盖掉,因此需要先保存原先的位置参数。
      # 你可以使用数组来保存
      #         original_params=("$@")
    
      echo "$1        $2,000,000 miles from the sum"
      #-------两个制表符---将后面的一系列 0 连到参数 $2 上。
    done
    
    exit 0
    

    一个单一变量也可以成为 for 循环中的 list
    样例-3. 文件信息:查看一个单一变量中含有的文件列表的文件信息

    #!/bin/bash
    # fileinfo.sh
    
    FILES="/usr/sbin/accept
    /usr/sbin/pwck
    /usr/sbin/chroot
    /usr/bin/fakefile
    /sbin/badblocks
    /sbin/ypbind"     # 你可能会感兴趣的一系列文件。
                      # 包含一个不存在的文件,/usr/bin/fakefile。
    
    echo
    
    for file in $FILES
    do
    
      if [ ! -e "$file" ]       # 检查文件是否存在。
      then
        echo "$file does not exist."; echo
        continue                # 继续判断下一个文件。
      fi
    
      ls -l $file | awk '{ print $8 "         file size: " $5 }'  # 输出其中的两个域。
      whatis `basename $file`   # 文件信息。
      # 脚本正常运行需要注意提前设置好 whatis 的数据。
      # 使用 root 权限运行 /usr/bin/makewhatis 可以完成。
      echo
    done
    
    exit 0
    

    for 循环中的 list 可以是一个参数

    样例-4. 操作含有一系列文件的参数

    #!/bin/bash
    
    filename="*txt"
    
    for file in $filename
    do
     echo "Contents of $file"
     echo "---"
     cat "$file"
     echo
    done
    

    如果在匹配文件扩展名的 for 循环中的 [list] 含有通配符(* 和 ?),那么将会进行文件名扩展

    样例-5. 在 for 循环中操作文件

    #!/bin/bash
    # list-glob.sh: 通过文件名扩展在 for 循环中产生 [list]。
    # 通配 = 文件名扩展。
    
    echo
    
    for file in *
    #           ^  Bash 在检测到通配表达式时,
    #+             会进行文件名扩展。
    do
      ls -l "$file"  # 列出 $PWD(当前工作目录)下的所有文件。
      #  回忆一下,通配符 "*" 会匹配所有的文件名,
      #+ 但是,在文件名扩展中,他将不会匹配以点开头的文件。
    
      #  如果没有匹配到文件,那么它将会扩展为它自身。
      #  为了防止出现这种情况,需要设置 nullglob 选项。
      #+    (shopt -s nullglob)
    done
    
    echo; echo
    
    for file in [jx]*
    do
      rm -f $file    # 删除当前目录下所有以 "j" 或 "x" 开头的文件。
      echo "Removed file \"$file\"".
    done
    
    echo
    
    exit 0
    

    如果在 for 循环中省略 in [list] 部分,那么循环将会遍历位置参数($@

    样例-6. 缺少 in [list]for 循环

    #!/bin/bash
    
    # 尝试在带参数和不带参数两种情况下调用这个脚本,观察发生了什么。
    
    for a
    do
     echo -n "$a "
    done
    
    #  缺失 'in list' 的情况下,循环会遍历 '$@'
    #+(命令行参数列表,包括空格)。
    
    echo
    
    exit 0
    

    可以在 for 循环中使用 命令代换 生成 list

    样例-7. 在 for 循环中使用命令代换生成 list

    #!/bin/bash
    # for-loopcmd.sh: 带命令代换所生成 [list] 的 for 循环
    
    NUMBERS="9 7 3 8 37.53"
    
    for number in `echo $NUMBERS`  # for number in 9 7 3 8 37.53
    do
      echo -n "$number "
    done
    
    echo
    exit 0
    

    下面是使用命令代换生成 list 的更加复杂的例子

    样例-8. 一种替代 grep 搜索二进制文件的方法

    #!/bin/bash
    # bin-grep.sh: 在二进制文件中定位匹配的字符串。
    
    # 一种替代 `grep` 搜索二进制文件的方法
    # 与 "grep -a" 的效果类似
    
    E_BADARGS=65
    E_NOFILE=66
    
    if [ $# -ne 2 ]
    then
      echo "Usage: `basename $0` search_string filename"
      exit $E_BADARGS
    fi
    
    if [ ! -f "$2" ]
    then
      echo "File \"$2\" does not exist."
      exit $E_NOFILE
    fi
    
    
    IFS=$'\012'       # 按照 Anton Filippov 的意见应该是
                      # IFS="\n"
    for word in $( strings "$2" | grep "$1" )
    # "strings" 命令列出二进制文件中的所有字符串。
    # 将结果通过管道输出到 "grep" 中,检查是不是匹配的字符串。
    do
      echo $word
    done
    
    #    可以换成下面的形式:
    #    strings "$2" | grep "$1" | tr -s "$IFS" '[\n*]'
    
    
    # 尝试运行脚本 "./bin-grep.sh mem /bin/ls"
    
    exit 0
    

    下面的例子同样展示了如何使用命令代换生成 list

    样例-9. 列出系统中的所有用户

    #!/bin/bash
    # userlist.sh
    
    PASSWORD_FILE=/etc/passwd
    n=1           # 用户数量
    
    for name in $(awk 'BEGIN{fs=":"}{print $1}' < "$PASSWORD_FILE" )
    # 分隔符 = :              ^^^^^^
    # 输出第一个域                    ^^^^^^^^
    # 读取密码文件 /etc/passwd                    ^^^^^^^^^^^^^^^^^
    do
      echo "USER #$n = $name"
      let "n += 1"
    done
    
    
    # USER #1 = root
    # USER #2 = bin
    # USER #3 = daemon
    # ...
    # USER #33 = bozo
    
    exit $?
    
    # 讨论:
    # -----
    # 一个普通用户是如何读取 /etc/passwd 文件的?
    # 提示:检查 /etc/passwd 的文件权限。
    # 这算不算是一个安全漏洞?为什么?
    

    另外一个关于 [list] 的例子也来自于命令代换

    样例-10. 检查目录中所有二进制文件

    #!/bin/bash
    # findstring.sh
    # 在指定目录的二进制文件中寻找指定的字符串。
    
    directory=/usr/bin
    fstring="Free Software Foundation"  # 查看哪些文件来自于 FSF。
    
    for file in $( find $directory -type f -name '*' | sort )
    do
      strings -f $file | grep "$fstring" | sed -e "s%$driectory%%"
      #  在 "sed" 表达式中,你需要替换掉 "/" 分隔符,
      #+ 因为 "/" 是一个会被过滤的字符。
      #  如果不做替换,将会产生一个错误。(你可以尝试一下。)
    done
    
    exit $?
    
    # 简单的练习:
    # ----------
    # 修改脚本,使其可以从命令行参数中获取 $directory 和 $fstring。
    

    最后一个关于 list 和命令代换的例子,但这个例子中的命令是一个函数

    generate_list ()
    {
      echo "one two three"
    }
    
    for word in $(generate_list)  # "word" 获得函数执行的结果。
    do
      echo "$word"
    done
    
    # one
    # two
    # three
    

    for 循环的结果可以通过管道导向至一个或多个命令中

    样例-11. 列出目录中的所有符号链接

    #!/bin/bash
    # symlinks.sh: 列出目录中的所有符号链接。
    
    directory=${1-`pwd`}
    # 如果没有特别指定,缺省目录为当前工作目录。
    # 等价于下面的代码块。
    # ---------------------------------------------------
    # ARGS=1                 # 只有一个命令行参数。
    #
    # if [ $# -ne "$ARGS" ]  # 如果不是只有一个参数的情况下
    # then
    #   directory=`pwd`      # 设为当前工作目录。
    # else
    #   directory=$1
    # fi
    # ---------------------------------------------------
    
    echo "symbolic links in directory \"$directory\""
    
    for file in "$( find $directory -type 1 )"   # -type 1 = 符号链接
    do
      echo "$file"
    done | sort                                  # 否则文件顺序会是乱序。
    #  严格的来说这里并不需要使用循环,
    #+ 因为 "find" 命令的输出结果已经被扩展成一个单一字符串了。
    #  然而,为了方便大家理解,我们使用了循环的方式。
    
    #  Dominik 'Aeneas' Schnitzer 指出,
    #+ 不引用 $( find $directory -type 1 ) 的话,
    #  脚本将在文件名包含空格时阻塞。
    
    exit 0
    
    
    # --------------------------------------------------------
    # Jean Helou 提供了另外一种方法:
    
    echo "symbolic links in directory \"$directory\""
    # 备份当前的内部字段分隔符。谨慎永远没有坏处。
    OLDIFS=$IFS
    IFS=:
    
    for file in $(find $directory -type 1 -printf "%p$IFS")
    do     #                              ^^^^^^^^^^^^^^^^
           echo "$file"
           done|sort
    
    # James "Mike" Conley 建议将 Helou 的代码修改为:
    
    OLDIFS=$IFS
    IFS='' # 空的内部字段分隔符意味着将不会分隔任何字符串
    for file in $( find $directory -type 1 )
    do
      echo $file
      done | sort
    
    #  上面的代码可以在目录名包含冒号(前一个允许包含空格)
    #+ 的情况下仍旧正常工作
    

    只需要对上一个样例做一些小小的改动,就可以把在标准输出 stdout 中的循环 重定向到文件中

    样例-12. 将目录中的所有符号链接保存到文件中

    #!/bin/bash
    # symlinks.sh: 列出目录中的所有符号链接。
    
    OUTFILE=symlinks.list
    
    directory=${1-`pwd`}
    # 如果没有特别指定,缺省目录为当前工作目录。
    
    
    echo "symbolic links in directory \"$directory\"" > "$OUTFILE"
    echo "---------------------------" >> "$OUTFILE"
    
    for file in "$( find $directory -type 1 )"    # -type 1 = 符号链接
    do
      echo "$file"
    done | sort >> "$OUTFILE"                     # 将 stdout 的循环结果
    #           ^^^^^^^^^^^^^                       重定向到文件。
    
    # echo "Output file = $OUTFILE"
    
    exit $?
    

    还有另外一种看起来非常像C语言中循环那样的语法,你需要使用到 双圆括号语法

    样例-13. C语言风格的循环

    #!/bin/bash
    # 用多种方式数到10。
    
    echo
    
    # 基础版
    for a in 1 2 3 4 5 6 7 8 9 10
    do
      echo -n "$a "
    done
    
    echo; echo
    
    # +==========================================+
    
    # 使用 "seq"
    for a in `seq 10`
    do
      echo -n "$a "
    done
    
    echo; echo
    
    # +==========================================+
    
    # 使用大括号扩展语法
    # Bash 3+ 版本有效。
    for a in {1..10}
    do
      echo -n "$a "
    done
    
    echo; echo
    
    # +==========================================+
    
    # 现在用类似C语言的语法再实现一次。
    
    LIMIT=10
    
    for ((a=1; a <= LIMIT ; a++))  # 双圆括号语法,不带 $ 的 LIMIT
    do
      echo -n "$a "
    done                           
    
    echo; echo
    
    # +==========================================+
    
    # 我们现在使用C语言中的逗号运算符来使得两个变量同时增加。
    
    for ((a=1, b=1; a <= LIMIT ; a++, b++))
    do  # 逗号连接操作。
      echo -n "$a-$b "
    done
    
    echo; echo
    
    exit 0
    

    接下来,我们将展示在真实环境中应用的循环

    样例-14. 在批处理模式下使用 efax

    #!/bin/bash
    # 传真(必须提前安装了 'efax' 模块)。
    
    EXPECTED_ARGS=2
    E_BADARGS=85
    MODEM_PORT="/dev/ttyS2"   # 你的电脑可能会不一样。
    #                ^^^^^       PCMCIA 调制解调卡缺省端口。
    
    if [ $# -ne $EXPECTED_ARGS ]
    # 检查是不是传入了适当数量的命令行参数。
    then
       echo "Usage: `basename $0` phone# text-file"
       exit $E_BADARGS
    fi
    
    
    if [ ! -f "$2" ]
    then
      echo "File $2 is not a text file."
      #     File 不是一个正常文件或者文件不存在。
      exit $E_BADARGS
    fi
    
    
    fax make $2              # 根据文本文件创建传真格式文件。
    
    for file in $(ls $2.0*)  # 连接转换后的文件。
                             # 在参数列表中使用通配符(文件名通配)。
    do
      fil="$fil $file"
    done
    
    efax -d "$MODEM_PORT"  -t "T$1" $fil   # 最后使用 efax。
    # 如果上面一行执行失败,尝试添加 -o1。
    
    
    #  上面只能指出 for 循环可以被压缩为
    #     efax -d /dev/ttyS2 -o1 -t "T$1" $2.0*
    #+ 但是这并不是一个好主意。
    
    exit $?   # efax 同时也会将诊断信息传递给标准输出。
    

    关键字dodone 圈定了 for 循环代码块的范围,但是在一些特殊的情况下,也可以被大括号取代

    for((n=1; n<=10; n++))
    # 没有 do!
    {
      echo -n "* $n *"
    }
    # 没有 done!
    
    
    # 输出:
    # * 1 ** 2 ** 3 ** 4 ** 5 ** 6 ** 7 ** 8 ** 9 ** 10 *
    # 并且 echo $? 返回 0,因此 Bash 并不认为这是一个错误。
    
    
    echo
    
    
    #  但是注意在典型的 for 循环 for n in [list] ... 中,
    #+ 需要在结尾加一个分号。
    
    for n in 1 2 3
    {  echo -n "$n "; }
    #               ^
    

    while 循环

    while 循环结构会在循环顶部检测循环条件,若循环条件为真 退出状态 为0,则循环持续进行。与 for 不同的是,while 循环是在不知道循环次数的情况下使用的

    while [ condition ]
    do
      command(s)...
    done
    

    while 循环结构中,你不仅可以使用像 if/test 中那样的 括号结构,也可以使用用途更广泛的 双括号结构while [[ condition ]]
    就像在 for 循环中那样,将 do 和循环条件放在同一行时需要加一个分号。

    while [ condition ] ; do
    

    while 循环中,括号结构并不是必须存在的,比如说 getopts 结构

    样例-15. 简单的 while 循环

    #!/bin/bash
    
    var0=0
    LIMIT=10
    
    while [ "$var0" -lt "$LIMIT" ]
    #      ^                    ^
    # 必须有空格,因为这是测试结构
    do
      echo -n "$var0 "        # -n 不会另起一行
      #             ^           空格用来分开输出的数字。
    
      var0=`expr $var0 + 1`   # var0=$(($var0+1))  效果相同。
                              # var0=$((var0 + 1)) 效果相同。
                              # let "var0 += 1"    效果相同。
    done                      # 还有许多其他的方法也可以达到相同的效果。
    
    echo
    
    exit 0
    

    样例-16. 另一个例子

    #!/bin/bash
    
    echo
                                   # 等价于:
    while [ "$var1" != "end" ]     # while test "$var1" != "end"
    do
      echo "Input variable #1 (end to exit) "
      read var1                    # 不是 'read $var1' (为什么?)。
      echo "variable #1 = $var1"   # 因为存在 "#",所以需要使用引号。
      # 如果输入的是 "end",也将会在这里输出。
      # 在结束本轮循环之前都不会再测试循环条件了。
      echo
    done
    
    exit 0
    

    一个 while 循环可以有多个测试条件,但只有最后的那一个条件决定了循环是否终止。这是一种你需要注意到的不同于其他循环的语法。

    样例-17. 多条件 while 循环

    #!/bin/bash
    
    var1=unset
    previous=$var1
    
    while echo "previous-variable = $previous"
          echo
          previous=$var1
          [ "$var1" != end ] # 记录下 $var1 之前的值。
          # 在 while 循环中有4个条件,但只有最后的那个控制循环。
          # 最后一个条件的退出状态才会被记录。
    do
    echo "Input variable #1 (end to exit) "
      read var1
      echo "variable #1 = $var1"
    done
    
    # 猜猜这是怎样实现的。
    # 这是一个很小的技巧。
    
    exit 0
    

    就像 for 循环一样, while 循环也可以使用双圆括号结构写得像�C语言那样

    样例-18. C语言风格的 while 循环

    #!/bin/bash
    # wh-loopc.sh: 在 "while" 循环中计数到10。
    
    LIMIT=10                 # 循环10次。
    a=1
    
    while [ "$a" -le $LIMIT ]
    do
      echo -n "$a "
      let "a+=1"
    done                     
    
    echo; echo
    
    # +==============================================+
    
    # 现在我们用C语言风格再写一次。
    
    ((a = 1))      # a=1
    # 双圆括号结构允许像C语言一样在赋值语句中使用空格。
    
    while (( a <= LIMIT ))   #  双圆括号结构,
    do                       #+ 并且没有使用 "$"。
      echo -n "$a "
      ((a += 1))             # let "a+=1"
      # 是的,就是这样。
      # 双圆括号结构允许像C语言一样自增一个变量。
    done
    
    echo
    
    exit 0
    

    在测试部分,while 循环可以调用 函数

    t=0
    
    condition ()
    {
      ((t++))
    
      if [ $t -lt 5 ]
      then
        return 0  # true 真
      else
        return 1  # false 假
      fi
    }
    
    while condition
    #     ^^^^^^^^^
    #     调用函数循环四次。
    do
      echo "Still going: t = $t"
    done
    
    # Still going: t = 1
    # Still going: t = 2
    # Still going: t = 3
    # Still going: t = 4
    

    if 测试结构一样,while 循环也可以省略括号

    while condition
    do
      command(s) ...
    done
    

    while 循环中结合 read 命令,我们就得到了一个非常易于使用的 while read结构,它可以用来读取和解析文件

    cat $filename |    # 从文件获得输入。
    while read line    # 只要还有可以读入的行,循环就继续。
    do
      ...
    done
    
    # ==================== 摘自样例脚本 "sd.sh" =================== #
    
      while read value   # 一次读入一个数据。
      do
        rt=$(echo "scale=$SC; $rt + $value" | bc)
        (( ct++ ))
      done
    
      am=$(echo "scale=$SC; $rt / $ct" | bc)
    
      echo $am; return $ct   # 这个功能“返回”了2个值。
      # 注意:这个技巧在 $ct > 255 的情况下会失效。
      # 如果要操作更大的数字,注释掉上面的 "return $ct" 就可以了。
    } <"$datafile"   # 传入数据文件。
    

    while 循环后面可以通过 < 将标准输入 重定位到文件 中,while 循环同样可以 通过管道传入标准输入中

    until

    while 循环相反,until 循环测试其顶部的循环条件,直到其中的条件为真时停止

    until [ condition-is-true ]
    do
      commands(s)...
    done
    

    注意到,跟其他的一些编程语言不同,until 循环的测试条件在循环顶部
    就像在 for 循环中那样,将 do 和循环条件放在同一行时需要加一个分号

    until[ condition-is-true ] ; do
    

    样例-19. until 循环

    #!/bin/bash
    
    END_CONDITION=end
    
    until [ "$var1" = "$END_CONDITION" ]
    # 在循环顶部测试条件。
    do
      echo "Input variable #1 "
      echo "($END_CONDITION to exit)"
      read var1
      echo "variable #1 = $var1"
      echo
    done
    
    #                ---                   #
    
    #  就像 "for" 和 "while" 循环一样,
    #+ "until" 循环也可以写的像C语言一样。
    
    LIMIT=10
    var=0
    
    until (( var > LIMIT ))
    do  # ^^ ^     ^     ^^   没有方括号,没有 $ 前缀。
      echo -n "$var "
      (( var++ ))
    done    # 0 1 2 3 4 5 6 7 8 9 10
    
    exit 0
    

    如何在 forwhileuntil 之间做出选择?我们知道在C语言中,在已知循环次数的情况下更加倾向于使用 for 循环。但是在Bash中情况可能更加复杂一些。Bash中的 for 循环相比起其他语言来说,结构更加松散,使用更加灵活。因此使用你认为最简单的就好。

    嵌套循环

    嵌套循环,顾名思义就是在循环里面还有循环。外层循环会不断的触发内层循环直到外层循环结束。当然,你仍然可以使用 break 可以终止外层或内层的循环。

    样例-20. 嵌套循环

    #!/bin/bash
    # nested-loop.sh: 嵌套 "for" 循环。
    
    outer=1             # 设置外层循环计数器。
    
    # 外层循环。
    for a in 1 2 3 4 5 
    do
      echo "Pass $outer in outer loop."
      echo "---------------------"
      inner=1           # 重设内层循环计数器。
    
      # =====================================
      # 内层循环。
      for b in 1 2 3 4 5
      do
        echo "Pass $inner in inner loop."
        let "inner+=1"  # 增加内层循环计数器。
      done
      # 内层循环结束。
      # =====================================
    
      let "outer+=1"    # 增加外层循环计数器。
      echo              # 在每次外层循环输出中加入空行。
    done
    # 外层循环结束。
    
    exit 0
    

    循环控制

    break, continue

    break 和 continue 命令的作用和在其他编程语言中的作用一样。break 用来中止(跳出)循环,而 continue 则是略过未执行的循环部分,直接进行下一次循环。

    样例-21. 循环中 breakcontinue 的作用

    #!/bin/bash
    
    LIMIT=19  # 循环上界
    
    echo
    echo "Printing Numbers 1 through 20 (but not 3 and 11)."
    
    a=0
    
    while [ $a -le "$LIMIT" ]
    do
     a=$(($a+1))
    
     if [ "$a" -eq 3 ] || [ "$a" -eq 11 ]  # 除了 3 和 11。
     then
       continue      # 略过本次循环的剩余部分。
     fi
    
     echo -n "$a "   # 当 a 等于 3 和 11 时,将不会执行这条语句。
    done
    
    # 思考:
    # 为什么循环不会输出到20?
    
    echo; echo
    
    echo Printing Numbers 1 through 20, but something happens after 2.
    
    ##################################################################
    
    # 用 'break' 代替了 'continue'。
    
    a=0
    
    while [ "$a" -le "$LIMIT" ]
    do
     a=$(($a+1))
    
     if [ "$a" -gt 2 ]
     then
       break  # 中止循环。
     fi
    
     echo -n "$a"
    done
    
    echo; echo; echo
    
    exit 0
    

    break 命令接受一个参数,普通的 break 命令仅仅跳出其所在的那层循环,而 break N 命令则可以跳出其上 N 层的循环

    样例-22. 跳出多层循环

    #!/bin/bash
    # break-levels.sh: 跳出循环.
    
    # "break N" 跳出 N 层循环。
    
    for outerloop in 1 2 3 4 5
    do
      echo -n "Group $outerloop:   "
    
      # ------------------------------------------
      for innerloop in 1 2 3 4 5
      do
        echo -n "$innerloop "
    
        if [ "$innerloop" -eq 3 ]
        then
          break  # 尝试一下 break 2 看看会发生什么。
                 # (它同时中止了内层和外层循环。)
        fi
      done
      # ------------------------------------------
    
      echo
    done
    
    echo
    
    exit 0
    

    break 类似,continue 也接受一个参数。普通的 continue 命令仅仅影响其所在的那层循环,而 continue N 命令则可以影响其上 N 层的循环

    样例-23. continue 影响外层循环

    #!/bin/bash
    # "continue N" 命令可以影响其上 N 层循环。
    
    for outer in I II III IV V           # 外层循环
    do
      echo; echo -n "Group $outer: "
    
      # --------------------------------------------------------------------
      for inner in 1 2 3 4 5 6 7 8 9 10  # 内层循环
      do
    
        if [[ "$inner" -eq 7 && "$outer" = "III" ]]
        then
          continue 2  # 影响两层循环,包括“外层循环”。
                      # 将其替换为普通的 "continue",那么只会影响内层循环。
        fi
    
        echo -n "$inner "  # 7 8 9 10 将不会出现在 "Group III."中。
      done
      # --------------------------------------------------------------------
    
    done
    
    echo; echo
    
    # 思考:
    # 想一个 "continue N" 在脚本中的实际应用情况。
    
    exit 0
    

    样例-24. 真实环境中的 continue N

    # Albert Reiner 举出了一个如何使用 "continue N" 的例子:
    # ---------------------------------------------------
    
    #  如果我有许多任务需要运行,并且运行所需要的数据都以文件的形
    #+ 式存在文件夹中。现在有多台设备可以访问这个文件夹,我想将任
    #+ 务分配给这些不同的设备来完成。
    #  那么我通常会在每台设备上执行下面的代码:
    
    while true:
    do
      for n in .iso.*
      do
        [ "$n" = ".iso.opts" ] && continue
        beta=${n#.iso.}
        [ -r .Iso.$beta ] && continue
        [ -r .lock.$beta ] && sleep 10 && continue
        lockfile -r0 .lock.$beta || continue
        echo -n "$beta: " `date`
        run-isotherm $beta
        date
        ls -alF .Iso.$beta
        [ -r .Iso.$beta ] && rm -rf .lock.$beta
        continue 2
      done
      break
    done
    
    exit 0
    
    # 这个脚本中出现的 sleep N 只针对这个脚本,通常的形式是:
    
    while true
    do
      for job in {pattern}
      do
        {job already done or running} && continue
        {mark job as running, do job, mark job as done}
        continue 2
      done
      break        # 或者使用类似 `sleep 600` 这样的语句来防止脚本结束。
    done
    
    #  这样做可以保证脚本只会在没有任务时(包括在运行过程中添加的任务)
    #+ 才会停止。合理使用文件锁保证多台设备可以无重复的并行执行任务(这
    #+ 在我的设备上通常会消耗好几个小时,所以我想避免重复计算)。并且,
    #+ 因为每次总是从头开始搜索文件,因此可以通过文件名决定执行的先后
    #+ 顺序。当然,你可以不使用 'continue 2' 来完成这些,但是你必须
    #+ 添加代码去检测某项任务是否完成(以此判断是否可以执行下一项任务或
    #+ 终止、休眠一段时间再执行下一项任务)。
    

    continue N 结构不易理解并且可能在一些情况下有歧义,因此不建议使用。

    测试与分支

    caseselect 结构并不属于循环结构,因为它们并没有反复执行代码块。但是和循环结构相似的是,它们会根据代码块顶部或尾部的条件控制程序流。

    下面介绍两种在代码块中控制程序流的方法:

    case (in) / esac

    在 shell 脚本中,case 模拟了 C/C++ 语言中的 switch,可以根据条件跳转到其中一个分支。其相当于简写版的 if/then/else 语句。很适合用来创建菜单选项哟!

    case "$variable" in
      "$condition1" )
        command...
      ;;
      "$condition2" )
        command...
      ;;
    esac
    
    • 对变量进行引用不是必须的,因为在这里不会进行字符分割

    • 条件测试语句必须以右括号 ) 结束

    • 每一段代码块都必须以双分号 ;; 结束

    • 如果测试条件为真,其对应的代码块将被执行,而后整个 case 代码段结束执行。

    • case 代码段必须以 esac 结束(倒着拼写case

    样例-25. 如何使用 case

    #!/bin/bash
    # 测试字符的种类。
    
    echo; echo "Hit a key, then hit return."
    read Keypress
    
    case "$Keypress" in
      [[:lower:]]   ) echo "Lowercase letter";;
      [[:upper:]]   ) echo "Uppercase letter";;
      [0-9]         ) echo "Digit";;
      *             ) echo "Punctuation, whitespace, or other";;
    esac      #  字符范围可以用[方括号]表示,也可以用 POSIX 形式的[[双方括号]]表示。
    
    # 在这个例子的第一个版本中,用来测试是小写还是大写字符使用的是 [a-z] 和 [A-Z]。
    # 这在一些特定的语言环境和 Linux 发行版中不起效。
    # POSIX 形式具有更好的兼容性。
    # 感谢 Frank Wang 指出这一点。
    
    # 练习:
    # -----
    # 这个脚本接受一个单字符然后结束。
    # 修改脚本,使得其可以循环接受输入,并且检测键入的每一个字符,直到键入 "X" 为止。
    # 提示:将所有东西包在 "while" 中。
    
    exit 0
    

    样例-26. 使用 case 创建菜单

    #!/bin/bash
    
    # 简易的通讯录数据库
    
    clear # 清屏。
    
    echo "          Contact List"
    echo "          ------- ----"
    echo "Choose one of the following persons:" 
    echo
    echo "[E]vans, Roland"
    echo "[J]ones, Mildred"
    echo "[S]mith, Julie"
    echo "[Z]ane, Morris"
    echo
    
    read person
    
    case "$person" in
    # 注意变量是被引用的。
    
      "E" | "e" )
      # 同时接受大小写的输入。
      echo
      echo "Roland Evans"
      echo "4321 Flash Dr."
      echo "Hardscrabble, CO 80753"
      echo "(303) 734-9874"
      echo "(303) 734-9892 fax"
      echo "revans@zzy.net"
      echo "Business partner & old friend"
      ;;
      # 注意用双分号结束这一个选项。
    
      "J" | "j" )
      echo
      echo "Mildred Jones"
      echo "249 E. 7th St., Apt. 19"
      echo "New York, NY 10009"
      echo "(212) 533-2814"
      echo "(212) 533-9972 fax"
      echo "milliej@loisaida.com"
      echo "Ex-girlfriend"
      echo "Birthday: Feb. 11"
      ;;
    
      # Smith 和 Zane 的信息稍后添加。
    
      *         )
      # 缺省设置。
      # 空输入(直接键入回车)也是执行这一部分。
      echo
      echo "Not yet in database."
      ;;
    
    esac
    
    echo
    
    # 练习:
    # -----
    # 修改脚本,使得其可以循环接受多次输入而不是只显示一个地址后终止脚本。
    
    exit 0
    

    你可以用 case 来检测命令行参数

    #!/bin/bash
    
    case "$1" in
      "") echo "Usage: ${0##*/} <filename>"; exit $E_PARAM;;
                          # 没有命令行参数,或者第一个参数为空。
                          # 注意 ${0##*/} 是参数替换 ${var##pattern} 的一种形式。
                          # 最后的结果是 $0.
    
      -*) FILENAME=./$1;; #  如果传入的参数以短横线开头,那么将其替换为 ./$1
                          #+ 以避免后续的命令将其解释为一个选项。
    
      * ) FILENAME=$1;;   # 否则赋值为 $1。
    esac
    

    下面是一个更加直观的处理命令行参数的例子:

    #!/bin/bash
    
    while [ $# -gt 0 ]; do    # 遍历完所有参数
      case "$1" in
        -d|--debug)
                  # 检测是否是 "-d" 或者 "--debug"。
                  DEBUG=1
                  ;;
        -c|--conf)
                  CONFFILE="$2"
                  shift
                  if [ ! -f $CONFFILE ]; then
                    echo "Error: Supplied file doesn't exist!"
                    exit $E_CONFFILE     # 找不到文件。
                  fi
                  ;;
      esac
      shift       # 检测下一个参数
    done
    

    样例-27. 使用命令替换生成 case 变量

    #!/bin/bash
    # case-cmd.sh: 使用命令替换生成 "case" 变量。
    
    case $( arch ) in   # $( arch ) 返回设备架构。
                        # 等价于 'uname -m"。
      i386 ) echo "80386-based machine";;
      i486 ) echo "80486-based machine";;
      i586 ) echo "Pentium-based machine";;
      i686 ) echo "Pentium2+-based machine";;
      *    ) echo "Other type of machine";;
    esac
    
    exit 0
    

    case 还可以用来做字符串模式匹配

    样例-28. 简单的字符串匹配

    #!/bin/bash
    # match-string.sh: 使用 'case' 结构进行简单的字符串匹配。
    
    match_string ()
    { # 字符串精确匹配。
      MATCH=0
      E_NOMATCH=90
      PARAMS=2     # 需要2个参数。
      E_BAD_PARAMS=91
    
      [ $# -eq $PARAMS ] || return $E_BAD_PARAMS
    
      case "$1" in
        "$2") return $MATCH;;
        *   ) return $E_NOMATCH;;
      esac
    
    }
    
    
    a=one
    b=two
    c=three
    d=two
    
    match_string $a     # 参数个数不够
    echo $?             # 91
    
    match_string $a $b  # 匹配不到
    echo $?             # 90
    
    match_string $a $d  # 匹配成功
    echo $?             # 0
    
    exit 0
    

    样例-29. 检查输入

    #!/bin/bash
    # isaplpha.sh: 使用 "case" 结构检查输入。
    
    SUCCESS=0
    FAILURE=1   #  以前是FAILURE=-1,
                #+ 但现在 Bash 不允许返回负值。
    
    isalpha ()  # 测试字符串的第一个字符是否是字母。
    {
    if [ -z "$1" ]                # 检测是否传入参数。
    then
      return $FAILURE
    fi
    
    case "$1" in
      [a-zA-Z]*) return $SUCCESS;;  # 是否以字母形式开始?
      *        ) return $FAILURE;;
    esac
    }             # 可以与 C 语言中的函数 "isalpha ()" 作比较。
    
    
    isalpha2 ()   # 测试整个字符串是否都是字母。
    {
      [ $# -eq 1 ] || return $FAILURE
    
      case $1 in
      *[!a-zA-Z]*|"") return $FAILURE;;
                   *) return $SUCCESS;;
      esac
    }
    
    isdigit ()    # 测试整个字符串是否都是数字。
    {             # 换句话说,也就是测试是否是一个整型变量。
      [ $# -eq 1 ] || return $FAILURE
    
      case $1 in
        *[!0-9]*|"") return $FAILURE;;
                  *) return $SUCCESS;;
      esac
    }
    
    
    
    check_var ()  # 包装后的 isalpha ()。
    {
    if isalpha "$@"
    then
      echo "\"$*\" begins with an alpha character."
      if isalpha2 "$@"
      then        # 其实没必要检查第一个字符是不是字母。
        echo "\"$*\" contains only alpha characters."
      else
        echo "\"$*\" contains at least one non-alpha character."
      fi
    else
      echo "\"$*\" begins with a non-alpha character."
                  # 如果没有传入参数同样同样返回“存在非字母”。
    fi
    
    echo
    
    }
    
    digit_check ()  # 包装后的 isdigit ()。
    {
    if isdigit "$@"
    then
      echo "\"$*\" contains only digits [0 - 9]."
    else
      echo "\"$*\" has at least one non-digit character."
    fi
    
    echo
    
    }
    
    
    a=23skidoo
    b=H3llo
    c=-What?
    d=What?
    e=$(echo $b)   # 命令替换。
    f=AbcDef
    g=27234
    h=27a34
    i=27.34
    
    check_var $a
    check_var $b
    check_var $c
    check_var $d
    check_var $e
    check_var $f
    check_var     # 如果不传入参数会发送什么?
    #
    digit_check $g
    digit_check $h
    digit_check $i
    
    
    exit 0        
    
    # 练习:
    # -----
    # 写一个函数 'isfloat ()' 来检测输入值是否是浮点数。
    # 提示:可以参考函数 'isdigit ()',在其中加入检测合法的小数点即可。
    

    select

    select 构建菜单

    select variable [in list]
    do
     command...
     break
    done
    

    而效果则是终端会提示用户输入列表中的一个选项。注意,select 默认使用提示字串3(Prompt String 3,$PS3, 即#?),但同样可以被修改

    样例 11-30. 使用 select 创建菜单

    #!/bin/bash
    
    PS3='Choose your favorite vegetable: ' # 设置提示字串。
                                           # 否则默认为 #?。
    
    echo
    
    select vegetable in "beans" "carrots" "potatoes" "onions" "rutabagas"
    do
      echo
      echo "Your favorite veggie is $vegetable."
      echo "Yuck!"
      echo
      break  # 如果没有 'break' 会发生什么?
    done
    
    exit
    
    # 练习:
    # -----
    # 修改脚本,使得其可以接受其他输入而不是 "select" 语句中所指定的。
    # 例如,如果用户输入 "peas,",那么脚本会通知用户 "Sorry. That is not on the menu."
    

    如果 in list 被省略,那么 select 将会使用传入脚本的命令行参数 $@ 或者传入函数的参数作为 list
    可以与 for variable in listin list 被省略的情况做比较

    样例-31. 在函数中使用 select 创建菜单

    #!/bin/bash
    
    PS3='Choose your favorite vegetable: '
    
    echo
    
    choice_of()
    {
    select vegetable
    # [in list] 被省略,因此 'select' 将会使用传入函数的参数作为 list。
    do
      echo
      echo "Your favorite veggie is $vegetable."
      echo "Yuck!"
      echo
      break
    done
    }
    
    choice_of beans rice carrorts radishes rutabaga spinach
    #         $1    $2   $3      $4       $5       $6
    #         传入了函数 choice_of()
    
    exit 0
    

    相关文章

      网友评论

        本文标题:Shell进阶脚本-循环与分支

        本文链接:https://www.haomeiwen.com/subject/wplgtrtx.html