到目前为止,我们编写的脚本缺乏大多数计算机程序中常见的功能——交互性(interactivity),即程序与用户交互的能力。虽然许多程序不需要是交互式的,但有些程序可以直接接受用户的输入。以上一章中的脚本为例:
xxxxxxxxxx
# test-integer2: evaluate the value of an integer.
INT=-5
if [[ "$INT" =~ ^-?[0-9]+$ ]]; then
if [ "$INT" -eq 0 ]; then
echo "INT is zero."
else
if [ "$INT" -lt 0 ]; then
echo "INT is negative."
else
echo "INT is positive."
fi
if [ $((INT % 2)) -eq 0 ]; then
echo "INT is even."
else
echo "INT is odd."
fi
fi
else
echo "INT is not an integer." >&2
exit 1
fi
每次我们想更改 INT
的值时,我们都必须编辑脚本。如果脚本可以向用户请求值,那将更有用。在本章中,我们将开始研究如何为我们的程序添加交互性。
read
—— 从标准输入读取值read
内置命令用于读取一行标准输入。此命令可用于读取键盘输入,或者在采用重定向时,从文件中读取一行数据。该命令具有以下语法:
read [ -options ] [ variable...]
其中 options 是下表中稍后列出的一个或多个可用选项, variable 是用于保存输入值的一个或者多个变量的名称。如果没有提供变量名,则shell变量 REPLY
包含数据行。
基本上, read
将标准输入中的字段分配给指定的变量。如果我们将整数求值脚本修改为使用 read
,它可能看起来像这样:
xxxxxxxxxx
# test-integer2: evaluate the value of an integer.
echo -n "Please enter an integer ->"
read int
if [[ "$int" =~ ^-?[0-9]+$ ]]; then
if [ "$int" -eq 0 ]; then
echo "INT is zero."
else
if [ "$int" -lt 0 ]; then
echo "int is negative." #负数
else
echo "int is positive." #正数
fi
if [ $((int % 2)) -eq 0 ]; then
echo "int is even." #偶数
else
echo "int is odd." #奇数
fi
fi
else
echo "int is not an integer." >&2
exit 1
fi
我们使用带有 -n
选项的 echo
(在输出中抑制尾随换行符)来显示提示,然后我们使用 read
为变量int输入一个值。运行此脚本的结果如下:
xxxxxxxxxx
[me@linuxbox ~]$ read-integer
Please enter an integer -> 5
5 is positive.
5 is odd.
read
可以将输入分配给多个变量,如下脚本所示:
xxxxxxxxxx
# read-multiple: read multiple values from keyboard
echo -n "Enter one or more values > "
read var1 var2 var3 var4 var5
echo "var1 = '$var1'"
echo "var2 = '$var2'"
echo "var3 = '$var3'"
echo "var4 = '$var4'"
echo "var5 = '$var5'"
在这个脚本中,我们最多分配和显示五个值。请注意,当给定不同数量的值时, read
的行为如下:
xxxxxxxxxx
[me@linuxbox ~]$ read-multiple
Enter one or more values > a b c d e
var1 = 'a'
var2 = 'b'
var3 = 'c'
var4 = 'd'
var5 = 'e'
[me@linuxbox ~]$ read-multiple
Enter one or more values > a
var1 = 'a'
var2 = ''
var3 = ''
var4 = ''
var5 = ''
[me@linuxbox ~]$ read-multiple
Enter one or more values > a b c d e f g
var1 = 'a'
var2 = 'b'
var3 = 'c'
var4 = 'd'
var5 = 'e f g'
如果 read
收到的数字小于预期的数字,则额外的变量为空,而过多的输入会导致最终变量包含所有额外的输入。
如果 read
命令后没有列出变量,则将为所有输入分配一个shell变量 REPLY
。
xxxxxxxxxx
# read-single: read multiple values into default variable
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"
运行此脚本会导致以下结果:
xxxxxxxxxx
[me@linuxbox ~]$ read-single
Enter one or more values > a b c d
REPLY = 'a b c d'
read
支持下表中描述的选项。
选项 | 描述 |
---|---|
-a array | 将输入分配给数组,从索引零开始。 我们将在【第35章】介绍数组。 |
-d delimiter | 字符串 delimiter 中的第一个字符用于指示输入的结束,而不是换行符。 |
-e | 使用 Readline 处理输入。这允许以与命令行相同的方式进行输入编辑。 |
-i string | 如果用户只是按 Enter 键,则使用 string 作为默认回复。 需要 -e 选项。 |
-n num | 读取输入的 num 个字符,而不是整行。 |
-p promt | 使用字符串 promt 显示输入提示。 |
-r | 原始模式。不要将反斜杠字符解释为转义符。 为了安全起见,建议使用此选项。 例如,在输入DOS路径名时,我们希望反斜杠被视为文字字符。 |
-s | 静默(silent)模式。 不要在键入字符时将其回显到显示器上。 这在输入密码和其他机密信息时很有用。 |
-t seconds | 超时。 seconds 秒后终止输入。如果输入超时,则 read 返回非零退出状态。 |
-u fd | 使用来自文件描述符(file descriptor) fd 的输入,而不是标准输入。 |
使用各种选项,我们可以用 read
做有趣的事情。例如,使用 -p
选项,我们可以提供一个提示字符串。
xxxxxxxxxx
# read-single: read multiple values into default variable
read -r -p "Enter one or more values > "
echo "REPLY = '$REPLY'"
使用 -t
和 -s
选项,我们可以编写一个脚本,读取“secret”输入,如果输入未在指定时间内完成,则超时。
xxxxxxxxxx
# read-secret: input a secret passphrase
if read -r -t 10 -sp "Enter secret passphrase > " secret_pass; then
echo -e "\nSecret passphrase = '$secret_pass'"
else
echo -e "\nInput timed out" >&2
exit 1
fi
该脚本提示用户输入密码,并等待10秒输入。如果条目未在指定时间内完成,则脚本将退出并显示错误。由于包含了 -s
选项,密码的字符在键入时不会回显到显示器上。
可以同时使用 -e
和 -i
选项为用户提供默认响应。
xxxxxxxxxx
# read-default: supply a default value if user presses Enter key.
read -e -p "What is your user name? " -i $USER
echo "You answered: '$REPLY'"
在这个脚本中,我们提示用户输入用户名,并使用环境变量 USER
提供默认值。当脚本运行时,它会显示默认字符串,如果用户只需按 Enter 键, read
就会将默认字符串分配给 REPLY
变量。
xxxxxxxxxx
[me@linuxbox ~]$ read-default
What is your user name? me
You answered: 'me'
通常,shell会对提供给读取的输入执行分词。正如我们所看到的,这意味着由一个或多个空格分隔的多个单词在输入行上成为单独的项目,并通过读取分配给单独的变量。此行为由名为 IFS
(Internal Field Separator,内部字段分隔符)的shell变量配置。IFS的默认值包含一个空格、一个制表符和一个换行符,每个换行符都将项目彼此分隔。
我们可以调整 IFS
的值来控制要读取的字段输入的分离。例如, /etc/passwd 文件包含使用冒号字符作为字段分隔符的数据行。通过将IFS的值更改为单个冒号,我们可以使用 read
输入 /etc/passwd 的内容,并成功地将字段分隔到不同的变量中。这里我们有一个脚本可以做到这一点:
xxxxxxxxxx
# read-ifs: read fields from a file
FILE=/etc/passwd
read -r -p "Enter a username > " user_name
file_info="$(grep "^$user_name:" $FILE)"
if [ -n "$file_info" ]; then
IFS=":" read -r user pw uid gid name home shell <<< "$file_info"
echo "User = '$user'"
echo "UID = '$uid'"
echo "GID = '$gid'"
echo "Full Name = '$name'"
echo "Home Dir. = '$home'"
echo "Shell = '$shell'"
else
echo "No such user '$user_name'" >&2
exit 1
fi
此脚本提示用户输入系统上帐户的用户名,然后显示 /etc/passwd 文件中用户记录中的不同字段。脚本包含两行有趣的内容。第一种情况如下:
file_info=$(grep "^$user_name:" $FILE)
此行将 grep
命令的结果分配给变量 file_info
。 grep
使用的正则表达式确保用户名只与 /etc/passwd 文件中的一行匹配。
第二条有趣的行是:
IFS=":" read user pw uid gid name home shell <<< "$file_info"
该行由三部分组成:变量赋值、以变量名列表作为参数的读取命令和一个奇怪的新重定向运算符。我们先看变量赋值。
shell允许在命令之前立即进行一个或多个变量赋值。这些任务会改变后续命令的环境。分配的效果是暂时的,仅在命令执行期间更改环境。在我们的例子中,IFS的值被更改为冒号字符。或者,我们可以这样编码:
OLD_IFS="$IFS" IFS=":" read user pw uid gid name home shell <<< "$file_info" IFS="$OLD_IFS"
在这里,我们存储IFS的值,分配一个新值,执行读取命令,然后将IFS还原为其原始值。显然,将变量赋值放在命令前面是做同样事情的一种更简洁的方法。
<<<
运算符表示here字符串(here string)。here字符串类似于here文档,只是更短,由单个字符串组成。在我们的示例中, /etc/passwd 文件中的数据行被馈送(fed)到 read
命令的标准输入。我们可能想知道为什么选择了这种相当倾斜的方法,而不是这样:
echo "$file_info" | IFS=":" read user pw uid gid name home shell
嗯,这是有原因的...
read
虽然 read
命令通常从标准输入中获取输入,但您不能这样做:
echo "foo" | read
我们希望这能奏效,但事实并非如此。命令将显示为成功,但 REPLY
变量将始终为空。这是为什么?
这个解释与shell处理管道的方式有关。在bash(以及sh等其他shell)中,管道创建子shell。这些是用于在管道中执行命令的shell及其环境的副本。在前面的示例中, read
是在子shell中执行的。
类Unix系统中的子shell创建了环境的副本,供进程在执行时使用。当进程完成时,环境的副本将被销毁。这意味着子shell永远不能改变其父进程的环境。 read
分配变量,然后这些变量成为环境的一部分。在前面的示例中, read
将值 foo
赋值给子shell环境中的变量 REPLY
,但当命令退出时,子shell及其环境将被破坏,赋值的效果也将丢失。
在这里使用字符串是解决这种行为的一种方法。 【第36章】讨论了另一种方法。
随着我们拥有键盘输入的新能力,随之而来的是一个额外的编程挑战,即验证输入。
通常,写得好的程序和写得不好的程序之间的区别在于程序处理意外情况的能力。
意外经常以错误输入的形式出现。我们在上一章的评估程序中做了一些这样的工作,在那里我们检查了整数的值,并筛选出空值和非数字字符。每当程序接收到输入时,执行这些编程检查以防止无效数据是很重要的。这对于由多个用户共享的程序尤其重要。如果一个程序只能由作者使用一次来执行某些特殊任务,那么为了经济利益而省略这些保障措施可能是可以原谅的。即便如此,如果程序执行删除文件等危险任务,为了以防万一,最好包括数据验证。
这里我们有一个验证各种输入的示例程序:
xxxxxxxxxx
# read-validate: validate input
invalid_input () {
echo "Invalid input '$REPLY'" >&2
exit 1
}
read -r -p "Enter a single item > "
# input is empty (invalid)
[[ -z "$REPLY" ]] && invalid_input
# input is multiple items (invalid)
(( "$(echo "$REPLY" | wc -w)" > 1 )) && invalid_input
# is input a valid filename?
if [[ "$REPLY" =~ ^[-[:alnum:]\._]+$ ]]; then
echo "'$REPLY' is a valid filename."
if [[ -e "$REPLY" ]]; then
echo "And file '$REPLY' exists."
else
echo "However, file '$REPLY' does not exist."
fi
# is input a floating point number?
if [[ "$REPLY" =~ ^-?[[:digit:]]*\.[[:digit:]]+$ ]]; then
echo "'$REPLY' is a floating point number."
else
echo "'$REPLY' is not a floating point number."
fi
# is input an integer?
if [[ "$REPLY" =~ ^-?[[:digit:]]+$ ]]; then
echo "'$REPLY' is an integer."
else
echo "'$REPLY' is not an integer."
fi
else
echo "The string '$REPLY' is not a valid filename."
fi
此脚本提示用户输入项目。随后对该项目进行分析以确定其内容。正如我们所看到的,该脚本使用了我们迄今为止所涵盖的许多概念,包括shell函数、 [[ ]]
、 (( ))
、控制运算符 &&
和 if
,以及适量的正则表达式。
一种常见的交互类型称为菜单驱动(menu-driven)。在菜单驱动的程序中,用户会看到一个选项列表,并被要求选择一个。例如,我们可以想象一个程序,它呈现了以下内容:
xxxxxxxxxx
Please Select:
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
Enter selection [0-3] >
使用我们从编写 sys_info_page
程序中学到的知识,我们可以构建一个菜单驱动的程序来执行上一个菜单上的任务:
xxxxxxxxxx
# read-menu: a menu driven system information program
clear
echo "
Please Select:
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
"
read -r -p "Enter selection [0-3] > "
if [[ "$REPLY" =~ ^[0-3]$ ]]; then
if [[ "$REPLY" == 0 ]]; then
echo "Program terminated."
exit
fi
if [[ "$REPLY" == 1 ]]; then
echo "Hostname: $HOSTNAME"
uptime
exit
fi
if [[ "$REPLY" == 2 ]]; then
df -h
exit
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
exit
fi
else
echo "Invalid entry." >&2
exit 1
fi
这个脚本在逻辑上分为两部分。第一部分显示菜单并输入用户的响应。第二部分识别响应并执行所选操作。请注意此脚本中 exit
命令的使用。这里使用它来防止脚本在执行操作后执行不必要的代码。程序中存在多个退出点通常是一个坏主意(这会使程序逻辑更难理解),但它在这个脚本中是有效的。
在本章中,我们迈出了实现交互性的第一步,允许用户通过键盘将数据输入到我们的程序中。使用到目前为止提出的技术,可以编写许多有用的程序,例如专门的计算程序和用于晦涩命令行工具的易于使用的前端。在下一章中,我们将以菜单驱动程序的概念为基础,使其变得更好。
仔细研究本章中的程序并完全理解它们的逻辑结构非常重要,因为即将到来的程序将变得越来越复杂。作为练习,使用 test
命令而不是 [[ ]]
复合命令重写本章中的程序。提示:使用 grep
计算正则表达式并计算退出状态。这将是一个很好的做法。