## # 读取键盘输入

The scripts we have written so far lack a feature common in most computer programs — interactivity. That is, the ability of the program to interact with the user. While many programs don’t need to be interactive, some programs benefit from being able to accept input directly from the user. Take, for example, this script from the previous chapter:

#!/bin/bash
# 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  Each time we want to change the value of INT, we have to edit the script. It would be much more useful if the script could ask the user for a value. In this chapter, we will begin to look at how we can add interactivity to our programs. 每次我们想要改变 INT 数值的时候，我们必须编辑这个脚本。如果脚本能请求用户输入数值，那 么它会更加有用处。在这个脚本中，我们将看一下我们怎样给程序增加交互性功能。 ### # read - 从标准输入读取数值 The read builtin command is used to read a single line of standard input. This command can be used to read keyboard input or, when redirection is employed, a line of data from a file. The command has the following syntax: 这个 read 内部命令被用来从标准输入读取单行数据。这个命令可以用来读取键盘输入，当使用 重定向的时候，读取文件中的一行数据。这个命令有以下语法形式： read [-options] [variable...]  where options is one or more of the available options listed below and variable is the name of one or more variables used to hold the input value. If no variable name is supplied, the shell variable REPLY contains the line of data. 这里的 options 是下面列出的可用选项中的一个或多个，且 variable 是用来存储输入数值的一个或多个变量名。 如果没有提供变量名，shell 变量 REPLY 会包含数据行。 Basically, read assigns fields from standard input to the specified variables. If we modify our integer evaluation script to use read, it might look like this: 基本上，read 会把来自标准输入的字段赋值给具体的变量。如果我们修改我们的整数求值脚本，让其使用 read ，它可能看起来像这样： #!/bin/bash # read-integer: 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 "Input value is not an integer." >&2 exit 1 fi  We use echo with the -n option (which suppresses the trailing newline on output) to display a prompt, then use read to input a value for the variable int. Running this script results in this: 我们使用带有 -n 选项（其会删除输出结果末尾的换行符）的 echo 命令，来显示提示信息， 然后使用 read 来读入变量 int 的数值。运行这个脚本得到以下输出： [me@linuxbox ~]$ read-integer
Please enter an integer -> 5
5 is positive.
5 is odd.


#!/bin/bash
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'"  In this script, we assign and display up to five values. Notice how read behaves when given different numbers of values: 在这个脚本中，我们给五个变量赋值并显示其结果。注意当给定不同个数的数值后，read 怎样操作： [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'


If read receives fewer than the expected number, the extra variables are empty, while an excessive amount of input results in the final variable containing all of the extra input. If no variables are listed after the read command, a shell variable, REPLY, will be assigned all the input:

#!/bin/bash
echo -n "Enter one or more values > "
echo "REPLY = '$REPLY'"  Running this script results in this: 这个脚本的输出结果是： [me@linuxbox ~]$ read-single
Enter one or more values > a b c d
REPLY = 'a b c d'


#### # 选项

read supports the following options:

Option Description
-a array Assign the input to array, starting with index zero. We will cover arrays in Chapter 36.
-d delimiter The first character in the string delimiter is used to indicate end of input, rather than a newline character.
-e Use Readline to handle input. This permits input editing in the same manner as the command line.
-n num Read num characters of input, rather than an entire line.
-p prompt Display a prompt for input using the string prompt.
-r Raw mode. Do not interpret backslash characters as escapes.
-s Silent mode. Do not echo characters to the display as they are typed. This is useful when inputting passwords and other confidential information.
-t seconds Timeout. Terminate input after seconds. read returns a non-zero exit status if an input times out.
-u fd Use input from file descriptor fd, rather than standard input.

-a array 把输入赋值到数组 array 中，从索引号零开始。我们 将在第36章中讨论数组问题。
-d delimiter 用字符串 delimiter 中的第一个字符指示输入结束，而不是一个换行符。
-n num 读取 num 个输入字符，而不是整行。
-p prompt 为输入显示提示信息，使用字符串 prompt。
-r Raw mode. 不把反斜杠字符解释为转义字符。
-s Silent mode. 不会在屏幕上显示输入的字符。当输入密码和其它确认信息的时候，这会很有帮助。
-u fd 使用文件描述符 fd 中的输入，而不是标准输入。

