Bourne Shell 指令碼/檔案和流
當您想到一臺計算機及其相關的一切時,您通常會列出各種不同的東西。
- 計算機本身
- 顯示器
- 鍵盤
- 滑鼠
- 您的硬碟驅動器以及其中的檔案和目錄
- 連線到網際網路的網路連線
- 印表機
- DVD 播放器
- 等等
讓您感到意外的是:Unix 沒有這些東西。好吧,幾乎沒有。Unix 當然有 _檔案_。Unix 有無窮無盡的檔案。由於 Unix 有檔案,它也具有“檔案之間”的概念(這樣想:如果您的宇宙只包含盒子,您會自動知道沒有盒子的空間)。但 Unix 除了這些什麼都不知道。整個(Unix)宇宙中的所有東西都是檔案。
一切都是檔案。即使是一些很難想象成檔案的東西,也是檔案。您的(資料)檔案是檔案。您的目錄是檔案。您的硬碟驅動器是檔案。您的 _鍵盤_、_顯示器_ 和 _印表機_ 都是檔案。是的,真的:您的鍵盤是無限大小的只讀檔案。您的顯示器和印表機是無限大小的只寫檔案。您的網路連線是可讀寫檔案。
在這一點上,您可能想知道:_為什麼?_ 為什麼 Unix 系統的設計者會想出這種瘋狂的想法?為什麼所有東西都是檔案?答案是:因為如果所有東西都是檔案,那麼您可以 _像檔案一樣對待_ 所有東西。或者換句話說,您可以以相同的方式對待 Unix 世界中的所有東西。並且,正如我們很快就會看到的那樣,這意味著您也可以 _使用檔案操作來組合_ 幾乎所有東西。
在我們繼續之前,再給您增加一層怪異:Unix 中的所有東西都是檔案。包括執行程式的 _程序_。實際上,這意味著執行的程式也是檔案。包括您一直在執行以練習指令碼編寫的互動式 shell 會話。是的,真的,帶有閃爍游標的文字螢幕也是檔案。我們也可以證明這一點。您可能還記得,在關於 執行命令 的章節中,我們提到過您可以使用 Ctrl+d 鍵組合退出 shell。因為這個組合會生成 Unix 字元用於... 沒錯,檔案結束符!
正如我們在上一節中提到的,Unix 中的所有東西都是檔案——除了 _檔案之間_ 的東西。在檔案之間,Unix 定義了一種機制,允許資料逐位地從一個檔案移動到另一個檔案:_流_。流字面上就是它的意思:從一個檔案流向另一個檔案的一小條資料流。實際上,橋樑可能是一個更好的名字,因為與流(它是持續的水流)不同,檔案之間的資料流不一定持續,甚至可能根本不使用。
在 Unix 世界中,一般約定是每個檔案都連線到至少三個流(這是因為對於那些實際上是程序或正在執行的程式的檔案來說,這個數字是最有用的)。可以有更多流,實際上每個檔案都可以連線到任意數量的流(例如,程式可以列印並開啟網路連線)。但是,所有檔案都有三個基本流,即使它們可能並不總是可用或使用。這些流被稱為“標準”流
- 標準輸入 (stdin)
- 檔案的標準輸入流。
- 標準輸出 (stdout)
- 檔案的標準輸出流。
- 標準錯誤 (stderr)
- 檔案的標準錯誤輸出流。
正如您可能猜到的那樣,這些流非常適合那些實際上是系統程序的檔案。實際上,許多程式語言(如 C、C++、Java 和 Pascal)在其標準 I/O 操作中使用完全相同的約定。由於 Unix 作業系統家族在系統定義的核心部分包含了這些流,因此這些流也是 Bourne Shell 的核心。
所以現在我們知道 Unix 中有一個基本的輸入和輸出通用機制;但是您如何在指令碼中獲取這些流?您必須做什麼才能將您的指令碼連線到標準輸出,或者從標準輸入讀取資料?好訊息是:什麼也不需要。您的指令碼會自動連線到執行它們的程序的標準輸入、輸出和錯誤流。當您讀取輸入時,它會自動從標準輸入獲取。您的輸出會直接傳送到標準輸出。程式錯誤會直接傳送到標準錯誤。實際上,您已經使用過這些流:到目前為止,每個列印過任何內容的示例都是透過指令碼的標準輸出流完成的。
那麼互動模式下的 shell 呢?它也使用這些標準流嗎?是的,它使用。在互動模式下,標準輸入流連線到鍵盤檔案。標準輸出和標準錯誤連線到顯示器檔案。
到目前為止,關於檔案和流的討論非常有趣,並且對 Unix 的深度洞察力。但是,瞭解這一切對您有什麼用呢?啊,問得好!
Bourne Shell 有一些內建功能,允許您完成一些涉及檔案及其流的巧妙技巧。您看,檔案不僅僅有流——您還可以交叉連線兩個檔案的流。在上一節的最後,我們說互動式會話的標準輸入連線到鍵盤檔案。實際上,它連線到鍵盤檔案的 _標準輸出_ 流。互動式會話的標準輸出和錯誤連線到顯示器檔案的 _標準輸入_ 流。因此,您可以將互動式會話的流連線到裝置的流。
等等。你還記得上面關於 Unix 將所有內容都視為檔案,意味著所有內容都像檔案一樣處理的評論嗎?這就是它重要的原因:你可以將來自 *任何* 檔案的流連線到 *任何* 其他檔案的流。你可以將互動式 shell 會話連線到印表機或網路,而不是連線到監視器(或者除了監視器之外)。你可以執行一個程式,並將它的輸出直接傳送到印表機,方法是重新連線程式的標準輸出流。你可以將一個程式的標準輸出流直接連線到 **另一個程式** 的標準輸入流,並建立程式鏈。Bourne Shell 使這一切變得非常簡單。
你是否突然感覺好像把手指插進了電源插座?這就是 shell 的強大力量流經你身體的感覺……
重定向:在 shell 中使用流
[edit | edit source]正如上一節所述,shell 程序透過標準流連線到(預設情況下)鍵盤和監視器。但很多時候你可能想要改變這種連線。將檔案連線到流是一個非常常見的操作,所以你可能會期望它被稱為“連線”或“連結”。但由於 Bourne Shell 有預設的連線,並且你所做的一切都是對預設連線的 *改變*,所以使用 shell 將檔案連線到(不同的)流實際上被稱為 **重定向**。
Bourne Shell 中內建了幾個與重定向相關的運算子。最基本也是最通用的一個是管道運算子,我們將在後面詳細介紹。其他的都與重定向到檔案有關。
重定向到檔案
[edit | edit source]正如我們在上一節解釋(或者更確切地說:暗示)的那樣,Bourne Shell 在 Unix 作業系統之上的一個極其強大的功能是能夠將程式串聯在一起。執行一個程式,讓它生成輸出,然後自動將該輸出作為輸入傳送到另一個程式。可能的組合是無限的,你所能實現的功能的強大程度也是無限的。
你可能想要傳送程式輸出的最常見地方之一是檔案系統中的檔案。而這一次,檔案指的是一個普通的、傳統的文字檔案,而不是 Unix 的“一切都是檔案,包括你的硬體”的檔案。為了實現這一點,你可以想象我們可以使用上面描述的串聯機制:讓一個程式透過標準輸出流生成輸出,然後將該流(即 *重定向輸出*)連線到一個程式的標準輸入流,該程式在檔案系統中建立一個文字檔案。這樣做絕對有效。但是,重定向到文字檔案是一個如此常見的操作,你不需要一個單獨的鏈末端程式來完成它。重定向到檔案直接內建在 Bourne Shell 中,透過以下運算子實現
- process > data file
- 將 process 的輸出重定向到文字檔案;如果需要,建立檔案,否則覆蓋其現有內容。
- process >> data file
- 將 process 的輸出重定向到文字檔案;如果需要,建立檔案,否則追加到其現有內容。
- process < data file
- 讀取文字檔案的內容,並將這些內容作為輸入重定向到 process。
重定向輸出
[edit | edit source]讓我們透過一些示例仔細看看這些運算子。以以下名為 'hello.sh' 的簡單 Bourne shell 指令碼為例
#!/bin/sh
echo Hello
此程式碼可以使用在 執行命令 一章中描述的任何方式執行。當我們執行指令碼時,它只是將字串 "Hello" 輸出到螢幕,然後將我們返回到提示符。但是,假設我們想要將輸出重定向到檔案而不是螢幕。我們可以使用重定向運算子來輕鬆實現這一點
$ hello.sh > myfile.txt
$
這一次,我們沒有在螢幕上看到字串 'Hello'。它到哪裡去了呢?好吧,它正處在我們想要它去的地方:名為 'myfile.txt' 的(新)文字檔案。讓我們使用 'cat' 命令檢查一下這個檔案
$ cat myfile.txt
Hello
$
讓我們再次執行該程式,這次使用 '>>' 運算子,然後再次使用 'cat' 命令檢查 'myfile.txt'
$ hello.sh >> myfile.txt
$ cat myfile.txt
Hello
Hello
$
你可以看到,'myfile.txt' 現在包含兩行——輸出已新增到檔案的末尾(或連線);這是由於使用了 '>>' 運算子。如果我們再次執行指令碼,這次使用單個大於號運算子,我們將得到
$ hello.sh > myfile.txt
$ cat myfile.txt
Hello
$
只有一個 'Hello' 再次出現,因為 '>' 將始終覆蓋現有檔案的內容(如果有)。
重定向輸入
[edit | edit source]好的,很明顯我們可以將輸出重定向到文字檔案。但從文字檔案中讀取資料呢?這也很常見。Bourne Shell 也在這裡幫助我們:整個從檔案讀取資料並將其輸入到流中的過程被 '<' 運算子捕獲。
預設情況下,'stdin' 從你的鍵盤輸入;執行 'cat' 命令,不帶任何引數,它將一直等待,直到你輸入一些東西
$ cat
我可以在這裡一整天輸入,但似乎永遠無法從
這臺愚蠢的機器上得到我的提示符。
我甚至按了幾次回車鍵 !!!
.....等等....等等
實際上,'cat' 會一直等待,直到你輸入 'Ctrl+D'(簡稱 'End of File Character' 或 'EOF')。要從其他地方重定向標準輸入,請使用 '<'(小於號運算子)
$ cat < myfile.txt
Hello
$
所以 'cat' 現在將從文字檔案 'myfile.txt' 中讀取資料;檔案末尾也會生成 'EOF' 字元,因此 'cat' 將像以前一樣退出。
請注意,我們之前以這種格式使用過 'cat'
它在功能上與
相同。但是,這兩種機制從根本上是不同的:一種使用命令的引數,另一種更通用,它重定向 'stdin'——這就是我們在這裡關心的。使用檔名作為 'cat' 的引數更方便,這也是 'cat' 的發明者將其新增進去的原因。但是,並非所有程式和指令碼都接受引數,所以這只是一個簡單的例子。
組合檔案重定向
[edit | edit source]可以在一行中重定向 'stdin' 和 'stdout'
$ cat < myfile.txt > mynewfile.txt
上面的命令將複製 'myfile.txt' 的內容到 'mynewfile.txt'(並將覆蓋 'mynewfile.txt' 的任何先前內容)。這再次只是一個方便的例子,因為我們通常會使用 'cp myfile.txt mynewfile.txt' 來實現這種效果。
重定向標準錯誤(和其他流)
[edit | edit source]到目前為止,我們已經查看了與檔案相關的“正常”標準流的重定向,即在一切按計劃進行且沒有錯誤時使用的檔案。但另一個流呢?用於錯誤的流?我們如何重定向它?例如,如果我們想將錯誤資料重定向到日誌檔案。
例如,考慮 ls 命令。如果你執行 'ls myfile.txt' 命令,它只會列出檔名 'myfile.txt'——如果該檔案存在。如果檔案 'myfile.txt' *不存在*,'ls' 會將錯誤返回到 'stderr' 流,預設情況下,在 Bourne Shell 中,它也連線到你的監視器。
所以,讓我們執行 'ls' 幾次,首先在一個存在的檔案上,然後在一個不存在的檔案上
然後
再次,這次只重定向 'stdout'
我們仍然看到錯誤訊息;'logfile.txt' 將被建立,但為空。這是因為我們現在重定向 stdout 流,而錯誤訊息寫入錯誤流。那麼我們如何告訴 shell 我們想重定向錯誤流呢?
為了理解答案,我們必須再講一些關於 Unix 檔案和流的理論知識。你看,我們可以用簡單的運算子重定向 stdin 和 stdout 的根本原因是,重定向這些流非常常見,shell 讓我們對這些流使用簡寫符號。但實際上,為了完全正確,我們應該在每種情況下都告訴 shell 我們想要重定向的是哪個流。通常情況下,shell 無法知道:可能會有大量流連線到任何檔案。為了區分這些流,每個連線到檔案的流都關聯著一個數字:按照慣例,0 是標準輸入,1 是標準輸出,2 是標準錯誤,其他任何流的編號都以此類推。要重定向任何特定的流,請在重定向運算子之前加上流號(稱為檔案描述符)。因此,要重定向我們示例中的錯誤訊息,我們將在重定向運算子之前加上 2,表示 stderr 流。
沒有輸出到螢幕,但如果我們檢查 'logfile.txt'
正如我們之前提到的,沒有數字的運算子是簡寫符號。換句話說,
實際上是
我們也可以像這樣獨立地重定向 'stdout' 和 'stderr'
$
'stdio.txt' 將為空,'logfile.txt' 將包含之前一樣的錯誤。
如果我們想將 stdout 和 stderr 重定向到同一個檔案,我們也可以使用檔案描述符
這裡的 '2>&1' 意味著類似於 '將 stderr 重定向到 stdout 所重定向的同一個檔案'。小心順序!如果你這樣做
你將把 stderr 重定向到 stdout 指向的檔案,然後將 stdout 傳送到其他地方 - 並且這兩個流最終將被重定向到不同的位置。
特殊檔案
[edit | edit source]我們之前說過,到目前為止討論的所有重定向運算子都重定向到資料檔案。雖然這在技術上是正確的,但 Unix 的魔法意味著不僅僅是這樣。你看,Unix 檔案系統往往包含一些被稱為“裝置”的特殊檔案,按照慣例,這些檔案都收集在 /dev 目錄中。這些裝置檔案包括代表你的硬碟驅動器、DVD 播放器、USB 儲存器等的那些檔案。它們還包括一些特殊檔案,如 /dev/null(也稱為垃圾桶;寫入該檔案的任何內容都會被丟棄)。你也可以將資料重定向到裝置檔案,就像重定向到普通資料檔案一樣。這裡要小心;你真的不想將原始文字資料重定向到硬碟驅動器的引導扇區(而且你可以這樣做!)。但是,如果你知道自己在做什麼,就可以透過重定向到裝置檔案來使用它們(例如,這就是在 Linux 中燒錄 DVD 的方式)。
作為一個例子,說明你可能實際使用裝置檔案的方式,在 Unix 的 'Solaris' 版本中,揚聲器及其麥克風可以透過檔案 '/dev/audio' 訪問。因此
將播放聲音,而
將錄製聲音(你需要使用 CTRL-C 來完成...)。
這很有趣
現在,一邊喊叫一邊揮動麥克風 - 就像吉米·亨德里克斯那樣的反饋。太棒了。你可能需要以 'root' 使用者身份登入才能嘗試這樣做。
一些重定向警告
[edit | edit source]敏銳的讀者會注意到上面的討論中有一兩件事。首先,一個檔案可以擁有不止與之關聯的標準流。重定向這些流合法嗎?這甚至可能嗎?答案是,技術上來說是可能的。你可以重定向檔案中的第 4 或第 5 個流(如果它們存在)。不過不要嘗試這樣做。如果有不止幾個流,你將無法知道自己重定向的是哪個流。另外,如果一個程式需要不止標準流,那麼這個程式也很有可能需要將額外的流傳送到特定位置。
其次,你可能已經注意到檔案描述符 0 是標準輸入流。這是否意味著你可以將程式的標準輸入重定向遠離程式?你可以執行以下操作嗎?
答案是,可以。如果你這樣做,事情就會崩潰。
管道、三通和命名管道
[edit | edit source]所以,在討論了所有這些關於重定向到檔案的知識之後,我們終於要開始討論它了:透過交叉連線流進行的通用重定向。這是重定向的最通用形式,也是最強大的形式。它被稱為管道,使用管道運算子 '|' 執行。管道允許你透過“管道”將兩個程序連線在一起,該管道直接將一個檔案的 stdout 連線到另一個檔案的 stdin。
舉個例子,讓我們考慮一下 'grep' 命令,它在給定關鍵字和要搜尋的文字的情況下返回匹配的字串。讓我們也使用 ps 命令,它列出機器上正在執行的程序。如果你執行
它通常會列出你機器上執行的多個頁面,你需要手動篩選這些頁面才能找到你想要的內容。假設你正在尋找一個你已知包含單詞 'oracle' 的程序;使用 'ps' 的輸出透過管道傳遞到 grep,grep 將只返回匹配的行
現在你只會收到你需要的行。如果仍然有很多行怎麼辦?沒問題,將輸出透過管道傳遞到命令 'more'(或 'pg'),如果螢幕滿了,它會暫停你的螢幕
如果要殺死所有這些程序怎麼辦?你需要 'kill' 程式,以及每個程序的程序號(ps 命令返回的第二列)。很簡單
在這個命令中,'ps' 列出程序,'grep' 將結果縮小到 oracle。'awk' 工具提取每行中的第二列。'xargs' 將每一行作為命令列引數,一次一行地提供給 'kill'。
管道可以用來連結任意數量的程式,只要在合理範圍內(我們不知道這些範圍是什麼!)
不要忘記你仍然可以將重定向器組合在一起
還有另一個可以與管道一起使用的有用機制:'tee'。要理解 'tee',想象一個形狀像 'T' 的管道 - 一個輸入,兩個輸出
'tee' 將複製傳遞給其 stdin 的任何內容,並將此內容重定向到給定的引數(檔案);然後,它還會將另一個副本傳送到其 stdout - 這意味著你可以有效地擷取管道,在此階段獲取副本,然後繼續透過其他命令進行管道傳輸;這可能對輸出到日誌檔案和複製到螢幕很有用。
關於管道命令的一點說明:管道程序在 Unix 環境中並行執行。有時,一個程序會被阻塞,等待另一個程序的輸入。但原則上,管道中的每個程序都與所有其他程序同時執行。
命名管道
[edit | edit source]我們一直在討論的內聯管道有一種變體,叫做“命名管道”。命名管道實際上是一個擁有自己的 'stdin' 和 'stdout' 的檔案 - 你可以將程序附加到這些檔案。這對於允許程式相互通訊非常有用,尤其是在你不知道一個程式何時會嘗試與另一個程式通訊(等待備份完成等)以及不想編寫複雜的基於網路的監聽器或執行笨拙的輪詢迴圈的情況下。
要建立“命名管道”,可以使用 'mkfifo' 命令(fifo=先入先出;因此資料以寫入時的相同順序讀出)。
$
這將建立一個名為 'mypipe' 的命名管道;接下來,我們可以開始使用它。
此測試最好使用兩個登入的終端執行
1. 從“終端 a”
'cat' 將坐在那裡等待輸入。
2. 從“終端 b”
$
這應該會立即完成。切換回“終端 a”;現在它將從管道中讀取並接收 'EOF',你將在螢幕上看到資料;命令將完成,你將回到命令提示符。
現在嘗試相反的方式
1. 從終端 'b'
這將一直等待,因為管道另一端沒有其他程序來“清空”它 - 它被阻塞了。
2. 從終端 'a'
與之前一樣,兩個程序現在都將完成,輸出顯示在終端 'a' 上。
這裡文件
[edit | edit source]到目前為止,我們已經討論了從資料檔案重定向到資料檔案以及交叉連線資料流。所有這些 shell 機制都基於擁有資料 "物理" 源——一個程序或一個數據檔案。然而,有時你可能想向目標提供一些資料,而沒有源提供它。在這種情況下,你可以使用一個名為 "Here Document" 的 "動態" 文件。Here Document 意味著你開啟一個虛擬文字文件(在記憶體中),像往常一樣在其中輸入內容,關閉它,然後將其視為任何普通檔案。
建立 Here Document 是透過使用輸入重定向運算子的變體來完成的:"&<<" 運算子。與輸入重定向運算子一樣,Here Document 運算子也接受一個引數。對於輸入重定向運算子,這個運算元是要輸入的檔案的名稱。對於 Here Document 運算子,它是用於終止 Here Document 的字串。因此,使用 Here Document 運算子看起來像這樣
Here Document 內容
例如
cat << %%
> This is a test.
> This test uses a here document.
> Hello world.
> This here document will end upon the occurrence of the string "%%" on a separate line.
> So this document is still open now.
> But now it will end....
> %%
這個測試使用了一個 Here Document。
你好世界。
這個 Here Document 將在單獨的行上出現 "%%" 字串時結束。
所以現在這個文件仍然是開放的。
但現在它將結束……
當在使用 Here Document 與變數或命令替換結合使用時,重要的是要意識到替換是在 Here Document 被傳遞之前進行的。例如
$ COMMAND=cat
$ PARAM='Hello World!!'
$ $COMMAND <<%
> `echo $PARAM`
> %