到目前為止我們編寫的腳本都缺乏一項在大多數計算機程序中都很常見的功能-交互性。也就是, 程序與用戶進行交互的能力。雖然許多程序不必是可交互的,但一些程序卻得到益處,能夠直接 接受用戶的輸入。以這個前面章節中的腳本為例:
~~~
#!/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
~~~
每次我們想要改變 INT 數值的時候,我們必須編輯這個腳本。如果腳本能請求用戶輸入數值,那 么它會更加有用處。在這個腳本中,我們將看一下我們怎樣給程序增加交互性功能。
## read - 從標準輸入讀取數值
這個 read 內部命令被用來從標準輸入讀取單行數據。這個命令可以用來讀取鍵盤輸入,當使用 重定向的時候,讀取文件中的一行數據。這個命令有以下語法形式:
~~~
read [-options] [variable...]
~~~
這里的 options 是下面列出的可用選項中的一個或多個,且 variable 是用來存儲輸入數值的一個或多個變量名。 如果沒有提供變量名,shell 變量 REPLY 會包含數據行。
基本上,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
~~~
我們使用帶有 -n 選項(其會刪除輸出結果末尾的換行符)的 echo 命令,來顯示提示信息, 然后使用 read 來讀入變量 int 的數值。運行這個腳本得到以下輸出:
~~~
[me@linuxbox ~]$ read-integer
Please enter an integer -> 5
5 is positive.
5 is odd.
~~~
read 可以給多個變量賦值,正如下面腳本中所示:
~~~
#!/bin/bash
# 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 怎樣操作:
~~~
[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,將會包含 所有的輸入:
~~~
#!/bin/bash
# read-single: read multiple values into default variable
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"
~~~
這個腳本的輸出結果是:
~~~
[me@linuxbox ~]$ read-single
Enter one or more values > a b c d
REPLY = 'a b c d'
~~~
### 選項
read 支持以下選送:
表29-1: read 選項
| 選項 | 說明 |
| --- | --- |
| -a array | 把輸入賦值到數組 array 中,從索引號零開始。我們 將在第36章中討論數組問題。 |
| -d delimiter | 用字符串 delimiter 中的第一個字符指示輸入結束,而不是一個換行符。 |
| -e | 使用 Readline 來處理輸入。這使得與命令行相同的方式編輯輸入。 |
| -n num | 讀取 num 個輸入字符,而不是整行。 |
| -p prompt | 為輸入顯示提示信息,使用字符串 prompt。 |
| -r | Raw mode. 不把反斜杠字符解釋為轉義字符。 |
| -s | Silent mode. 不會在屏幕上顯示輸入的字符。當輸入密碼和其它確認信息的時候,這會很有幫助。 |
| -t seconds | 超時. 幾秒鐘后終止輸入。read 會返回一個非零退出狀態,若輸入超時。 |
| -u fd | 使用文件描述符 fd 中的輸入,而不是標準輸入。 |
使用各種各樣的選項,我們能用 read 完成有趣的事情。例如,通過-p 選項,我們能夠提供提示信息:
~~~
#!/bin/bash
# read-single: read multiple values into default variable
read -p "Enter one or more values > "
echo "REPLY = '$REPLY'"
~~~
通過 -t 和 -s 選項,我們可以編寫一個這樣的腳本,讀取“秘密”輸入,并且如果在特定的時間內 輸入沒有完成,就終止輸入。
~~~
#!/bin/bash
# read-secret: input a secret pass phrase
if read -t 10 -sp "Enter secret pass phrase > " secret_pass; then
echo -e "\nSecret pass phrase = '$secret_pass'"
else
echo -e "\nInput timed out" >&2
exit 1
if
~~~
這個腳本提示用戶輸入一個密碼,并等待輸入10秒鐘。如果在特定的時間內沒有完成輸入, 則腳本會退出并返回一個錯誤。因為包含了一個 -s 選項,所以輸入的密碼不會出現在屏幕上。
## IFS
通常,shell 對提供給 read 的輸入按照單詞進行分離。正如我們所見到的,這意味著多個由一個或幾個空格 分離開的單詞在輸入行中變成獨立的個體,并被 read 賦值給單獨的變量。這種行為由 shell 變量__IFS__ (內部字符分隔符)配置。IFS?的默認值包含一個空格,一個 tab,和一個換行符,每一個都會把 字段分割開。
我們可以調整?IFS?的值來控制輸入字段的分離。例如,這個 /etc/passwd 文件包含的數據行 使用冒號作為字段分隔符。通過把?IFS?的值更改為單個冒號,我們可以使用 read 讀取 /etc/passwd 中的內容,并成功地把字段分給不同的變量。這個就是做這樣的事情:
~~~
#!/bin/bash
# read-ifs: read fields from a file
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
~~~
這個腳本提示用戶輸入系統中一個帳戶的用戶名,然后顯示在文件 /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 的值改為一個冒號。 另外,我們也可以這樣編碼:
~~~
OLD_IFS="$IFS"
IFS=":"
read user pw uid gid name home shell <<< "$file_info"
IFS="$OLD_IFS"
~~~
我們先存儲 IFS 的值,然后賦給一個新值,再執行 read 命令,最后把 IFS 恢復原值。顯然,完成相同的任務, 在命令之前放置變量名賦值是一種更簡明的方式。
這個?`<<<`?操作符指示一個 here 字符串。一個 here 字符串就像一個 here 文檔,只是比較簡短,由 單個字符串組成。在這個例子中,來自 /etc/passwd 文件的數據發送給 read 命令的標準輸入。 我們可能想知道為什么選擇這種相當晦澀的方法而不是:
~~~
echo "$file_info" | IFS=":" read user pw uid gid name home shell
~~~
> 你不能管道 read
>
> 雖然通常 read 命令接受標準輸入,但是你不能這樣做:
>
> echo “foo” | read
>
> 我們期望這個命令能生效,但是它不能。這個命令將顯示成功,但是 REPLY 變量 總是為空。為什么會這樣?
>
> 答案與 shell 處理管道線的方式有關系。在 bash(和其它 shells,例如 sh)中,管道線 會創建子 shell。它們是 shell 的副本,且用來執行命令的環境變量在管道線中。 上面示例中,read 命令將在子 shell 中執行。
>
> 在類 Unix 的系統中,子 shell 執行的時候,會為進程創建父環境的副本。當進程結束 之后,環境副本就會被破壞掉。這意味著一個子 shell 永遠不能改變父進程的環境。read 賦值變量, 然后會變為環境的一部分。在上面的例子中,read 在它的子 shell 環境中,把 foo 賦值給變量 REPLY, 但是當命令退出后,子 shell 和它的環境將被破壞掉,這樣賦值的影響就會消失。
>
> 使用 here 字符串是解決此問題的一種方法。另一種方法將在37章中討論。
## 校正輸入
從鍵盤輸入這種新技能,帶來了額外的編程挑戰,校正輸入。很多時候,一個良好編寫的程序與 一個拙劣程序之間的區別就是程序處理意外的能力。通常,意外會以錯誤輸入的形式出現。在前面 章節中的計算程序,我們已經這樣做了一點兒,我們檢查整數值,甄別空值和非數字字符。每次 程序接受輸入的時候,執行這類的程序檢查非常重要,為的是避免無效數據。對于 由多個用戶共享的程序,這個尤為重要。如果一個程序只使用一次且只被作者用來執行一些特殊任務, 那么為了經濟利益而忽略這些保護措施,可能會被原諒。即使這樣,如果程序執行危險任務,比如說 刪除文件,所以最好包含數據校正,以防萬一。
這里我們有一個校正各種輸入的示例程序:
~~~
#!/bin/bash
# read-validate: validate input
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
~~~
這個腳本提示用戶輸入一個數字。隨后,分析這個數字來決定它的內容。正如我們所看到的,這個腳本 使用了許多我們已經討論過的概念,包括 shell 函數,`[[ ]]`,`(( ))`,控制操作符?`&&`,以及?`if`?和 一些正則表達式。
## 菜單
一種常見的交互類型稱為菜單驅動。在菜單驅動程序中,呈現給用戶一系列選擇,并要求用戶選擇一項。 例如,我們可以想象一個展示以下信息的程序:
~~~
Please Select:
1.Display System Information
2.Display Disk Space
3.Display Home Space Utilization
0.Quit
Enter selection [0-3] >
~~~
使用我們從編寫 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
~~~
The presence of multiple `exit` points in a program is generally a bad idea (it makes
從邏輯上講,這個腳本被分為兩部分。第一部分顯示菜單和用戶輸入。第二部分確認用戶反饋,并執行 選擇的行動。注意腳本中使用的 exit 命令。在這里,在一個行動執行之后, exit 被用來阻止腳本執行不必要的代碼。 通常在程序中出現多個 exit 代碼是一個壞想法(它使程序邏輯較難理解),但是它在這個腳本中起作用。
## 總結歸納
在這一章中,我們向著程序交互性邁出了第一步;允許用戶通過鍵盤向程序輸入數據。使用目前 已經學過的技巧,有可能編寫許多有用的程序,比如說特定的計算程序和容易使用的命令行工具 前端。在下一章中,我們將繼續建立菜單驅動程序概念,讓它更完善。
### 友情提示
仔細研究本章中的程序,并對程序的邏輯結構有一個完整的理解,這是非常重要的,因為即將到來的 程序會日益復雜。作為練習,用 test 命令而不是`[[ ]]`復合命令來重新編寫本章中的程序。 提示:使用 grep 命令來計算正則表達式及其退出狀態。這會是一個不錯的實踐。
## 拓展閱讀
* Bash 參考手冊有一章關于內部命令的內容,其包括了`read`命令:
[http://www.gnu.org/software/bash/manual/bashref.html#Bash-Builtins](http://www.gnu.org/software/bash/manual/bashref.html#Bash-Builtins)
- 第一章:引言
- 第二章:什么是shell
- 第三章:文件系統中跳轉
- 第四章:研究操作系統
- 第五章:操作文件和目錄
- 第六章:使用命令
- 第七章:重定向
- 第八章:從shell眼中看世界
- 第九章:鍵盤高級操作技巧
- 第十章:權限
- 第十一章:進程
- 第十二章:shell環境
- 第十三章:VI簡介
- 第十四章:自定制shell提示符
- 第十五章:軟件包管理
- 第十六章:存儲媒介
- 第十七章:網絡系統
- 第十八章:查找文件
- 第十九章:歸檔和備份
- 第二十章:正則表達式
- 第二十一章:文本處理
- 第二十二章:格式化輸出
- 第二十三章:打印
- 第二十四章:編譯程序
- 第二十五章:編寫第一個shell腳本
- 第二十六章:啟動一個項目
- 第二十七章:自頂向下設計
- 第二十八章:流程控制 if分支結構
- 第二十九章:讀取鍵盤輸入
- 第三十章:流程控制 while/until 循環
- 第三十一章:疑難排解
- 第三十二章:流程控制 case分支
- 第三十三章:位置參數
- 第三十四章:流程控制 for循環
- 第三十五章:字符串和數字
- 第三十六章:數組
- 第三十七章:奇珍異寶