鍍金池/ 教程/ Linux/ 讀取鍵盤輸入
網(wǎng)絡(luò)系統(tǒng)
打印
重定向
使用命令
位置參數(shù)
權(quán)限
文本處理
疑難排解
layout: book-zh title: 自定制 shell 提示符
查找文件
layout: book-zh title: vi 簡(jiǎn)介
shell 環(huán)境
什么是 shell
編譯程序
鍵盤高級(jí)操作技巧
流程控制:case 分支
流程控制:if 分支結(jié)構(gòu)
layout: book-zh title: 軟件包管理
進(jìn)程
存儲(chǔ)媒介
格式化輸出
編寫第一個(gè) Shell 腳本
啟動(dòng)一個(gè)項(xiàng)目
流程控制:while/until 循環(huán)
文件系統(tǒng)中跳轉(zhuǎn)
字符串和數(shù)字
讀取鍵盤輸入
歸檔和備份
探究操作系統(tǒng)
流程控制:for 循環(huán)
自頂向下設(shè)計(jì)
數(shù)組
操作文件和目錄
奇珍異寶
從 shell 眼中看世界
正則表達(dá)式

讀取鍵盤輸入

到目前為止我們編寫的腳本都缺乏一項(xiàng)在大多數(shù)計(jì)算機(jī)程序中都很常見的功能-交互性。也就是, 程序與用戶進(jìn)行交互的能力。雖然許多程序不必是可交互的,但一些程序卻得到益處,能夠直接 接受用戶的輸入。以這個(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)求用戶輸入數(shù)值,那 么它會(huì)更加有用處。在這個(gè)腳本中,我們將看一下我們?cè)鯓咏o程序增加交互性功能。

read - 從標(biāo)準(zhǔn)輸入讀取數(shù)值

這個(gè) read 內(nèi)部命令被用來從標(biāo)準(zhǔn)輸入讀取單行數(shù)據(jù)。這個(gè)命令可以用來讀取鍵盤輸入,當(dāng)使用 重定向的時(shí)候,讀取文件中的一行數(shù)據(jù)。這個(gè)命令有以下語(yǔ)法形式:

read [-options] [variable...]

這里的 options 是下面列出的可用選項(xiàng)中的一個(gè)或多個(gè),且 variable 是用來存儲(chǔ)輸入數(shù)值的一個(gè)或多個(gè)變量名。 如果沒有提供變量名,shell 變量 REPLY 會(huì)包含數(shù)據(jù)行。