Using the various options, we can do interesting things with read. For example, with the -p option, we can provide a prompt string:

#!/bin/bash
read -p "Enter one or more values > "
echo "REPLY = '$REPLY'"  With the -t and -s options we can write a script that reads “secret” input and times out if the input is not completed in a specified time: 通过 -t 和 -s 选项，我们可以编写一个这样的脚本，读取“秘密”输入，并且如果在特定的时间内 输入没有完成，就终止输入。 #!/bin/bash # read-secret: input a secret pass phrase if read -t 10 -sp "Enter secret pass phrase > " secret_pass; then echo "\nSecret pass phrase = '$secret_pass'"
else
echo "\nInput timed out" >&2
exit 1
fi


The script prompts the user for a secret pass phrase and waits ten seconds for input. If the entry is not completed within the specified time, the script exits with an error. Since the -s option is included, the characters of the pass phrase are not echoed to the display as they are typed.

### # IFS

Normally, the shell performs word splitting on the input provided to read. As we have seen, this means that multiple words separated by one or more spaces become separate items on the input line, and are assigned to separate variables by read. This behavior is configured by a shell variable named IFS (for Internal Field Separator). The default value of IFS contains a space, a tab, and a newline character, each of which will separate items from one another.

We can adjust the value of IFS to control the separation of fields input to read. For example, the /etc/passwd file contains lines of data that use the colon character as a field separator. By changing the value of IFS to a single colon, we can use read to input the contents of /etc/passwd and successfully separate fields into different variables. Here we have a script that does just that:

#!/bin/bash
FILE=/etc/passwd
read -p "Enter a user name > " user_name
file_info=$(grep "^$user_name:" $FILE) if [ -n "$file_info" ]; then
IFS=":" read 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


This script prompts the user to enter the user name of an account on the system, then displays the different fields found in the user’s record in the /etc/passwd file. The script contains two interesting lines. The first is:

file_info=$(grep "^$user_name:" $FILE)  This line assigns the results of a grep command to the variable file_info. The regular expression used by grep assures that the user name will only match a single line in the /etc/passwd file. 这一行把 grep 命令的输入结果赋值给变量 file_info。grep 命令使用的正则表达式 确保用户名只会在 /etc/passwd 文件中匹配一行。 The second interesting line is this one: 第二个有意思的一行是： IFS=":" read user pw uid gid name home shell <<< "$file_info"


The line consists of three parts: a variable assignment, a read command with a list of variable names as arguments, and a strange new redirection operator. We’ll look at the variable assignment first.

The shell allows one or more variable assignments to take place immediately before a command. These assignments alter the environment for the command that follows. The effect of the assignment is temporary; only changing the environment for the duration of the command. In our case, the value of IFS is changed to a colon character. Alternately, we could have coded it this way:

Shell 允许在一个命令之前给一个或多个变量赋值。这些赋值会暂时改变之后的命令的环境变量。 在这种情况下，IFS 的值被改成一个冒号。等效的，我们也可以这样写：

OLD_IFS="$IFS" IFS=":" read user pw uid gid name home shell <<< "$file_info"
IFS="$OLD_IFS"  where we store the value of IFS, assign a new value, perform the read command, then restore IFS to its original value. Clearly, placing the variable assignment in front of the command is a more concise way of doing the same thing. 我们先存储 IFS 的值，然后赋给一个新值，再执行 read 命令，最后把 IFS 恢复原值。显然，完成相同的任务， 在命令之前放置变量名赋值是一种更简明的方式。 The <<< operator indicates a here string. A here string is like a here document, only shorter, consisting of a single string. In our example, the line of data from the /etc/passwd file is fed to the standard input of the read command. We might wonder why this rather oblique method was chosen rather than: 这个 <<< 操作符指示一个 here 字符串。一个 here 字符串就像一个 here 文档，只是比较简短，由 单个字符串组成。在这个例子中，来自 /etc/passwd 文件的数据发送给 read 命令的标准输入。 我们可能想知道为什么选择这种相当晦涩的方法而不是： echo "$file_info" | IFS=":" read user pw uid gid name home shell


