for命令

for命令可以执行指定次数的一个或多个命令。其基本格式如下:

  1. do
  2. command
  3. command
  4. ...
  5. done

dodone之间的命令叫做循环体,其执行的次数由in后面的列表条目个数而定。在执行循环时,第一个单词word1被赋给变量var,然后执行循环体。接下来,列表中第二个单词word2被赋给变量var,再执行循环体。这个过程会持续下去,列表中后续的单词被赋给var,执行循环体中的命令,直至处理完列表中最后一个单词wordn。这时列表中已经没有单词了,for命令也随之结束。done之后的命令继续得以执行。

  1. for i in 1 2 3
  2. do
  3. echo $i
  4. done

$@$*变量

我们要编写一个名为args的程序,它可以一行一个的方式显示出命令行中所有的参数。

  1. echo Number of arguments passed is $#
  2. for arg in $*
  3. do
  4. echo $arg
  5. done

现在来试一下:

  1. $ args a b c
  2. Number of arguments passed is 3
  3. a
  4. b
  5. c
  6. $ args 'a b' c
  7. Number of arguments passed is 2
  8. a
  9. b
  10. c

仔细观察第二个例子:尽管’a b’是作为单个参数传给了args,但它在for循环中仍切分成了两个值。这是因为Shell会使用a b cfor命令中的$*替换,同时丢弃引号。因此,该例的循环会执行3次。
Shell 使用$1、$2……来替换$*,但如果你使用特殊的Shell变量"$@",那么传入程序中的值则是"$1"、"$2"……关键的不同在于$@ 两边的双引号,如果没有了双引号,该变量的效果和$*无异。
回到args程序中,使用"$@"代替未引用的$*

  1. echo Number of arguments passed is $#
  2. for arg in "$@"
  3. do
  4. echo $arg
  5. done

不使用列表的for命令

在使用for命令的时候,Shell还能够识别一种特殊的写法。如果你忽略in以及后续的列表:

  1. for var
  2. do
  3. command
  4. command
  5. ...
  6. done

Shell自动遍历命令行中输入的所有参数,就和下面的格式一样:

  1. for var in "$@"
  2. do
  3. command
  4. command
  5. ...
  6. done

while命令

第二种循环命令是while语句。其命令格式为:

  1. while commandt
  2. do
  3. command
  4. command
  5. ...
  6. done

执行commandt并测试其退出状态。如果为0,执行一次dodone之间的命令。然后再次执行commandt并测试其退出状态。如果为 0,继续执行dodone之间的命令。这个过程一直持续到commandt返回非0的退出状态码。这时循环结束。接下来执行done之后的命令。注意,如果commandt在首次执行时返回非0的退出状态码,那么dodone之间的命令一次都不会执行。

  1. i=1
  2. while [ "$i" -le 5 ]
  3. do
  4. echo $i
  5. i=$((i + 1))
  6. done

while循环常和shift命令搭配使用,用于处理命令行中数量不定的参数。考虑下面的程序,该程序会打印出命令行中的所有参数,一行一个。

  1. #
  2. # 打印出命令行参数,一行一个
  3. #
  4. while [ "$#" -ne 0 ]
  5. do
  6. echo "$1"
  7. shift
  8. done

until命令

只要测试表达式返回真(0),while命令就继续执行。until命令正好相反:只要测试表达式返回假,它就会不停地执行代码块,直到返回真为止。until的一般格式如下:

  1. until commandt
  2. do
  3. command
  4. command
  5. ...
  6. done

while类似,如果commandt首次执行的时候就返回0,那么dodone之间的命令一次都不执行。尽管两个命令极为相似,但until 命令适合于编写那种需要等待特定事件发生的程序。

  1. #
  2. # 等待指定用户登录
  3. #
  4. if [ "$#" -ne 1 ]
  5. then
  6. echo "Usage: waitfor user"
  7. exit 1
  8. fi
  9. user="$1"
  10. #
  11. # 每 60 秒检查一次登录状态
  12. #
  13. until who | grep "^$user " > /dev/null
  14. do
  15. sleep 60
  16. done
  17. #
  18. # 运行到此处,说明用户已经登录
  19. #
  20. echo "$user has logged on"

