在上一章中,我们开发了一个菜单驱动程序来生成各种系统信息。该程序工作正常,但仍然存在严重的可用性问题。它只执行一个选项,然后终止。更糟糕的是,如果做出了无效的选择,程序将以错误终止,而不会给用户重试的机会。如果我们能以某种方式构建程序,使其能够一遍又一遍地重复菜单显示和选择,直到用户选择退出程序,那就更好了。
在本章中,我们将介绍一个称为循环(looping)的编程概念,它可用于使程序的部分重复。shell提供了三个用于循环的复合命令。我们将在本章中介绍其中的两个,在后面的章节中介绍第三个。
日常生活充满了重复的活动。每天上班、遛狗和切胡萝卜都是需要重复一系列步骤的任务。让我们考虑切一根胡萝卜。如果我们用伪代码表示此活动,它可能看起来像这样:
1.拿砧板  2.拿刀  3.把胡萝卜放在砧板上  4.举起刀  5.推动胡萝卜  6.切胡萝卜  7.如果整根胡萝卜都切成薄片,那就退出;否则转到步骤4
步骤4到7形成一个循环。重复循环中的操作,直到达到“整根胡萝卜切片”的条件。
whilebash可以表达类似的想法。假设我们想按从1到5的顺序显示五个数字。bash脚本可以按如下方式构造:
xxxxxxxxxx# while-count: display a series of numbers count=1 while [[ "$count" -le 5 ]]; do     echo "$count"     count=$((count + 1)) doneecho "Finished."执行时,此脚本显示以下内容:
xxxxxxxxxx[me@linuxbox ~]$ while-count12345Finished.while 命令的语法如下:
while commands; do commands; done
以上脚本可以简化为一行命令:
xxxxxxxxxx[me@linuxbox ~]$ count=1;while [[ "$count" -le 5 ]]; do echo "$count" ; count=$((count + 1)); done;echo "Finished."类似 if , while 评估命令列表的退出状态。只要退出状态为零,它就会在循环内执行命令。在前面的脚本中,创建了变量 count 并为其分配了初始值1。 while 命令评估 [[ ]] 复合命令的退出状态。只要 [[ ]] 命令返回退出状态为零,循环中的命令就会被执行。在每个循环结束时,重复 [[ ]] 命令。在循环的五次迭代后, count 的值增加到6, [[ ]] 命令不再返回退出状态为零,循环终止。程序继续执行循环后的下一个语句。
我们可以使用 while 循环来改进上一章的读取菜单程序。
xxxxxxxxxx# while-menu: a menu driven system information program DELAY=3 # Number of seconds to display results while [[ "$REPLY" != 0 ]]; do     clear     cat << _EOF_         Please Select:                 1. Display System Information         2. Display Disk Space         3. Display Home Space Utilization         0. Quit _EOF_     read -r -p "Enter selection [0-3] > "         if [[ "$REPLY" =~ ^[0-3]$ ]]; then         if [[ $REPLY == 1 ]]; then             echo "Hostname: $HOSTNAME"             uptime             sleep "$DELAY"         fi         if [[ "$REPLY" == 2 ]]; then             df -h             sleep "$DELAY"         fi         if [[ "$REPLY" == 3 ]]; then             if [[ "$(id -u)" -eq 0 ]]; then                 echo "Home Space Utilization (All Users)"                 du -sh /home/*             else                 echo "Home Space Utilization ($USER)"                 du -sh "$HOME"             fi             sleep "$DELAY"        fi     else         echo "Invalid entry."         sleep "$DELAY"     fi doneecho "Program terminated."通过将菜单封闭在while循环中,我们可以让程序在每次选择后重复显示菜单。只要 REPLY 不等于0,循环就会继续,菜单就会再次显示,让用户有机会进行另一次选择。在每个操作结束时,都会执行 sleep 命令,因此程序将暂停几秒钟,以便在屏幕被清除和菜单重新显示之前看到选择的结果。一旦 REPLY 等于0,表示“退出”选择,循环终止,执行继续,完成以下行。
break 和 continuebash提供了两个内置命令,可用于控制循环内的程序流。
break 命令会立即终止循环,程序控制将继续执行循环后的下一条语句。continue 命令将跳过循环的其余部分,程序控制将在循环的下一次迭代中恢复。在这里,我们看到一个包含 break 和 continue 的 while-menu 程序版本:
xxxxxxxxxx# while-menu: a menu driven system information program DELAY=3 # Number of seconds to display results while true; do     clear     cat << _EOF_         Please Select:                 1. Display System Information         2. Display Disk Space         3. Display Home Space Utilization         0. Quit _EOF_     read -r -p "Enter selection [0-3] > "         if [[ "$REPLY" =~ ^[0-3]$ ]]; then         if [[ $REPLY == 1 ]]; then             echo "Hostname: $HOSTNAME"             uptime             sleep "$DELAY"             continue        fi         if [[ "$REPLY" == 2 ]]; then             df -h             sleep "$DELAY"             continue        fi         if [[ "$REPLY" == 3 ]]; then             if [[ "$(id -u)" -eq 0 ]]; then                 echo "Home Space Utilization (All Users)"                 du -sh /home/*             else                 echo "Home Space Utilization ($USER)"                 du -sh "$HOME"             fi             sleep "$DELAY"            continue        fi         if [[ "$REPLY" == 0 ]]; then            break        fi    else         echo "Invalid entry."         sleep "$DELAY"     fi doneecho "Program terminated."在这个版本的脚本中,我们通过使用 true 命令向 while 提供退出状态来设置一个无休止的循环(一个永远不会自行终止的循环)。由于 true 总是以退出状态为零退出,因此循环永远不会结束。这是一种令人惊讶的常见脚本技术。由于循环永远不会自行结束,因此程序员有责任在适当的时候提供一些打破循环的方法。在此脚本中,当选择0选项时,使用 break 命令退出循环。 continue 命令已包含在其他脚本选项的末尾,以实现更高效的执行。通过使用 continue ,脚本将跳过识别选择时不需要的代码。例如,如果选择并识别了1个选项,则没有理由测试其他选项。
select这将是一个很好的时机来提及用于创建循环菜单的 select  shell内置。它的语法如下:
select var in [string...;] do commands ; done
其中 var 是一个变量, string 是菜单选项的文本。
当 select 执行时,它会显示 sting ,后跟PS3(提示字符串prompt string 3)变量的内容,作为用户输入的提示。一旦做出选择,它就会使用用户的输入设置 REPLY 变量(就像读取一样),并在变量 var 中返回与选择相关的字符串。一旦设置了值,就会执行命令,并再次显示另一个选择的提示。这听起来有点令人困惑,但我们可以用这个小脚本来演示:
xxxxxxxxxx# select-demo: select builtin demoPS3="Your choice: "select my_choice in First Second Third Fourth Quit; do    echo "REPLY= $REPLY my_choice= $my_choice"    [[ "$my_choice" == "Quit" ]] && breakdone首先,我们用所需的提示字符串设置 PS3 变量的内容。接下来,我们执行 select 。在这个例子中,我们有五个字符串,虽然我们使用了单个单词作为字符串,但我们可以使用任何类型的引用文本。对于我们的命令,我们只需回显 select 所做的分配。我们还测试了变量 my_choice 的内容,看看用户是否选择了“Quit”选项,如果是,我们执行一个中断以退出循环。
xxxxxxxxxx[me@linuxbox ~]$ select-demo1) First2) Second3) Third4) Fourth5) QuitYour choice:当 select 首次执行时,它会显示我们的每个字符串,前面是一个数字,后面是提示字符串。用户接下来输入代表所需选择的数字。然后, select 命令将 REPLY 变量设置为包含用户输入的任何内容以及相应的字符串(如果有的话)。
xxxxxxxxxxYour choice: 1REPLY= 1 my_choice= FirstYour choice: 2REPLY= 2 my_choice= Second在这里,我们看到用户输入了“1”, echo 命令显示了 REPLY 和 my_choice 变量的值。 select 将重复显示提示字符串,直到用户输入“5”。如果用户输入了无效值, select 会将 my_choice 设置为空字符串。如果用户只是键入 Enter ,这将导致select重新开始并重新显示菜单选项列表。
xxxxxxxxxxYour choice: 6REPLY= 6 my_choice=Your choice: abcREPLY= abc my_choice=select 循环将无限期地继续,直到遇到中断命令或用户键入 ctrl-d 表示文件结束。
xxxxxxxxxxYour choice: 5REPLY= 5 my_choice= Quit[me@linuxbox ~]$select 的一个有趣特性是,它不会在标准输出上显示菜单选项或提示字符串,而是使用标准错误。这实际上很方便,因为它允许重定向循环中命令所做的实际工作,例如:
xxxxxxxxxx[me@linuxbox ~]$ select-demo > choices.txt当我们执行此重定向时,菜单和提示仍会显示,但 echo 命令的输出会被重定向。
让我们制作一个系统信息脚本的替代版本,用 select 替换之前的 while 循环。
xxxxxxxxxx# select-menu: a menu driven system information programDELAY=3 # Number of seconds to display resultsPS3="Enter selection [1-4] > "select str in \    "Display System Information" \    "Display Disk Space" \    "Display Home Space Utilization" \    "Quit"; do    if [[ "$REPLY" == "1" ]]; then        echo "Hostname: $HOSTNAME"        uptime        sleep "$DELAY"        continue    fi    if [[ "$REPLY" == "2" ]]; then        df -h        sleep "$DELAY"        continue    fi    if [[ "$REPLY" == "3" ]]; then        if [[ $(id -u) -eq 0 ]]; then            echo "Home Space Utilization (All Users)"            du -sh /home/* 2> /dev/null        else            echo "Home Space Utilization ($USER)"            du -sh "$HOME" 2> /dev/null        fi        sleep "$DELAY"        continue    fi    if [[ "$REPLY" == "4" ]]; then        break    fi    if [[ -z "$str" ]]; then        echo "Invalid entry."        sleep "$DELAY"    fidoneecho "Program terminated."在我们的备用脚本中,我们设置 PS3 变量,然后用四个字符串调用 select 。虽然我们随后可以通过 select 测试 str 变量集,但测试 REPLY 变量并采取相应行动更容易。在循环结束时,我们检查 str 变量的长度是否为零,表示值无效。
那么,我们应该使用哪种方法来构建菜单呢? select 命令很有趣,但除了在菜单显示中使用标准错误外,它并没有真正为我们节省多少编码工作,而且它极大地限制了菜单显示的视觉设计。
untiluntil 命令与 while 非常相似,除了当遇到非零退出状态时,它不会退出循环,而是相反。until loop 会一直继续,直到它收到零退出状态。在 while-count 脚本中,只要 count 变量的值小于或等于5,我们就继续循环。通过使用 until 对脚本进行编码,我们可以得到相同的结果。
xxxxxxxxxx# until-count: display a series of numbers count=1 until [[ "$count" -gt 5 ]]; do     echo "$count"    count=$((count + 1)) doneecho "Finished."通过将测试表达式更改为 $count -gt 5 ,until 将在正确的时间终止循环。选用 while 或 until 循环的决定,通常取决于哪一种能编写出最清晰测试的循环。
while 和 until 可以处理标准输入。这允许使用 while和 until循环处理文件。在下面的例子中,我们将显示前面章节使用的 distros.txt 文件的内容:
xxxxxxxxxx# while-read: read lines from a file while read -r distro version release; do     printf "Distro: %s\tVersion: %s\tReleased: %s\n" \         "$distro" \         "$version" \         "$release"done < distros.txt为了将文件重定向到循环,我们将重定向运算符放在 done 语句之后。循环将使用 read 从重定向文件中输入字段。读取命令将在读取每一行后退出,退出状态为零,直到到达文件末尾。此时,它将以非零退出状态退出,从而终止循环。也可以将标准输入管道到回路中。
xxxxxxxxxx# while-read2: read lines from a file sort -k 1,1 -k 2n distros.txt | while read -r distro version release; do     printf "Distro: %s\tVersion: %s\tReleased: %s\n" \     "$distro" \     "$version" \     "$release"done在这里,我们获取 sort 命令的输出并显示文本流。但是,重要的是要记住,由于管道将在子shell中执行循环,因此当循环终止时,在循环中创建或分配的任何变量都将丢失。
随着循环的引入以及我们之前遇到的分支、子程序和序列,我们已经介绍了程序中使用的主要类型的流控制。bash还有更多的技巧,但它们是对这些基本概念的改进。