read命令_Linux read命令:讀取從鍵盤或檔案輸入的資料

2020-07-16 10:04:31
不知道你是否還記得你的第一次計算機程式設計。我對“第一次”記憶猶新,那是一個很神奇的時刻。當我把有生以來編寫的第一個 C 程式交給計算機執行時,黑色的螢幕上顯示出了一行字“please input your name:”。

我懷著無比激動的心情寫下我的名字“roc”,然後按下回車。美妙的時刻就此凝固了,計算機毫無差錯地顯示出:
welcome !!! roc.

如果現在讓你用 Shell 來實現同樣效果的程式,不知道你是不是能很快就寫出來呢?

本文就來幫你實現這個程式,在實現該程式之前,我們還是先來學習一下 Linux 系統中最擅長傾聽的命令——read 吧。

傾聽鍵盤的聲音

read 命令用來傾聽標準輸入或檔案輸入,並把資訊存放到變數中。從 read 命令的定義可以看出,傾聽鍵盤是 read 的主要職責之一。

現在,我們用 Shell 來模擬一下上面的 C 程式吧:
#!/bin/bash
echo -n "please input your name:"
read name
echo "welcome !!! $name"
exit 0

就這麼簡單,使用 read 命令來傾聽使用者的輸入,並把使用者的輸入內容自動儲存到指定的 name 變數中,最後使用 echo 實現輸入內容的顯示。

上面的 Shell 指令碼是不是已經是最精簡的了?當然不是,還可以繼續精簡,我們可以直接使用 read 自帶的顯示提示語功能,省略程式中的 echo 語句:
#!/bin/bash
read -p "please input your name:" name
echo "welcome !!! $name"
exit 0
上面指令碼中的 read 的-p選項就是用來顯示提示語的。

在這個例子中,我們只是讓 read 來接收 name 變數的值,那麼 read 可以同時接收兩個甚至更多的變數嗎?答案是可以的。
#!/bin/bash
read -p "please input your name and place:" name place
echo "welcome $name to $place"
exit 0 

看到了嗎?read 後面可以指定兩個變數 name 和 place,當使用者輸入完成時,read 會以空格來分割使用者的輸入內容,並把輸入的內容分別存放到後面的變數中。

按照這個規律,read 完全可以指定 3 個、4 個……N 個變數。需要注意的是,當使用者的實際輸入和程式期望輸入的變數個數不等時,又會出現什麼情況呢?

這是一個好問題,比如上面的指令碼期望使用者輸入 name 和 place 兩個變數的值,而我們卻故意輸入一個資料或三個資料,這時來看看指令碼的執行情況:

範例一:只輸入一個資料
[[email protected] ~]$ ./test.sh
please input your name and place: roc
welcome roc to

範例二:輸入多個資料
[[email protected] ~]$ ./test.sh
please input your name and place:roc beijing shanghai tianjin
welcome roc to beijing shanghai tianjin

通過上面指令碼的執行,我們可以得出以下的結論:
  1. 如果輸入的資料數量少於變數的個數,那麼多餘的變數不會獲取到資料,即變數值為空。
  2. 如果輸入的資料數量多於變數的個數,那麼超出的資料將都賦值給最後一個變數。

還有一個非常特殊的情況,就是在編寫指令碼時,如果 read 命令後面沒有寫任何變數,指令碼執行時,我們輸入資料,那輸入的資料會存放到哪裡呢?

對於這種極端情況,Shell 的設計者早就預料到了:使用者的資料會存放到一個叫作 $REPLY 的環境變數中去。
#!/bin/bash
##
read -p "please input your name and place:"
echo "welcome $REPLY"
exit 0

[[email protected] ~]$ bash test.sh
please input your name and place:roc beijing
welcome roc beijing

read 非常樂於傾聽使用者的聲音,但有的時候,使用者長時間不輸入,那 read 也可以有自己的後手。read 命令提供了-t選項,可以用來設定一個傾聽的時限。如果超過所設定的時限的話,那麼 read 的耐心也就到此為止嘍。
#!/bin/bash
if read -t 5 -p "please input your name within 5s:" name
then
    echo "welcome !!! $name"
else
    echo "sorry, too slow"
fi
exit 0
上面的指令碼就使用了-t選項,用來指定等待輸入的時長(秒)。

上述指令碼表示 read 會等待使用者的輸入,但醜話說在前,如果 5 秒內使用者沒有響應,那麼 read 就會自動結束,並顯示“sorry,too slow”來抱怨一下使用者。可見,read 命令也可以有自己的態度的。

密碼輸入場景

還有一種使用場景非常特殊,就是密碼輸入,我們都不希望密碼明文顯示在螢幕上,萬一被別人看到了,可就不妙了。對於這種場景,我們希望實現隱藏輸入的效果,read 命令使用-s選項即可實現這樣的效果:
#!/bin/bash
read -s -p "please input your code:" password
echo "hehe, your password is $password"

上面的程式使用了-s選項,它實現的效果是使用者在輸入的時候,螢幕上不顯示任何資訊。這樣,是不是再也不用擔心密碼被別人看到了?

內容來自檔案