循环的高级概念

跳出循环

有时候程序逻辑要求立刻退出循环语句。要想退出程序中的循环,可以使用break命令:

  1. break

break命令可用于退出这种无限循环,退出时机通常是在出现错误或者处理结束的时候:

  1. while true
  2. do
  3. cmd=$(getcmd)
  4. if [ "$cmd" = quit ]
  5. then
  6. break
  7. else
  8. processcmd "$cmd"
  9. fi
  10. done

在这里,while循环会不停地执行getcmdprocesscmd程序,直到cmd等于quit。这时会执行break命令,退出循环。
如果使用这种形式的break命令:

  1. break n

可以立即退出第n层内循环,因此,在下面的代码中:

  1. for file
  2. do
  3. ...
  4. while [ "$count" -lt 10 ]
  5. do
  6. ...
  7. if [ -n "$error" ]
  8. then
  9. break 2
  10. fi
  11. ...
  12. done
  13. ...
  14. done

如果error变量不为空,则退出whilefor循环。

跳过循环中余下的命令

continue命令类似于break,唯一的不同在于它不会退出整个循环,而只是跳过当前迭代中剩下的命令。然后程序立即进入下一次迭代,继续正常执行。和break一样,continue后面也可以加上一个可选的数字,因此:

  1. continue n

会跳过最内侧的n个循环中的命令,继续往下执行。

  1. for file
  2. do
  3. if [ ! -e "$file" ]
  4. then
  5. echo "$file not found!"
  6. continue
  7. fi
  8. #
  9. # 处理文件
  10. #
  11. ...
  12. done

检查每一个file的值,确保执行的文件存在。如果不存在的话,打印出信息并跳过for循环中相关的文件处理命令。

在后台执行循环

整个循环都可以在后台执行,这只需要在done语句后加上一个&就可以了:

  1. $ for file in memo[1-4]
  2. > do
  3. > run $file
  4. > done & 放入后台执行

这个以及随后的例子能够奏效的原因在于Shell将循环视为一种独立的小程序,因此对于任何出现在代码块关闭语句(如 done、fi 和 esac)之后的内容都可以使用重定向,也可以利用&将循环放入后台,甚至是作为命令管道的一部分。

循环上的I/O重定向

你也可以在循环上执行I/O重定向。循环输入重定向会应用于循环中所有从标准输入中读取数据的命令。由循环到文件的输出重定向会应用于循环中所有向标准输出写入的命令。所有一切的发生位置都是在循环关闭语句done

  1. $ for i in 1 2 3 4
  2. > do
  3. > echo $i
  4. > done > loopout 重定向循环的输出到 loopout

个别语句也可以不使用代码块的重定向,这就好像Shell程序中的其他语句可以直接写明从哪里读取或向哪里写入。要强制写入或从终端读取,可以利用/dev/tty,该文件总是指向终端程序,不管你用的是 Mac、Linux 还是 UNIX 系统。在下面的循环中,所有的输出都被重定向到了文件output,除了其中的echo命令,其输出被重定向到了终端:

  1. for file
  2. do
  3. echo "Processing file $file" > /dev/tty
  4. ...
  5. done > output

你也可以重定向循环的标准错误输出,只需要在done之后加上2> file就可以了:

  1. while [ "$endofdata" -ne TRUE ]
  2. do
  3. ...
  4. done 2> errors

循环中所有写入到标准错误中的输出都会被重定向到errors。2>的另一种写法常用于确保所有的错误信息都出现在终端,哪怕是脚本已经将其输出重定向到了文件或管道:

  1. echo "Error: no file" 1>&2

在默认情况下,echo会将输出写入到标准输出(文件描述符1),而文件描述符2仍旧指向标准错误,不会受到文件重定向或管道的影响。因此,上面的写法会将echo应该输出到文件描述符1的错误信息重定向到文件描述符2(标准错误)。你可以使用下面的代码来测试:

  1. for i in "once"
  2. do
  3. echo "Standard output message"
  4. echo "Error message" 1>&2
  5. done > /dev/null

将数据导入及导出循环

