第28章:读取键盘输入
迄今为止我们所写的脚本都缺乏一个重要的计算机程序的特性——交互性(interactivity),即,程序和用户之间交互的能力。很多程序不需要交互性,而有些程序则受益与能接收直接从用户处而来的输入。例如,上一章中的脚本:
每次想要变更 INT
的数值,我们不得不编辑脚本。如果脚本能要求用户给一个数值,就会更有用了。本章中,我们将学习如何能将交互性加入到程序中。
read 从标准输入读取数值
read
内建命令用来读取标准输入的单独一行。该命令可以用来读取键盘输入,或者当启用重定向时,从一个文件中读取一行数据。该命令句法如下:
read [-options] [variable...]
其中,options
是一个或多个可用的选项,列在表 28-1 中,variable
是一个或多个变量的名称,用来获取输入值。如果没有提供变量名,shell 变量 REPLY
会包含一行数据。
基本上,read
从标准输入分配字段到指定变量。如果我们修改整数评估脚本以使用 read
,那看起来会是这样:
我们用带 -n
选项的 echo
(用来抑制输出时尾随的换行符)来显示一个提示,随后用 read
为变量 INT
输入一个值。运行这个脚本会导致这样:
read
可以将输入分配到多个变量,如下面这个脚本所示:
在这个脚本中,我们分配并显示了五个值。需要注意当给到 read
不同数目的值的时候,它的行为是怎样的,如下:
如果 read
收到的值少于期望的数目,则多余的变量为空值,当有额外数目的输入,则会导致最后的一个变量包含了所有额外的输入。
如果在 read
命令之后没有列出变量,就会将输入分配给一个名为 REPLY
的 shell 变量。
运行后效果如下:
选项
read
支持的选项见表 28-1 所述。
表 28-1:read
选项
选项
描述
-a
array
将输入分配到数组 array,索引从 0
开始。我们将在第 35 章学习数组。
-d
delimiter
delimiter 字符串中的第一个字符用来指示输入的终结,而非用换行符来指示。
-e
使用读行来处理输入。这允许以与命令行相同的方式进行输入编辑。
-i
string
如果用户简单地按了 Enter
键后,使用 string 作为默认的回复。需要 -e
选项。
-n
num
读取输入的 num 个字符,而非整行输入。
-p
prompt
使用字符串 prompt 字符串为输入显示一个提示。
-r
原始模式。不将反斜杠字符解释为转义。
-s
静默模式。当用户输入时,不回显字符到显示器。当输入密码和其它机密信息时,该选项很有用。
-t
seconds
超时。在 seconds 秒之后终止输入。如果输入超时,read
会返回一个非零的退出状态。
-u
fd
从文件描述符 fd 输入,而非标准输入。
使用各种选项,我们可以用 read
做出有趣的事情。例如,用 -p
选项,可以提供一个提示字符串。
用 -t
和 -s
选项,我们可以写一个读取「秘密」输入和一个如果在规定时间内没有完成输入就会超时的脚本。
该脚本提示用户需要用十秒钟的时间输入一个密码。如果在指定时间内没有完成输入,脚本就会因为错误而退出。因为包含了 -s
选项,密码字符在被键入时不会回显在屏幕上。
同时使用 -e
和 -i
选项,可以给用户提供一个默认的响应。
在脚本中,我们提示用户输入一个用户名,并使用了环境变量 USER
以提供一个默认值。当运行脚本时,它显示了一个默认字符串,并且,如果用户简单地按下了 Enter
键,read
将会把默认字符串分配给 REPLY
变量。
IFS
通常,shell 对输入执行分词以提供给 read
。我们已经看到,这意味着被一个或多个空格分隔的多个单词在输入行上成为了单独的项目,并且被 read
分配给了单独的变量。这个行为是由一个名为 IFS
(Internal Field Separator 内部字段分隔符)的 shell 变量所配置的。默认的 IFS
的值,包含了一个空格、一个制表符和一个换行符,其中任一字符都会将项目之间彼此分隔开。
我们可以调整 IFS
的值以控制字段分隔,以便输入到 read
。例如,/etc/passwd
文件包含了使用冒号作为字段分隔符的多行数据。将 IFS
的值改成单个冒号,可以用 read
输入 /etc/passwd
的内容,并成功地将字段分隔为不同的变量。这里有一个脚本,就是做这个工作的:
该脚本提示用户输入系统上一个账户的用户名,然后显示该用户记录在 /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"
这一行有三个部分组成:一个变量分配、带一个变量名列表作为参数的 read
命令、和一个新奇的重定向操作符。我们先来看变量分配。
shell 允许在一个命令前立即进行一个或多个变量分配。这些分配为随后的命令变更了系统环境。分配的效用仅仅是临时变更了该命令持续期间的系统环境。在本例中,IFS
的值被变更为一个冒号。作为替代,我们还可以这样编写:
我们将 IFS
的值保存到一个新变量中,执行 read
命令,然后恢复 IFS
的原始值。很明显,做着相同的事情,将变量分配放置在命令之前是一个更简明的方式。
<<<
操作符只是了一个 here 字符串(here string)。here 字符串就像 here 文档,只不过更短,由单个字符串组成。在上面这个例子中,在 /etc/passwd
中的一行数据被投送到标准输入的 read
命令中。我们可能想知道为何选择这种斜的方式,而不是这样:
echo "$file_info" | IFS=":" read user pw uid gid name home shell
好吧,有一个理由……
不能管道传输
read
read
命令自然地从标准输入取得输入,但是你不能这么做:
echo "foo" | read
我们期待这样能工作,但是不能。命令会显示执行成功,但是
REPLY
变量将总是空的。为什么呢?这个解释与外壳处理管道的方式有关。 在
bash
(和 一些其它的 shell 如sh
)里,管道会创建 subshells。这是用来执行管道中的命令的 shell 及其环境的副本。在刚才的例子中,read
是在一个 subshell 中执行的。在类 Unix 系统中的 subshells 会创建环境副本,以供进程在执行中使用。当进程完成时,即摧毁环境副本。这意味着一个 subshell 永远不能改变其父进程的环境。
read
分配变量,变量随之成为环境的一部分。在前面这个例子中,read
分配了值foo
到其 subshell 中的变量REPLY
,但是当命令退出时, subshell 及其环境被摧毁了,所以分配的效果也就消失了。使用 here 字符串是解决该问题的一个方法。另一个方法会在第 36 章中讨论。
验证输入
伴随着键盘输入的新能力而来的一个额外的编程挑战是验证输入。编写良好的程序和垃圾代码之间的差异, 通常在于程序处理意外事件的能力。通常,意外输入会以错误的输入形式出现。在上一章中,我们已经在评估程序中完成了一部分,我们检查了整数的值,筛选出空值和非数字字符。程序在每次接收到输入时执行这种检查很重要,它防止了非法数据。这对于分享给多个用户的程序来说,尤其重要。如果一款程序仅使用一次或仅仅由作者执行一些特殊的任务,那么出于经济上的考虑忽略这些保护措施是可以被原谅的。甚至于如果程序执行的是如删除文件之类的危险任务,那么,为了以防万一,加入数据验证是明智的。
这里有一个示例程序,验证各种输入:
这个脚本提示用户输入一个项目。 随后对该项目进行分析以确定其内容。 如我们所见,脚本使用了许多迄今已经学过的概念,包括了 shell 函数、[[ ]]
、(( ))
、控制操作符、if
和一堆正则表达式。
菜单
互动性的一个常见类型是菜单驱动(menu-driven)。在菜单驱动程序中,展示给用户一个选择列表并要求其选择一个。例如,我们可以想象一个程序如下:
使用从写 sys_info_page
程序中所学的知识,我们可以构建一个菜单驱动程序,来执行上面这个菜单的任务:
脚本在逻辑上分成两个部分。第一部分显示了菜单和从用户处输入的回应。第二部分识别回应并执行所选择的行为。注意脚本中用到的 exit
。用在这里是防止脚本在某个行为已经执行之后再去执行不必要的代码。在一个程序中出现多个 exit
,通常是一个坏主意(这使得程序在逻辑上很难理解),但是在这个脚本中,是有效的。
总结
本章中,我们迈出了走向互动性的第一步,允许用户通过键盘输入数据到程序中。使用目前学到的技术可以写出许多有用的程序,如指定的计算程序和供神秘的命令行工具的易用的前端程序。下一章中我们将在菜单驱动程序概念的基础上,使其更完善。
额外学分
认真研究本章中的程序,并全面了解它们的逻辑结构方式,这很重要,因为以后的程序将越来越复杂。作为练习,使用 test
命令而非 [[ ]]
复合命令重写本章中的程序。提示:使用 grep
以评估正则表达式并检测退出状态。这会是一个好的实践。
扩展阅读
Last updated
Was this helpful?