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