命令输出可以导入循环(把该命令放在循环命令之前并以管道符号结尾),循环的输出也可以导入另一个命令。在下面的例子中,for命令的输出被导入了wc

  1. $ for i in 1 2 3 4
  2. > do
  3. > echo $i
  4. > done | wc l
  5. 4

单行循环

如果你发现自己经常直接在命令行中输入循环,可能会希望试试下面这种可以在单行上输入全部命令的便捷写法:在列表最后一项后面加上一个分号,在循环中每个命令后面也加上一个分号。
下面的循环:

  1. for i in 1 2 3 4
  2. do
  3. echo $i
  4. done

可以使用便捷写法写作:

  1. for i in 1 2 3 4; do echo $i; done

getopts命令

Shell提供了一个叫做getopts的内建命令,可以轻松地处理命令行参数。该命令的一般形式为:

  1. getopts options variable

我们很快就会深入讲解字符串选项。目前,只需要知道单字母选项可以照原样写出,需要参数的选项后面要加上冒号,因此ab:c表示允许使用-a-c-b,但是-b需要另外指定参数。
getopts命令专门用来在循环中执行,这使得它能够非常方便地针对用户指定的每个选项执行所需的操作。在每次循环中,getopts都会检查下一个命令行参数,通过查看该参数是否以减号开头,随后是否是在options中指定的字符来决定选项是否有效。如果没有问题,getopts就会将匹配的选项字母保存在指定的variable中,然后返回为0的退出状态码。
如果减号后面的字符没有在options中列出,getopts会在返回为0的退出状态码之前,将问号保存在variable中。另外还会向标准错误写入错误信息,告知用户指定的选项有问题。如果命令行中已经没有选项或者当前选项不是以减号开头,getopts会返回非0的退出状态码,允许脚本接着处理其他的参数。

  1. getopts "air" option

这里的第一个参数air指定了可接受的命令选项(-a、-i 和-r),optiongetopts用来存放每个匹配值的变量名。getopts命令也允许选项在命令行中聚在一起或分组出现。这种形式可以通过一个减号,后面跟上多个连续的选项来实现。例如,foo命令可以像这样执行:

  1. foo -a -r -i

也可以像这样:

  1. foo -ari

getopts的威力可要比我们目前介绍的强大得多!例如,它还可以处理需要参数的选项。要想正确解析带有参数的选项,getopts要求选项及其参数之间至少要有一个空格。在这种情况下,选项不能写成分组的形式。在选项字母后面加上一个冒号,以此告知getopts指定的选项后面要求有一个参数。

  1. getopts mt: option

如果getopts没有在选项后找到要求的参数,它会将问号保存到变量中并向标准错误中输出错误信息。否则,就将选项字符保存在变量中,把用户指定的参数放在一个叫做OPTARG的特殊变量中。
关于getopts,还有最后一点要注意:另一个特殊变量OPTIND的初始值为1,随后每当getopts返回时都会被更新为下一个要处理的命令行参数的序号。

  1. #
  2. # 等待指定用户登录 -- 版本 3
  3. #
  4. # 设置默认值
  5. mailopt=FALSE
  6. interval=60
  7. # 处理命令选项
  8. while getopts mt: option
  9. do
  10. case "$option"
  11. in
  12. m) mailopt=TRUE;;
  13. t) interval=$OPTARG;;
  14. \?) echo "Usage: waitfor [-m] [-t n] user"
  15. echo " -m means to be informed by mail"
  16. echo " -t means check every n secs."
  17. exit 1;;
  18. esac
  19. done
  20. # 确保指定了用户名
  21. if [ "$OPTIND" -gt "$#" ]
  22. then
  23. echo "Missing user name!"
  24. exit 2
  25. fi
  26. shiftcount=$((OPTIND 1))
  27. shift $shiftcount
  28. user=$1
  29. #
  30. # 检查用户是否登录
  31. #
  32. until who | grep "^$user " > /dev/null
  33. do
  34. sleep $interval
  35. done
  36. #
  37. # 如果执行到此处,说明用户已经登录
  38. #
  39. if [ "$mailopt" = FALSE]
  40. then
  41. echo "$user has logged on"
  42. else
  43. runner=$(who am i | cut -cl-8)
  44. echo "$user has logged on" | mail $runner
  45. fi