11. 循环与分支 - 11.1 循环

优质
小牛编辑
128浏览
2023-12-01

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

for 循环

for arg in [list]

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

  1. for arg in [list]
  2. do
  3. command(s)...
  4. done

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

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

参数 list 中允许含有 通配符

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

for arg in [list] ; do

样例 11-1. 简单的 for 循环

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

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

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

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

一个单一变量也可以成为 for 循环中的 [list]。

样例 11-3. 文件信息:查看一个单一变量中含有的文件列表的文件信息

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

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

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

  1. #!/bin/bash
  2. filename="*txt"
  3. for file in $filename
  4. do
  5. echo "Contents of $file"
  6. echo "---"
  7. cat "$file"
  8. echo
  9. done

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

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

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

如果在 for 循环中省略 in [list] 部分,那么循环将会遍历位置参数($@)。样例 A-15 中使用到了这一点。也可以查看 样例 15-17

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

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

可以在 for 循环中使用 命令代换 生成 [list]。查看 样例 16-54样例 11-11样例 16-48

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

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

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

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

  1. #!/bin/bash
  2. # bin-grep.sh: 在二进制文件中定位匹配的字符串。
  3. # 一种替代 `grep` 搜索二进制文件的方法
  4. # 与 "grep -a" 的效果类似
  5. E_BADARGS=65
  6. E_NOFILE=66
  7. if [ $# -ne 2 ]
  8. then
  9. echo "Usage: `basename $0` search_string filename"
  10. exit $E_BADARGS
  11. fi
  12. if [ ! -f "$2" ]
  13. then
  14. echo "File "$2" does not exist."
  15. exit $E_NOFILE
  16. fi
  17. IFS=$'12' # 按照 Anton Filippov 的意见应该是
  18. # IFS="n"
  19. for word in $( strings "$2" | grep "$1" )
  20. # "strings" 命令列出二进制文件中的所有字符串。
  21. # 将结果通过管道输出到 "grep" 中,检查是不是匹配的字符串。
  22. do
  23. echo $word
  24. done
  25. # 就像 S.C. 指出的那样,第 23-30 行可以换成下面的形式:
  26. # strings "$2" | grep "$1" | tr -s "$IFS" '[n*]'
  27. # 尝试运行脚本 "./bin-grep.sh mem /bin/ls"
  28. exit 0

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

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

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

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

样例 11-10. 检查目录中所有二进制文件的原作者

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

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

  1. generate_list ()
  2. {
  3. echo "one two three"
  4. }
  5. for word in $(generate_list) # "word" 获得函数执行的结果。
  6. do
  7. echo "$word"
  8. done
  9. # one
  10. # two
  11. # three

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

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

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

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

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

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

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

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

  1. #!/bin/bash
  2. # 用多种方式数到10。
  3. echo
  4. # 基础版
  5. for a in 1 2 3 4 5 6 7 8 9 10
  6. do
  7. echo -n "$a "
  8. done
  9. echo; echo
  10. # +==========================================+
  11. # 使用 "seq"
  12. for a in `seq 10`
  13. do
  14. echo -n "$a "
  15. done
  16. echo; echo
  17. # +==========================================+
  18. # 使用大括号扩展语法
  19. # Bash 3+ 版本有效。
  20. for a in {1..10}
  21. do
  22. echo -n "$a "
  23. done
  24. echo; echo
  25. # +==========================================+
  26. # 现在用类似C语言的语法再实现一次。
  27. LIMIT=10
  28. for ((a=1; a <= LIMIT ; a++)) # 双圆括号语法,不带 $ 的 LIMIT
  29. do
  30. echo -n "$a "
  31. done # 从 ksh93 中学习到的特性。
  32. echo; echo
  33. # +==========================================+
  34. # 我们现在使用C语言中的逗号运算符来使得两个变量同时增加。
  35. for ((a=1, b=1; a <= LIMIT ; a++, b++))
  36. do # 逗号连接操作。
  37. echo -n "$a-$b "
  38. done
  39. echo; echo
  40. exit 0

还可以查看 样例 27-16样例 27-17样例 A-6

-—

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

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

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

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

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

while 循环

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

  1. while [ condition ]
  2. do
  3. command(s)...
  4. done

while 循环结构中,你不仅可以使用像 if/test 中那样的 括号结构,也可以使用用途更广泛的 双括号结构while [[ condition ]])。

就像在 for 循环中那样,将 do 和循环条件放在同一行时需要加一个分号。

while [ condition ] ; do

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

样例 11-15. 简单的 while 循环

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

样例 11-16. 另一个例子

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

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

样例 11-17. 多条件 while 循环

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

就像 for 循环一样, while 循环也可以使用双圆括号结构写得像C语言那样(也可以查看样例 8-5)。

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

  1. #!/bin/bash
  2. # wh-loopc.sh: 在 "while" 循环中计数到10。
  3. LIMIT=10 # 循环10次。
  4. a=1
  5. while [ "$a" -le $LIMIT ]
  6. do
  7. echo -n "$a "
  8. let "a+=1"
  9. done # 没什么好奇怪的吧。
  10. echo; echo
  11. # +==============================================+
  12. # 现在我们用C语言风格再写一次。
  13. ((a = 1)) # a=1
  14. # 双圆括号结构允许像C语言一样在赋值语句中使用空格。
  15. while (( a <= LIMIT )) # 双圆括号结构,
  16. do #+ 并且没有使用 "$"。
  17. echo -n "$a "
  18. ((a += 1)) # let "a+=1"
  19. # 是的,就是这样。
  20. # 双圆括号结构允许像C语言一样自增一个变量。
  21. done
  22. echo
  23. # 这可以让C和Java程序猿感觉更加舒服。
  24. exit 0

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

  1. t=0
  2. condition ()
  3. {
  4. ((t++))
  5. if [ $t -lt 5 ]
  6. then
  7. return 0 # true 真
  8. else
  9. return 1 # false 假
  10. fi
  11. }
  12. while condition
  13. # ^^^^^^^^^
  14. # 调用函数循环四次。
  15. do
  16. echo "Still going: t = $t"
  17. done
  18. # Still going: t = 1
  19. # Still going: t = 2
  20. # Still going: t = 3
  21. # Still going: t = 4

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

  1. while condition
  2. do
  3. command(s) ...
  4. done

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

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

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

until

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

  1. until [ condition-is-true ]
  2. do
  3. commands(s)...
  4. done

注意到,跟其他的一些编程语言不同,until 循环的测试条件在循环顶部。

就像在 for 循环中那样,将 do 和循环条件放在同一行时需要加一个分号。

until[ condition-is-true ] ; do

样例 11-19. until 循环

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

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