到目前為止我們編寫的腳本都缺乏一項在大多數(shù)計算機程序中都很常見的功能-交互性。也就是, 程序與用戶進行交互的能力。雖然許多程序不必是可交互的,但一些程序卻得到益處,能夠直接 接受用戶的輸入。以這個前面章節(jié)中的腳本為例:
#!/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 數(shù)值的時候,我們必須編輯這個腳本。如果腳本能請求用戶輸入數(shù)值,那 么它會更加有用處。在這個腳本中,我們將看一下我們怎樣給程序增加交互性功能。
這個 read 內(nèi)部命令被用來從標準輸入讀取單行數(shù)據(jù)。這個命令可以用來讀取鍵盤輸入,當使用 重定向的時候,讀取文件中的一行數(shù)據(jù)。這個命令有以下語法形式:
read [-options] [variable...]
這里的 options 是下面列出的可用選項中的一個或多個,且 variable 是用來存儲輸入數(shù)值的一個或多個變量名。 如果沒有提供變量名,shell 變量 REPLY 會包含數(shù)據(jù)行。
基本上,read 會把來自標準輸入的字段賦值給具體的變量。如果我們修改我們的整數(shù)求值腳本,讓其使用 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 選項(其會刪除輸出結(jié)果末尾的換行符)的 echo 命令,來顯示提示信息, 然后使用 read 來讀入變量 int 的數(shù)值。運行這個腳本得到以下輸出:
[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'"
在這個腳本中,我們給五個變量賦值并顯示其結(jié)果。注意當給定不同個數(shù)的數(shù)值后,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 命令接受到變量值數(shù)目少于期望的數(shù)字,那么額外的變量值為空,而多余的輸入數(shù)據(jù)則會 被包含到最后一個變量中。如果 read 命令之后沒有列出變量名,則一個 shell 變量,REPLY,將會包含 所有的輸入:
#!/bin/bash
# read-single: read multiple values into default variable
echo -n "Enter one or more values > "
read
echo "REPLY = '$REPLY'"
這個腳本的輸出結(jié)果是:
[me@linuxbox ~]$ read-single
Enter one or more values > a b c d
REPLY = 'a b c d'
read 支持以下選送:
| 選項 | 說明 |
|---|---|
| -a array | 把輸入賦值到數(shù)組 array 中,從索引號零開始。我們 將在第36章中討論數(shù)組問題。 |
| -d delimiter | 用字符串 delimiter 中的第一個字符指示輸入結(jié)束,而不是一個換行符。 |
| -e | 使用 Readline 來處理輸入。這使得與命令行相同的方式編輯輸入。 |
| -n num | 讀取 num 個輸入字符,而不是整行。 |
| -p prompt | 為輸入顯示提示信息,使用字符串 prompt。 |
| -r | Raw mode. 不把反斜杠字符解釋為轉(zhuǎn)義字符。 |
| -s | Silent mode. 不會在屏幕上顯示輸入的字符。當輸入密碼和其它確認信息的時候,這會很有幫助。 |
| -t seconds | 超時. 幾秒鐘后終止輸入。read 會返回一個非零退出狀態(tài),若輸入超時。 |
| -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 選項,我們可以編寫一個這樣的腳本,讀取“秘密”輸入,并且如果在特定的時間內(nèi) 輸入沒有完成,就終止輸入。
#!/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秒鐘。如果在特定的時間內(nèi)沒有完成輸入, 則腳本會退出并返回一個錯誤。因為包含了一個 -s 選項,所以輸入的密碼不會出現(xiàn)在屏幕上。
通常,shell 對提供給 read 的輸入按照單詞進行分離。正如我們所見到的,這意味著多個由一個或幾個空格 分離開的單詞在輸入行中變成獨立的個體,并被 read 賦值給單獨的變量。這種行為由 shell 變量IFS (內(nèi)部字符分隔符)配置。IFS 的默認值包含一個空格,一個 tab,和一個換行符,每一個都會把 字段分割開。
我們可以調(diào)整 IFS 的值來控制輸入字段的分離。例如,這個 /etc/passwd 文件包含的數(shù)據(jù)行 使用冒號作為字段分隔符。通過把 IFS 的值更改為單個冒號,我們可以使用 read 讀取 /etc/passwd 中的內(nèi)容,并成功地把字段分給不同的變量。這個就是做這樣的事情:
#!/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
這個腳本提示用戶輸入系統(tǒng)中一個帳戶的用戶名,然后顯示在文件 /etc/passwd/ 文件中關(guān)于用戶記錄的 不同字段。這個腳本包含兩個有趣的文本行。 第一個是:
file_info=$(grep "^$user_name:" $FILE)
這一行把 grep 命令的輸入結(jié)果賦值給變量 file_info。grep 命令使用的正則表達式 確保用戶名只會在 /etc/passwd 文件中匹配一個文本行。
第二個有意思的文本行是:
IFS=":" read user pw uid gid name home shell <<< "$file_info"
這一行由三部分組成:一個變量賦值,一個帶有一串參數(shù)的 read 命令,和一個奇怪的新的重定向操作符。 我們首先看一下變量賦值。
Shell 允許在一個命令之前立即發(fā)生一個或多個變量賦值。這些賦值為跟隨著的命令更改環(huán)境變量。 這個賦值的影響是暫時的;只是在命令存在期間改變環(huán)境變量。在這種情況下,IFS 的值改為一個冒號。 另外,我們也可以這樣編碼:
OLD_IFS="$IFS"
IFS=":"
read user pw uid gid name home shell <<< "$file_info"
IFS="$OLD_IFS"
我們先存儲 IFS 的值,然后賦給一個新值,再執(zhí)行 read 命令,最后把 IFS 恢復原值。顯然,完成相同的任務, 在命令之前放置變量名賦值是一種更簡明的方式。
這個 <<< 操作符指示一個 here 字符串。一個 here 字符串就像一個 here 文檔,只是比較簡短,由
單個字符串組成。在這個例子中,來自 /etc/passwd 文件的數(shù)據(jù)發(fā)送給 read 命令的標準輸入。
我們可能想知道為什么選擇這種相當晦澀的方法而不是:
echo "$file_info" | IFS=":" read user pw uid gid name home shell
你不能管道 read
雖然通常 read 命令接受標準輸入,但是你不能這樣做:
echo "foo" | read
我們期望這個命令能生效,但是它不能。這個命令將顯示成功,但是 REPLY 變量 總是為空。為什么會這樣?
答案與 shell 處理管道線的方式有關(guān)系。在 bash(和其它 shells,例如 sh)中,管道線 會創(chuàng)建子 shell。它們是 shell 的副本,且用來執(zhí)行命令的環(huán)境變量在管道線中。 上面示例中,read 命令將在子 shell 中執(zhí)行。
在類 Unix 的系統(tǒng)中,子 shell 執(zhí)行的時候,會為進程創(chuàng)建父環(huán)境的副本。當進程結(jié)束 之后,環(huán)境副本就會被破壞掉。這意味著一個子 shell 永遠不能改變父進程的環(huán)境。read 賦值變量, 然后會變?yōu)榄h(huán)境的一部分。在上面的例子中,read 在它的子 shell 環(huán)境中,把 foo 賦值給變量 REPLY, 但是當命令退出后,子 shell 和它的環(huán)境將被破壞掉,這樣賦值的影響就會消失。
使用 here 字符串是解決此問題的一種方法。另一種方法將在37章中討論。
從鍵盤輸入這種新技能,帶來了額外的編程挑戰(zhàn),校正輸入。很多時候,一個良好編寫的程序與 一個拙劣程序之間的區(qū)別就是程序處理意外的能力。通常,意外會以錯誤輸入的形式出現(xiàn)。在前面 章節(jié)中的計算程序,我們已經(jīng)這樣做了一點兒,我們檢查整數(shù)值,甄別空值和非數(shù)字字符。每次 程序接受輸入的時候,執(zhí)行這類的程序檢查非常重要,為的是避免無效數(shù)據(jù)。對于 由多個用戶共享的程序,這個尤為重要。如果一個程序只使用一次且只被作者用來執(zhí)行一些特殊任務, 那么為了經(jīng)濟利益而忽略這些保護措施,可能會被原諒。即使這樣,如果程序執(zhí)行危險任務,比如說 刪除文件,所以最好包含數(shù)據(jù)校正,以防萬一。
這里我們有一個校正各種輸入的示例程序:
#!/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
這個腳本提示用戶輸入一個數(shù)字。隨后,分析這個數(shù)字來決定它的內(nèi)容。正如我們所看到的,這個腳本
使用了許多我們已經(jīng)討論過的概念,包括 shell 函數(shù),[[ ]],(( )),控制操作符 &&,以及 if 和
一些正則表達式。
一種常見的交互類型稱為菜單驅(qū)動。在菜單驅(qū)動程序中,呈現(xiàn)給用戶一系列選擇,并要求用戶選擇一項。 例如,我們可以想象一個展示以下信息的程序:
Please Select:
1.Display System Information
2.Display Disk Space
3.Display Home Space Utilization
0.Quit
Enter selection [0-3] >
使用我們從編寫 sys_info_page 程序中所學到的知識,我們能夠構(gòu)建一個菜單驅(qū)動程序來執(zhí)行 上述菜單中的任務:
#!/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
從邏輯上講,這個腳本被分為兩部分。第一部分顯示菜單和用戶輸入。第二部分確認用戶反饋,并執(zhí)行 選擇的行動。注意腳本中使用的 exit 命令。在這里,在一個行動執(zhí)行之后, exit 被用來阻止腳本執(zhí)行不必要的代碼。 通常在程序中出現(xiàn)多個 exit 代碼是一個壞想法(它使程序邏輯較難理解),但是它在這個腳本中起作用。
在這一章中,我們向著程序交互性邁出了第一步;允許用戶通過鍵盤向程序輸入數(shù)據(jù)。使用目前 已經(jīng)學過的技巧,有可能編寫許多有用的程序,比如說特定的計算程序和容易使用的命令行工具 前端。在下一章中,我們將繼續(xù)建立菜單驅(qū)動程序概念,讓它更完善。
仔細研究本章中的程序,并對程序的邏輯結(jié)構(gòu)有一個完整的理解,這是非常重要的,因為即將到來的
程序會日益復雜。作為練習,用 test 命令而不是[[ ]]復合命令來重新編寫本章中的程序。
提示:使用 grep 命令來計算正則表達式及其退出狀態(tài)。這會是一個不錯的實踐。
Bash 參考手冊有一章關(guān)于內(nèi)部命令的內(nèi)容,其包括了read命令:
http://www.gnu.org/software/bash/manual/bashref.html#Bash-Builtins