读取键盘输入

    每次我们想要改变 INT 数值的时候,我们必须编辑这个脚本。如果脚本能请求用户输入数值,那么它会更加有用处。在这个脚本中,我们将看一下我们怎样给程序增加交互性功能。

    这个 read 内部命令被用来从标准输入读取单行数据。这个命令可以用来读取键盘输入,当使用重定向的时候,读取文件中的一行数据。这个命令有以下语法形式:

    这里的 options 是下面列出的可用选项中的一个或多个,且 variable 是用来存储输入数值的一个或多个变量名。如果没有提供变量名,shell 变量 REPLY 会包含数据行。

    基本上,read 会把来自标准输入的字段赋值给具体的变量。如果我们修改我们的整数求值脚本,让其使用 read ,它可能看起来像这样:

    1. #!/bin/bash
    2. # read-integer: evaluate the value of an integer.
    3. echo -n "Please enter an integer -> "
    4. read int
    5. if [[ "$int" =~ ^-?[0-9]+$ ]]; then
    6. if [ $int -eq 0 ]; then
    7. echo "$int is zero."
    8. else
    9. if [ $int -lt 0 ]; then
    10. echo "$int is negative."
    11. else
    12. echo "$int is positive."
    13. fi
    14. if [ $((int % 2)) -eq 0 ]; then
    15. echo "$int is even."
    16. else
    17. echo "$int is odd."
    18. fi
    19. fi
    20. else
    21. echo "Input value is not an integer." >&2
    22. exit 1
    23. fi

    我们使用带有 -n 选项(其会删除输出结果末尾的换行符)的 echo 命令,来显示提示信息,然后使用 read 来读入变量 int 的数值。运行这个脚本得到以下输出:

    1. [me@linuxbox ~]$ read-integer
    2. Please enter an integer -> 5
    3. 5 is positive.
    4. 5 is odd.

    read 可以给多个变量赋值,正如下面脚本中所示:

    1. #!/bin/bash
    2. # read-multiple: read multiple values from keyboard
    3. echo -n "Enter one or more values > "
    4. read var1 var2 var3 var4 var5
    5. echo "var1 = '$var1'"
    6. echo "var2 = '$var2'"
    7. echo "var3 = '$var3'"
    8. echo "var4 = '$var4'"
    9. echo "var5 = '$var5'"

    在这个脚本中,我们给五个变量赋值并显示其结果。注意当给定不同个数的数值后,read 怎样操作:

    1. [me@linuxbox ~]$ read-multiple
    2. Enter one or more values > a b c d e
    3. var1 = 'a'
    4. var2 = 'b'
    5. var3 = 'c'
    6. var4 = 'd'
    7. var5 = 'e'
    8. [me@linuxbox ~]$ read-multiple
    9. Enter one or more values > a
    10. var1 = 'a'
    11. var2 = ''
    12. var3 = ''
    13. var4 = ''
    14. var5 = ''
    15. [me@linuxbox ~]$ read-multiple
    16. Enter one or more values > a b c d e f g
    17. var2 = 'b'
    18. var3 = 'c'
    19. var4 = 'd'
    20. var5 = 'e f g'

    如果 read 命令接受到变量值数目少于期望的数字,那么额外的变量值为空,而多余的输入数据则会被包含到最后一个变量中。如果 read 命令之后没有列出变量名,则一个 shell 变量,REPLY,将会包含所有的输入:

    这个脚本的输出结果是:

    1. [me@linuxbox ~]$ read-single
    2. REPLY = 'a b c d'

    29.1.1 选项

    read 支持以下选项:

    使用各种各样的选项,我们能用 read 完成有趣的事情。例如,通过-p 选项,我们能够提供提示信息:

    1. #!/bin/bash
    2. # read-single: read multiple values into default variable
    3. read -p "Enter one or more values > "
    4. echo "REPLY = '$REPLY'"

    通过 -t 和 -s 选项,我们可以编写一个这样的脚本,读取“秘密”输入,并且如果在特定的时间内输入没有完成,就终止输入。

    1. #!/bin/bash
    2. # read-secret: input a secret pass phrase
    3. if read -t 10 -sp "Enter secret pass phrase > " secret_pass; then
    4. echo "\nSecret pass phrase = '$secret_pass'"
    5. else
    6. echo "\nInput timed out" >&2
    7. exit 1
    8. fi

    29.1.2 使用 IFS 间隔输入字符

    通常,shell 对提供给 read 的输入按照单词进行分离。正如我们所见到的,这意味着多个由一个或几个空格分离开的单词在输入行中变成独立的个体,并被 read 赋值给单独的变量。这种行为由 shell 变量 IFS(内部字符分隔符)配置。IFS 的默认值包含一个空格,一个 tab,和一个换行符,每一个都会把字段分割开。

    我们可以调整 IFS 的值来控制输入字段的分离。例如,这个 /etc/passwd 文件包含的数据行使用冒号作为字段分隔符。通过把 IFS 的值更改为单个冒号,我们可以使用 read 读取 /etc/passwd 中的内容,并成功地把字段分给不同的变量。这个就是做这样的事情:

    1. #!/bin/bash
    2. # read-ifs: read fields from a file
    3. FILE=/etc/passwd
    4. read -p "Enter a user name > " user_name
    5. file_info=$(grep "^$user_name:" $FILE)
    6. if [ -n "$file_info" ]; then
    7. IFS=":" read user pw uid gid name home shell <<< "$file_info"
    8. echo "User = '$user'"
    9. echo "UID = '$uid'"
    10. echo "GID = '$gid'"
    11. echo "Full Name = '$name'"
    12. echo "Home Dir. = '$home'"
    13. echo "Shell = '$shell'"
    14. else
    15. echo "No such user '$user_name'" >&2
    16. exit 1
    17. fi

    这个脚本提示用户输入系统中一个帐户的用户名,然后显示在文件 /etc/passwd/ 文件中关于用户记录的不同字段。这个脚本包含有趣的两行。 第一个是:

    1. file_info=$(grep "^$user_name:" $FILE)

    这一行把 grep 命令的输入结果赋值给变量 file_info。grep 命令使用的正则表达式确保用户名只会在 /etc/passwd 文件中匹配一行。

    第二个有意思的一行是:

    这一行由三部分组成:对一个变量的赋值操作,一个带有一串参数的 read 命令,和一个奇怪的新的重定向操作符。我们首先看一下变量赋值。

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

    1. OLD_IFS="$IFS"
    2. IFS=":"
    3. read user pw uid gid name home shell <<< "$file_info"
    4. IFS="$OLD_IFS"

    我们先存储 IFS 的值,然后赋给一个新值,再执行 read 命令,最后把 IFS 恢复原值。显然,完成相同的任务,在命令之前放置变量名赋值是一种更简明的方式。

    这个 “<<<” 操作符指示一个 here 字符串。一个 here 字符串就像一个 here 文档,只是比较简短,由单个字符串组成。在这个例子中,来自 /etc/passwd 文件的数据发送给 read 命令的标准输入。我们可能想知道为什么选择这种相当晦涩的方法而不是:

    1. echo "$file_info" | IFS=":" read user pw uid gid name home shell

    29.2 验证输入

    从键盘输入这种新技能,带来了额外的编程挑战,校正输入。很多时候,一个良好编写的程序与一个拙劣程序之间的区别就是程序处理意外的能力。通常,意外会以错误输入的形式出现。在前面章节中的计算程序,我们已经这样做了一点儿,我们检查整数值,甄别空值和非数字字符。每次程序接受输入的时候,执行这类的程序检查非常重要,为的是避免无效数据。对于由多个用户共享的程序,这个尤为重要。如果一个程序只使用一次且只被作者用来执行一些特殊任务,那么为了经济利益而忽略这些保护措施,可能会被原谅。即使这样,如果程序执行危险任务,比如说删除文件,所以最好包含数据校正,以防万一。

    这里我们有一个校正各种输入的示例程序:

    1. #!/bin/bash
    2. # read-validate: validate input
    3. invalid_input () {
    4. echo "Invalid input '$REPLY'" >&2
    5. exit 1
    6. }
    7. read -p "Enter a single item > "
    8. # input is empty (invalid)
    9. [[ -z $REPLY ]] && invalid_input
    10. # input is multiple items (invalid)
    11. (( $(echo $REPLY | wc -w) > 1 )) && invalid_input
    12. # is input a valid filename?
    13. if [[ $REPLY =~ ^[-[:alnum:]\._]+$ ]]; then
    14. echo "'$REPLY' is a valid filename."
    15. if [[ -e $REPLY ]]; then
    16. echo "And file '$REPLY' exists."
    17. else
    18. echo "However, file '$REPLY' does not exist."
    19. fi
    20. if [[ $REPLY =~ ^-?[[:digit:]]*\.[[:digit:]]+$ ]]; then
    21. echo "'$REPLY' is a floating point number."
    22. echo "'$REPLY' is not a floating point number."
    23. fi
    24. # is input an integer?
    25. if [[ $REPLY =~ ^-?[[:digit:]]+$ ]]; then
    26. echo "'$REPLY' is an integer."
    27. else
    28. echo "'$REPLY' is not an integer."
    29. fi
    30. else
    31. echo "The string '$REPLY' is not a valid filename."
    32. fi

    这个脚本提示用户输入一个数字。随后,分析这个数字来决定它的内容。正如我们所看到的,这个脚本使用了许多我们已经讨论过的概念,包括 shell 函数,”[[ ]]”,”(( ))”,控制操作符 “&&”,以及 “if” 和一些正则表达式。

    一种常见的交互类型称为菜单驱动。在菜单驱动程序中,呈现给用户一系列选择,并要求用户选择一项。例如,我们可以想象一个展示以下信息的程序:

    1. Please Select:
    2. 1.Display System Information
    3. 2.Display Disk Space
    4. 3.Display Home Space Utilization
    5. 0.Quit
    6. Enter selection [0-3] >

    使用我们从编写 sys_info_page 程序中所学到的知识,我们能够构建一个菜单驱动程序来执行上述菜单中的任务:

    1. #!/bin/bash
    2. # read-menu: a menu driven system information program
    3. clear
    4. echo "
    5. Please Select:
    6. 1. Display System Information
    7. 2. Display Disk Space
    8. 3. Display Home Space Utilization
    9. 0. Quit
    10. "
    11. read -p "Enter selection [0-3] > "
    12. if [[ $REPLY =~ ^[0-3]$ ]]; then
    13. if [[ $REPLY == 0 ]]; then
    14. echo "Program terminated."
    15. exit
    16. fi
    17. if [[ $REPLY == 1 ]]; then
    18. echo "Hostname: $HOSTNAME"
    19. uptime
    20. exit
    21. fi
    22. if [[ $REPLY == 2 ]]; then
    23. df -h
    24. exit
    25. fi
    26. if [[ $REPLY == 3 ]]; then
    27. if [[ $(id -u) -eq 0 ]]; then
    28. echo "Home Space Utilization (All Users)"
    29. du -sh /home/*
    30. else
    31. echo "Home Space Utilization ($USER)"
    32. du -sh $HOME
    33. fi
    34. exit
    35. fi
    36. else
    37. echo "Invalid entry." >&2
    38. exit 1
    39. fi

    从逻辑上讲,这个脚本被分为两部分。第一部分显示菜单和用户输入。第二部分确认用户反馈,并执行选择的行动。注意脚本中使用的 exit 命令。在这里,在一个行动执行之后, exit 被用来阻止脚本执行不必要的代码。通常在程序中出现多个 exit 代码不是一个好主意(它使程序逻辑较难理解),但是它在这个脚本中可以使用。

    29.4 本章总结

    在这一章中,我们向着程序交互性迈出了第一步;允许用户通过键盘向程序输入数据。使用目前已经学过的技巧,有可能编写许多有用的程序,比如说特定的计算程序和容易使用的命令行工具前端。在下一章中,我们将继续建立菜单驱动程序概念,让它更完善。

    仔细研究本章中的程序,并对程序的逻辑结构有一个完整的理解,这是非常重要的,因为即将到来的程序会日益复杂。作为练习,用 test 命令而不是 “[[ ]]” 复合命令来重新编写本章中的程序。提示:使用 grep 命令来计算正则表达式及其退出状态。这会是一个不错的练习。

    拓展阅读