在上一章中,我们开发了一个菜单驱动程序来生成各种系统信息。该程序工作正常,但仍然存在严重的可用性问题。它只执行一个选项,然后终止。更糟糕的是,如果做出了无效的选择,程序将以错误终止,而不会给用户重试的机会。如果我们能以某种方式构建程序,使其能够一遍又一遍地重复菜单显示和选择,直到用户选择退出程序,那就更好了。
在本章中,我们将介绍一个称为循环(looping)的编程概念,它可用于使程序的部分重复。shell提供了三个用于循环的复合命令。我们将在本章中介绍其中的两个,在后面的章节中介绍第三个。
日常生活充满了重复的活动。每天上班、遛狗和切胡萝卜都是需要重复一系列步骤的任务。让我们考虑切一根胡萝卜。如果我们用伪代码表示此活动,它可能看起来像这样:
1.拿砧板 2.拿刀 3.把胡萝卜放在砧板上 4.举起刀 5.推动胡萝卜 6.切胡萝卜 7.如果整根胡萝卜都切成薄片,那就退出;否则转到步骤4
步骤4到7形成一个循环。重复循环中的操作,直到达到“整根胡萝卜切片”的条件。
while
bash可以表达类似的想法。假设我们想按从1到5的顺序显示五个数字。bash脚本可以按如下方式构造:
xxxxxxxxxx
# while-count: display a series of numbers
count=1
while [[ "$count" -le 5 ]]; do
echo "$count"
count=$((count + 1))
done
echo "Finished."
执行时,此脚本显示以下内容:
xxxxxxxxxx
[me@linuxbox ~]$ while-count
1
2
3
4
5
Finished.
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
done
echo "Program terminated."
通过将菜单封闭在while循环中,我们可以让程序在每次选择后重复显示菜单。只要 REPLY
不等于0,循环就会继续,菜单就会再次显示,让用户有机会进行另一次选择。在每个操作结束时,都会执行 sleep
命令,因此程序将暂停几秒钟,以便在屏幕被清除和菜单重新显示之前看到选择的结果。一旦 REPLY
等于0,表示“退出”选择,循环终止,执行继续,完成以下行。
break
和 continue
bash提供了两个内置命令,可用于控制循环内的程序流。
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
done
echo "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 demo
PS3="
Your choice: "
select my_choice in First Second Third Fourth Quit; do
echo "REPLY= $REPLY my_choice= $my_choice"
[[ "$my_choice" == "Quit" ]] && break
done
首先,我们用所需的提示字符串设置 PS3
变量的内容。接下来,我们执行 select
。在这个例子中,我们有五个字符串,虽然我们使用了单个单词作为字符串,但我们可以使用任何类型的引用文本。对于我们的命令,我们只需回显 select
所做的分配。我们还测试了变量 my_choice
的内容,看看用户是否选择了“Quit”选项,如果是,我们执行一个中断以退出循环。
xxxxxxxxxx
[me@linuxbox ~]$ select-demo
1) First
2) Second
3) Third
4) Fourth
5) Quit
Your choice:
当 select
首次执行时,它会显示我们的每个字符串,前面是一个数字,后面是提示字符串。用户接下来输入代表所需选择的数字。然后, select
命令将 REPLY
变量设置为包含用户输入的任何内容以及相应的字符串(如果有的话)。
xxxxxxxxxx
Your choice: 1
REPLY= 1 my_choice= First
Your choice: 2
REPLY= 2 my_choice= Second
在这里,我们看到用户输入了“1”, echo
命令显示了 REPLY
和 my_choice
变量的值。 select
将重复显示提示字符串,直到用户输入“5”。如果用户输入了无效值, select
会将 my_choice
设置为空字符串。如果用户只是键入 Enter ,这将导致select重新开始并重新显示菜单选项列表。
xxxxxxxxxx
Your choice: 6
REPLY= 6 my_choice=
Your choice: abc
REPLY= abc my_choice=
select
循环将无限期地继续,直到遇到中断命令或用户键入 ctrl-d 表示文件结束。
xxxxxxxxxx
Your choice: 5
REPLY= 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 program
DELAY=3 # Number of seconds to display results
PS3="
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"
fi
done
echo "Program terminated."
在我们的备用脚本中,我们设置 PS3
变量,然后用四个字符串调用 select
。虽然我们随后可以通过 select
测试 str
变量集,但测试 REPLY
变量并采取相应行动更容易。在循环结束时,我们检查 str
变量的长度是否为零,表示值无效。
那么,我们应该使用哪种方法来构建菜单呢? select
命令很有趣,但除了在菜单显示中使用标准错误外,它并没有真正为我们节省多少编码工作,而且它极大地限制了菜单显示的视觉设计。
until
until
命令与 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))
done
echo "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还有更多的技巧,但它们是对这些基本概念的改进。