While the read command normally takes input from standard input, you cannot do this:

We would expect this to work, but it does not. The command will appear to succeed but the REPLY variable will always be empty. Why is this?

The explanation has to do with the way the shell handles pipelines. In bash (and other shells such as sh), pipelines create subshells. These are copies of the shell and its environment which are used to execute the command in the pipeline. In our example above, read is executed in a subshell.

Subshells in Unix-like systems create copies of the environment for the processes to use while they execute. When the processes finishes the copy of the environment is destroyed. This means that a subshell can never alter the environment of its parent process. read assigns variables, which then become part of the environment. In the example above, read assigns the value “foo” to the variable REPLY in its subshell’s environment, but when the command exits, the subshell and its environment are destroyed, and the effect of the assignment is lost.

Using here strings is one way to work around this behavior. Another method is discussed in Chapter 37.

### # 校正输入

With our new ability to have keyboard input comes an additional programming challenge, validating input. Very often the difference between a well-written program and a poorly written one is in the program’s ability to deal with the unexpected. Frequently, the unexpected appears in the form of bad input. We’ve done a little of this with our evaluation programs in the previous chapter, where we checked the value of integers and screened out empty values and non-numeric characters. It is important to perform these kinds of programming checks every time a program receives input, to guard against invalid data. This is especially important for programs that are shared by multiple users. Omitting these safeguards in the interests of economy might be excused if a program is to be used once and only by the author to perform some special task. Even then, if the program performs dangerous tasks such as deleting files, it would be wise to include data validation, just in case.

Here we have an example program that validates various kinds of input:

#!/bin/bash
invalid_input () {
echo "Invalid input '$REPLY'" >&2 exit 1 } read -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  This script prompts the user to enter an item. The item is subsequently analyzed to determine its contents. As we can see, the script makes use of many of the concepts that we have covered thus far, including shell functions, [[ ]], (( )), the control operator &&, and if, as well as a healthy dose of regular expressions. 这个脚本提示用户输入一个数字。随后，分析这个数字来决定它的内容。正如我们所看到的，这个脚本 使用了许多我们已经讨论过的概念，包括 shell 函数，[[ ]](( ))，控制操作符 &&，以及 if 和 一些正则表达式。 ### # 菜单 A common type of interactivity is called menu-driven. In menu-driven programs, the user is presented with a list of choices and is asked to choose one. For example, we could imagine a program that presented the following: 一种常见的交互类型称为菜单驱动。在菜单驱动程序中，呈现给用户一系列选择，并要求用户选择一项。 例如，我们可以想象一个展示以下信息的程序： Please Select: 1.Display System Information 2.Display Disk Space 3.Display Home Space Utilization 0.Quit Enter selection [0-3] >  Using what we learned from writing our sys_info_page program, we can construct a menu-driven program to perform the tasks on the above menu: 使用我们从编写 sys_info_page 程序中所学到的知识，我们能够构建一个菜单驱动程序来执行 上述菜单中的任务： #!/bin/bash # 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 -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


This script is logically divided into two parts. The first part displays the menu and inputs the response from the user. The second part identifies the response and carries out the selected action. Notice the use of the exit command in this script. It is used here to prevent the script from executing unnecessary code after an action has been carried out. The presence of multiple ｀exit｀ points in a program is generally a bad idea (it makes program logic harder to understand), but it works in this script.

### # 总结归纳

In this chapter, we took our first steps toward interactivity; allowing users to input data into our programs via the keyboard. Using the techniques presented thus far, it is possible to write many useful programs, such as specialized calculation programs and easy-to-use front ends for arcane command line tools. In the next chapter, we will build on the menu-driven program concept to make it even better.

#### # 友情提示

It is important to study the programs in this chapter carefully and have a complete understanding of the way they are logically structured, as the programs to come will be increasingly complex. As an exercise, rewrite the programs in this chapter using the test command rather than the [[ ]] compound command. Hint: use grep to evaluate the regular expressions and evaluate its exit status. This will be good practice.