前面我們說過,read 命令不僅能監聽鍵盤輸入,還能讀取檔案內容。上面的範例都在說鍵盤,下面就來說檔案。

首先,我們準備一個素材檔案,其內容如下:
[[email protected] ~]$ cat -n test.txt
     1   19248
     2   19247
     3   19246

下面,我們為大家展示 read 命令的三種讀取檔案的方法。

第一種方法:使用-u選項
#! /bin/bash
# assign the file descriptor to file for input fd # 3 is Input file
exec 3< test.txt
 
#  read  the file using fd # 3
count=0
while read -u 3 var
do
        let count=$count+1
        echo "Line $count:$var"
done
echo "finished"
echo "Line no is $count"
 
# Close fd # 3
exec 3<&-
上面的指令碼通過“exec 3<test.txt”生成了編號為 3 的檔案描述符,接著通過“read-u 3 var”來讀取檔案內容。最後通過“exec 3<&-”關閉了 3 號檔案描述符。

我們來看一下指令碼的執行效果:
# 執行效果
[[email protected] ~]$ ./test.sh
Line 1:19248
Line 2:19247
Line 3:19246
finished
Line no is 3

第二種方法:使用管道
#!/bin/bash
count=1
cat test.txt | while read line
do
echo "Line $count:$line"
let count=$count+1
done
echo "finished"
echo "Line no is $count"
exit 0
請注意上面的用法“cat test.txt|while read line”,通過這樣的方式來遍歷 test.txt 檔案的內容,並按行賦值給變數 line。

# 執行效果
[[email protected] ~]$ ./test.sh
Line 1:19248
Line 2:19247
Line 3:19246
finished
Line no is 1
咦,有個問題。指令碼最後輸出的是 Line no is 1,怎麼行數是 1 呢,明明是 3 才對啊?

上面這個問題,是由於管道導致的。我們知道,管道的兩邊一般需要新建進程,當執行完 while 語句後,新進程也就結束了。而指令碼中 count 是新進程中的自定義變數,進程結束後該變數也就消失了(自定義變數的生命週期結束)。當指令碼執行 echo 時,顯示的 count 變數是指令碼中第一行定義的變數的值,而不是 while 語句中的那個 count 變數了,因而輸出的結果當然就是 1 了。

第三種方法:使用重定向
#!/bin/bash
count=0
while read line
do
let count=$count+1
echo "Line $count:$line"
done < test.txt
echo "finished"
echo "Line no is $count"
exit 0

由於第二種方法中的 count 變數計數有誤,所以我們才為大家介紹第三種方法,這種方法可以有效規避因為新建進程而導致的變數值無法保留的問題。

執行指令碼,看看執行結果是否符合預期:
[[email protected] ~]$ bash test.sh
Line 1:19248
Line 2:19247
Line 3:19246
finished
Line no is 3

分析上面的第二種方法和第三種方法可以發現,它們最核心的區別是“管道技術”變成了“重定向技術”,從而成功規避了進程中新生成自定義變數的問題。

神奇的換行符

我們接著上面重定向的例子來說,我們把 test.txt 檔案的內容稍微變動一下。
[[email protected] ~]$ cat -n test.txt
     1   19248 
     2   19247 
     3   19246

可以看到,我們在前兩行的後面分別加上了一個反斜線(),這個符號表示續行符,它實現的效果是“肉眼看上去是換行了,但是 Linux 認為並沒有換行”。

如果我們把這個檔案作為上面例子的輸入,那麼會輸出什麼呢?我們來看一下實際效果:
[[email protected] ~]$ ./test.sh
Line 1:19248 19247 19246
finished
Line no is 1

是不是和你想的一樣?這就是續行符()的作用,read 命令在讀取資料時,當讀取到續行符()時,它不認為這是一行的結束,而是會繼續讀取下一行,直到遇到真正的換行符(n)為止。

這樣一解釋,大家就應該知道為什麼指令碼執行結果裡只有 Line 1 了吧。

其實在 read 讀取到的資料中,所有的跳脫符表示的特殊含義都是起作用的,如果你不想讓它們起作用的話,請使用-r選項吧。
#! /bin/bash
# assign the file descriptor to file for input fd # 3 is Input file
exec 3< test.txt
 
#  read  the file using fd # 3
count=0
while read -u 3 -r var
do
        let count=$count+1
        echo "Line $count:$var"
done
echo "finished"
echo "Line no is $count"
 
 
# Close fd # 3
exec 3<&-

上面的程式碼中,我們會發現“read-u 3-r var”這一行增加了-r選項,如果我們仍以含有續行符的那個檔案為輸入檔案的話,輸出的結果就變成:
[[email protected] ~]$ ./test.sh
Line 1:19248 
Line 2:19247 
Line 3:19246
finished
Line no is 3

可以看到,續行符的作用被完全忽略了,原本的續行符()被當作原始的反斜線()符號了。這就是-r選項的威力,它的眼裡不揉沙子,任何符號都沒有特殊身份。

最後,我們要特別說明的一點是,-r選項不僅對讀取的檔案內容有效,而且對鍵盤的輸入也是有效的。