基本上,read 會(huì)把來自標(biāo)準(zhǔn)輸入的字段賦值給具體的變量。如果我們修改我們的整數(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 選項(xiàng)(其會(huì)刪除輸出結(jié)果末尾的換行符)的 echo 命令,來顯示提示信息, 然后使用 read 來讀入變量 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 命令之后沒有列出變量名,則一個(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'

選項(xiàng)

read 支持以下選送:

表29-1: read 選項(xiàng)
選項(xiàng) 說明
-a array 把輸入賦值到數(shù)組 array 中,從索引號(hào)零開始。我們 將在第36章中討論數(shù)組問題。
-d delimiter 用字符串 delimiter 中的第一個(gè)字符指示輸入結(jié)束,而不是一個(gè)換行符。
-e 使用 Readline 來處理輸入。這使得與命令行相同的方式編輯輸入。
-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 完成有趣的事情。例如,通過-p 選項(xiàng),我們能夠提供提示信息:

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

通過 -t 和 -s 選項(xiàng),我們可以編寫一個(gè)這樣的腳本,讀取“秘密”輸入,并且如果在特定的時(shí)間內(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

這個(gè)腳本提示用戶輸入一個(gè)密碼,并等待輸入10秒鐘。如果在特定的時(shí)間內(nèi)沒有完成輸入, 則腳本會(huì)退出并返回一個(gè)錯(cuò)誤。因?yàn)榘艘粋€(gè) -s 選項(xiàng),所以輸入的密碼不會(huì)出現(xiàn)在屏幕上。

IFS

通常,shell 對(duì)提供給 read 的輸入按照單詞進(jìn)行分離。正如我們所見到的,這意味著多個(gè)由一個(gè)或幾個(gè)空格 分離開的單詞在輸入行中變成獨(dú)立的個(gè)體,并被 read 賦值給單獨(dú)的變量。這種行為由 shell 變量IFS (內(nèi)部字符分隔符)配置。IFS 的默認(rèn)值包含一個(gè)空格,一個(gè) tab,和一個(gè)換行符,每一個(gè)都會(huì)把 字段分割開。

我們可以調(diào)整 IFS 的值來控制輸入字段的分離。例如,這個(gè) /etc/passwd 文件包含的數(shù)據(jù)行 使用冒號(hào)作為字段分隔符。通過把 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è)腳本提示用戶輸入系統(tǒng)中一個(gè)帳戶的用戶名,然后顯示在文件 /etc/passwd/ 文件中關(guān)于用戶記錄的 不同字段。這個(gè)腳本包含兩個(gè)有趣的文本行。 第一個(gè)是:

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

這一行把 grep 命令的輸入結(jié)果賦值給變量 file_info。grep 命令使用的正則表達(dá)式 確保用戶名只會(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è)例子中,來自 /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 處理管道線的方式有關(guān)系。在 bash(和其它 shells,例如 sh)中,管道線 會(huì)創(chuàng)建子 shell。它們是 shell 的副本,且用來執(zhí)行命令的環(huán)境變量在管道線中。 上面示例中,read 命令將在子 shell 中執(zhí)行。

在類 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 字符串是解決此問題的一種方法。另一種方法將在37章中討論。

校正輸入

從鍵盤輸入這種新技能,帶來了額外的編程挑戰(zhàn),校正輸入。很多時(shí)候,一個(gè)良好編寫的程序與 一個(gè)拙劣程序之間的區(qū)別就是程序處理意外的能力。通常,意外會(huì)以錯(cuò)誤輸入的形式出現(xiàn)。在前面 章節(jié)中的計(jì)算程序,我們已經(jīng)這樣做了一點(diǎn)兒,我們檢查整數(shù)值,甄別空值和非數(shù)字字符。每次 程序接受輸入的時(shí)候,執(zhí)行這類的程序檢查非常重要,為的是避免無(wú)效數(shù)據(jù)。對(duì)于 由多個(gè)用戶共享的程序,這個(gè)尤為重要。如果一個(gè)程序只使用一次且只被作者用來執(zhí)行一些特殊任務(wù), 那么為了經(jīng)濟(jì)利益而忽略這些保護(hù)措施,可能會(huì)被原諒。即使這樣,如果程序執(zhí)行危險(xiǎn)任務(wù),比如說 刪除文件,所以最好包含數(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è)腳本提示用戶輸入一個(gè)數(shù)字。隨后,分析這個(gè)數(shù)字來決定它的內(nèi)容。正如我們所看到的,這個(gè)腳本 使用了許多我們已經(jīng)討論過的概念,包括 shell 函數(shù),[[ ]],(( )),控制操作符 &&,以及 if 和 一些正則表達(dá)式。

菜單

一種常見的交互類型稱為菜單驅(qū)動(dòng)。在菜單驅(qū)動(dòng)程序中,呈現(xiàn)給用戶一系列選擇,并要求用戶選擇一項(xiàng)。 例如,我們可以想象一個(gè)展示以下信息的程序:

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

使用我們從編寫 sys_info_page 程序中所學(xué)到的知識(shí),我們能夠構(gòu)建一個(gè)菜單驅(qū)動(dòng)程序來執(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è)腳本被分為兩部分。第一部分顯示菜單和用戶輸入。第二部分確認(rèn)用戶反饋,并執(zhí)行 選擇的行動(dòng)。注意腳本中使用的 exit 命令。在這里,在一個(gè)行動(dòng)執(zhí)行之后, exit 被用來阻止腳本執(zhí)行不必要的代碼。 通常在程序中出現(xiàn)多個(gè) exit 代碼是一個(gè)壞想法(它使程序邏輯較難理解),但是它在這個(gè)腳本中起作用。

總結(jié)歸納

在這一章中,我們向著程序交互性邁出了第一步;允許用戶通過鍵盤向程序輸入數(shù)據(jù)。使用目前 已經(jīng)學(xué)過的技巧,有可能編寫許多有用的程序,比如說特定的計(jì)算程序和容易使用的命令行工具 前端。在下一章中,我們將繼續(xù)建立菜單驅(qū)動(dòng)程序概念,讓它更完善。

友情提示

仔細(xì)研究本章中的程序,并對(duì)程序的邏輯結(jié)構(gòu)有一個(gè)完整的理解,這是非常重要的,因?yàn)榧磳⒌絹淼?程序會(huì)日益復(fù)雜。作為練習(xí),用 test 命令而不是[[ ]]復(fù)合命令來重新編寫本章中的程序。 提示:使用 grep 命令來計(jì)算正則表達(dá)式及其退出狀態(tài)。這會(huì)是一個(gè)不錯(cuò)的實(shí)踐。

拓展閱讀