第29章:流程控制:while / until 循环

上一章中,我们开发了一款菜单驱动程序来制作各种类型的系统信息。程序能正常工作,但是还有个重大的可用性问题。它仅仅执行了单个选项之后就终止了。更糟糕的是,如果做出不合法的选择,程序就会报错并终止,没有给用户重新尝试的机会。如果能构建程序使其重复显示菜单并可以重复选择,直到用户自己选择退出程序为止,则会更友好。

本章中,我们将学习一个编程概念,循环(looping),使得部分程序可以重复。shell 提供了三种循环的复合命令。这里,我们将学两种,第三种将在后续章节中学习。

循环

日常生活中充满了重复的行为。每天工作、遛狗和切胡萝卜片,这些任务都涉及到重复一系列的步骤。让我们考虑切胡萝卜片吧。如果用伪代码表达这个行为,它可能看起来是这样的:

  1. 拿菜板

  2. 拿菜刀

  3. 把胡萝卜放在菜板上

  4. 举刀

  5. 把胡萝卜往前推

  6. 切片

  7. 如果整根胡萝卜都切成片了,就退出;否则回到第 4 步

从第 4 步到第 7 步组成了一个循环。循环中的行为会一直重复,直到满足「整根胡萝卜都切成片」这个条件为止。

while

bash 可以表达一个相似的概念。假设我们想要按序显示从 1 到 5 这五个数字。bash 脚本可以这样构建:

#!/bin/bash

# while-count: display a series of numbers

count=1

while [[ "$count" -le 5 ]]; do
    echo "$count"
    count=$((count + 1))
done
echo "Finished."

执行后,该脚本显示如下:

[me@linuxbox ~]$ while-count
1
2
3
4
5
Finished.

while 命令的句法如下:

while commands; do commands; done

if 一样,while 评估命令列表的退出状态。只要退出状态是 0,它就执行循环内的命令。在上面的脚本中,创建了变量 count,并分配了初值 1while 命令评估 [[ ]] 复合命令的退出状态。只要 [[ ]] 命令返回为零的退出状态,就执行在循环中的命令。在每一次循环的末尾,重复 [[ ]] 命令。在五次迭代循环之后,count 的之已经增加到了 6[[ ]] 命令不在返回为零的退出状态,循环终止了。程序就以循环之后的下一条命令继续。

我们可以用一个 while 循环来改进上一章的 read-menu 程序。

#!/bin/bash

# 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 -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
done
echo "Program terminated."

把菜单包括进一个 while 循环,就可以让程序在每次选择之后重复显示菜单。只要 REPLY 不等于 0,则循环会继续,菜单会再次显示,以便给用户作出另一个选择的机会。每次行为之后,会执行 sleep 命令,使得程序暂停几秒钟,以便在清屏再次显示菜单之前能看到选择的结果。一旦 REPLY 等于 0,指示 quit 选项,则循环终止,程序继续执行 done 之后的语句。

跳出循环

bash 提供了两个内建命令可以用来控制循环内的程序流。break 命令立即终止一个循环,程序控制用循环体的下一条语句重新开始。continue 命令导致循环内的其余命令被忽略,程序控制以下一次迭代的循环重新开始。来看一个整合了 breakcontinuewhile-menu 程序:

#!/bin/bash

# while-menu2: 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 -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
done
echo "Program terminated."

这个版本的脚本中,我们用 true 命令提供一个退出状态给 while,设置了一个无限循环(endless loop 即永远不会自行终止的循环)。由于 true 总是以 0 为退出状态,循环永远也不会终止。这是一个令人惊奇且常见的脚本技术。因为循环不会自行终止,就需要程序员提供某种方式在适当的时间退出循环。在这个脚本中,当选择 0 时,就用 break 命令退出循环。continue 命令被包含在其它脚本选项的末尾以提供更佳的执行效率。通过使用 continue,当确定一个选项之后,脚本会忽略不需要的代码。例如,如果选择了 1 并确定之后,就没有理由去测试其它选项了。

until

until 命令很像 while,只不过不是在遇到一个非零的退出状态才退出循环,而是相反。一个 until 循环会持续循环,直到接收到一个为 0 的退出状态。在 while-count 脚本中,只要 count 变量的值小于或等于 5,就会持续循环。用 until 编写脚本,我们可以得到同样的结果。

#!/bin/bash

# until-count: display a series of numbers

count=1

until [[ "$count" -gt 5 ]]; do
    echo "$count"
    count=$((count + 1))
done
echo "Finished."

将测试表达式变更为 $count -gt 5until 会在正确的时间终止循环。决定是否使用 whileuntil 循环,通常是选择一种可以最清晰的测试来编写。

用循环读取文件

whileuntil 可以处理标准输入。这使得文件可以用 whileuntil 循环来处理。在下面的例子中,我们将显示在前几章中出现过的 distros.txt 的内容:

#!/bin/bash

# while-read: read lines from a file

while read distro version release; do
    printf "Distro: %s\tVersion: %s\tReleased: %s\n" \
        "$distro" \
        "$version" \
        "$release"
done < distros.txt

要重定向一个文件到循环中,我们在 done 之后放置了重定向符。循环将使用 read 从重定向的文件中输入字段。read 命令会在读取每行后退出,其退出状态为 0,直至到达文件末尾。到文件末尾,它会以一个非零的退出状态退出,从而终结循环。还可以将标准输入通过管道输入到循环中。

#!/bin/bash

# while-read2: read lines from a file

sort -k 1,1 -k 2n distros.txt | while read distro version release; do

    printf "Distro: %s\tVersion: %s\tReleased: %s\n" \
        "$distro" \
        "$version" \
        "$release"

done

这里,我们取得 sort 命令的输出并显示其文本流。不过,重要的是要记住,因为管道会在 subshell 中执行循环,任何在循环中创建或分配的变量都会在循环终止时丢失。

总结

介绍了循环,和之前遇到的分支、子路径和序列,我们已经学习了编程中的流程控制的主要类型。bash 还有更多的窍门,但是它们都是在这些基本概念上的进一步完善罢了。

扩展阅读

Last updated

Was this helpful?