快速學會 shell 程式設計

非同步社群發表於2017-11-01

在過去幾十年中所出現的UNIX和類UNIX作業系統家族已經成為如今最為流行、使用最廣泛的作業系統之一,這都算不上什麼祕密了。對於使用了多年UNIX的程式設計師而言,一切都順理成章:UNIX系統為程式開發提供了既優雅又高效的環境。這正是Dennis Ritchie和Ken Thompson在20世紀60年代晚期在貝爾實驗室開發UNIX時的初衷。

在本書中,我們使用的術語UNIX泛指基於UNIX的作業系統大家族,其中包括像Solaris這樣真正的UNIX作業系統以及像Linux和Mac OS X這樣的類UNIX作業系統。

UNIX系統最重要的特性之一就是各式各樣的程式。超過200個基本命令會隨著標準作業系統發行,Linux還對標準命令數量做了擴充,通常能達到700~1000個!這些命令(也稱為工具)從統計檔案行數、傳送電子郵件到顯示特定年份的日曆,可謂無所不能。

不過UNIX真正的威力並非來自數量龐大的命令,而在於你可以非常輕鬆、優雅地將這些命令組合在一起完成非常複雜的任務。

UNIX的標準使用者介面是命令列,其實就是Shell,它的角色是作為使用者和系統最底層之間(核心)的緩衝帶。Shell就是一個程式,讀入使用者輸入的命令,將其轉換成系統更易於理解的形式。它還包括了一些核心程式設計構件,可以做出判斷、執行迴圈以及為變數儲值。

從AT&T發行版(源自Stephen Bourne在貝爾實驗室編寫的初版)開始,標準Shell就是同UNIX系統捆綁在一起的。自那時起,IEEE根據Bourne Shell以及後續的一些其他Shell制訂了標準。該標準目前的(本書寫作之時)版本是Shell and Utilities volume of IEEE Std 1003.1-2001,也稱為POSIX標準。本書餘下的內容都離不開Shell。
在本章中,你將學習到什麼是UNIX的Shell,Shell能夠做什麼,以及為什麼說它是每個高階使用者工具箱中不可或缺的一部分。20分鐘輕鬆學會shell很容易,不過如果想要全面掌握還需要專業的書籍來深度學習。
本文摘自《UNIX/Linux/OS X中的Shell程式設計(第4版)》


試讀地址:
www.epubit.com.cn/book/online…

2.1 核心和實用工具

UNIX系統在邏輯上被劃分為兩個不同的部分:核心和實用工具(Utility),如圖2.1所示。或者你也可以認為是核心和其他部分,通常來說,所有的訪問都要經由Shell。

圖2.1 UNIX系統

核心是UNIX系統的核心所在,當開啟計算機並啟動(booted)之後,核心就位於計算機的記憶體中,直到關機為止。

組成完整的UNIX系統的各種實用工具位於計算機磁碟中,在需要的時候會被載入到記憶體中並執行。實際上你所知道的所有UNIX命令都是實用工具,因此這些命令所對應的程式也都在磁碟上,僅在需要時才會被載入記憶體。舉例來說,當你執行date命令時,UNIX系統會將名為date的程式從磁碟上載入到記憶體中,讀取其程式碼來執行特定的操作。

Shell也是一個實用工具程式,它作為登入過程的一部分被載入到記憶體中執行。實際上,有必要了解當終端或終端視窗中的第一個Shell啟動時所發生的一系列事件。

2.2 登入Shell

在早期,終端是一個物理裝置,通過線纜連線到安裝了UNIX系統的硬體上。而如今,終端程式能夠讓你停留在Linux、Mac或Windows環境內部,在受控視窗(managed window)中同網路上的裝置互動。通常來說,你會啟動如Terminal或xterm這類程式,然後在需要的時候利用sshtelnetrlogin連線到遠端系統。

對於系統上的每個物理終端,都會啟用一個叫作getty的程式,如圖2.2所示。

圖2.2 getty程式

只要系統允許使用者登入,UNIX系統(更準確地說,應該是叫作init的程式)就會在每個終端埠自動啟動一個getty程式。getty是一個裝置驅動程式,能夠讓login程式在其所分配的終端上顯示login:,等待使用者輸入內容。

如果你是通過ssh這類程式來連線的,會分配到一個偽終端或偽tty。這就是為什麼在輸入who命令時會看到有類似於ptty3pty1這樣的條目。

在這兩種情況下,會有程式讀取賬戶和密碼資訊,對這些資訊進行驗證,如果沒有問題的話,就呼叫登入所需的登入程式。

只要輸入相應字元並敲下Enter鍵,login程式就完成了登入過程(見圖2.3)。

login開始執行時,它會在終端上顯示字串Password:,然後等待使用者輸入密碼。完成輸入並按下Enter鍵後(出於安全性的考慮,你在螢幕上看不到輸入的內容),login會比對檔案/etc/passwd中相應的條目來驗證登入名和密碼。每個使用者在該檔案中都有對應的條目,其中包括了登入名、主目錄以及使用者登入後要啟動的程式。最後一部分資訊(登入Shell)儲存在每行最後一個冒號之後。如果這個冒號後面沒有內容,則預設使用標準Shell,即/bin/sh

圖2.3 使用者sue終端上啟動的login

如果是通過終端程式登入,資料交換也許會涉及系統上的程式(如ssh)和伺服器上的程式(如sshd),要是你在自己的UNIX計算機上開啟了視窗,可能不需要再次輸入密碼就能夠立刻登入。非常方便!

把話題轉回密碼檔案。下面3行展示了/etc/passwd檔案內容的典型形式,對應著系統使用者:suepatbob

sue:*:15:47::/users/sue:
pat:*:99:7::/users/pat:/bin/ksh
bob:*:13:100::/users/data:/users/data/bin/data_entry複製程式碼

login將所輸入密碼的加密形式與特定賬戶儲存在/etc/shadow中的加密形式進行比對之後,如果沒有問題,它會檢查要執行的登入程式的名稱。在絕大多數情況下,這個登入程式會是/bin/sh/bin/ksh/bin/bash。在少數情況下,可能會是一個特殊的定製程式或者/bin/nologin,後者用於不能進行互動式訪問的賬戶(常用於檔案所有權管理)。其背後的理念就是你可以為登入賬戶進行設定,使其登入到系統之後能夠自動執行指定的程式。大多數時候指定的程式都是Shell,畢竟它是一種通用的實用工具,不過這並非是唯一的選擇。

來看使用者sue。一旦該使用者通過驗證,login會結束掉自身,將控制權轉交給sue的終端連線,該連線與標準Shell相連,然後login就從記憶體中消失了(見圖2.4)。

按照之前/etc/passwd檔案中顯示的其他條目,pat得到的是儲存在/bin下的ksh(這是Korn Shell),bob得到的是一個名為data_entry的指定程式(見圖2.5)。

圖2.4 login執行/usr/bin/sh

圖2.5 3個登入的使用者

之前提到過,init程式會針對網路連線執行類似於getty的程式。例如,sshdtelnetdrlogind會響應來自sshtelnetrlogin的連線請求。這些程式並沒有直接和特定的物理終端或調變解調器線路聯絡在一起,而是將使用者的Shell連線到偽終端上。你可以在X Window系統的視窗中或使用who命令檢視是否已經通過網路或聯網的終端連線登入到了系統中:

$ who
phw      pts/0    Jul 20 17:37          使用rlogin登入
$複製程式碼

2.3 在Shell中輸入命令

當Shell啟動時,它會在終端中顯示出一個命令列提示符,通常是美元符$,然後等待使用者輸入命令(圖2.6中的第1步和第2步)。每次輸入命令並按Enter鍵(第3步),Shell就會分析輸入的內容,然後執行所請求的操作(第4步)。

如果你要求Shell呼叫某個程式,Shell會搜尋磁碟,查詢環境變數PATH中指定的所有目錄,直到找到指定的程式。找到了該程式後,Shell會將自己複製一份(稱為子Shell),讓核心使用指定的程式替換這個子Shell,接著登入Shell就會“休眠”,等待被呼叫的程式執行完畢(第5步)。核心將指定程式複製到記憶體中並開始執行。這個複製過來的程式稱為程式。程式和程式之間是有區別的,前者是儲存在磁碟上的檔案,而後者位於記憶體中並被逐行執行。

如果程式將輸出寫入到標準輸出中,那麼輸出內容會出現在終端裡,除非你將其重定向或通過管道導向其他命令。與此類似,如果程式從標準輸入中讀取輸入,那麼它會等著你輸入內容,除非輸入被重定向到了另一個檔案或通過管道從其他命令匯入(第6步)。

當命令執行完畢後,就會從記憶體中消失,控制權再次交給登入Shell,它會提示你輸入下一條命令(第7步和第8步)。

圖2.6 命令執行週期

注意,只要你沒有登出系統,這個週期就會周而復始下去。如果登出系統,Shell就會終止執行,系統將會啟動一個新的getty(或者rlogind等)並等待其他使用者登入,如圖2.7所示。

重要的是要認識到Shell就是一個程式而已。它在系統中沒有什麼特權,也就是說,只要有足夠的專業技術和熱情,任何人都可以建立自己的Shell。這就是為什麼如今會有這麼多不同風格的Shell,其中包括由Stephen Bourne開發的古老的Bourne Shell、由David Korn開發的KornShell、主要用於Linux系統的Bourne again Shell以及由Bill Joy開發的C Shell。這些Shell都旨在應對特定的需求,各自都有自己獨特的功能和特色。

圖2.7 登入週期

2.4 Shell的職責

現在你知道了Shell會分析(用計算機行話來說,就是解析)輸入的每一行命令,然後執行指定的程式。在解析期間,檔名中的特殊字元(如*)會被擴充套件,就像第一章講到的那樣。

Shell還有其他的職責,如圖2.8所示。

圖2.8 Shell的職責

2.4.1 程式執行

Shell負責執行你在終端中指定的所有程式。

每次輸入一行內容,Shell就會分析該行,然後決定執行什麼操作。就Shell而言,每一行都遵循以下基本格式:

program-name arguments複製程式碼

說得更正式些,輸入的這一行叫做命令列。Shell會掃描該命令列,確定要執行的程式名稱及所傳入的程式引數。

Shell使用一些特殊字元來確定程式名稱及每個引數的起止。這些字元統稱為空白字元(whitespace characters),它們包括空格符、水平製表符和行尾符(更正式的叫法是換行符)。連續的多個空白字元會被Shell忽略。如果你輸入命令

mv    tmp/mazewars games複製程式碼

Shell會掃描該命令列,提取行首到第一個空白字元之間的所有內容作為待執行的程式名稱:mv。隨後的空白字元(多餘的空格)會被忽略,直到下一個空白字元之間的字元作為mv的第一個引數:tmp/mazewars。再到下一個空白字元(在本例中是換行符)之間的字元作為mv的第二個引數:games。解析完命令列之後,Shell就開始執行mv命令,其中包括兩個指定的引數:tmp/mazewarsgames(見圖2.9)。

圖2.9 執行帶有兩個引數的mv命令

剛才提到過,多個空白字元會被Shell忽略。這意味著當Shell處理下面的命令列時:

echo            when   do        we      eat? 複製程式碼

會向echo程式傳遞4個引數:whendoweeat?(見圖2.10)。

圖2.10 執行帶有4個引數的echo命令

echo會提取命令引數並將其顯示在終端中,因此在輸出的引數之間加上一個空格會使得命令輸出變得更易讀:

$ echo           when   do        we    eat? 
when do we eat? 
$複製程式碼

結果證明echo命令完全看不到這些空白字元,它們都被Shell給“沒收”了。等到第5章講引用的時候,你就知道該如何把空白字元包含到程式引數中了,不過,通常來說,去掉這些多餘的空白字元正是我們想要的做法。

我們之前講到過,Shell會搜尋磁碟,直到找到需要執行的程式為止,然後由UNIX核心負責程式的執行。在大多數時候,的確如此。但有些命令實際上是內建於Shell自身中的。這些內建命令包括cdpwdecho。Shell在磁碟中搜尋命令之前,它首先會判斷該命令是否為內建命令,如果是的話,就直接執行。

不過在呼叫命令之前,Shell還有點事需要處理,因此,讓我們先來討論一下這方面的內容。

2.4.2 變數及檔名替換

和比較正式的程式語言一樣,Shell允許將值賦給變數。只要你在命令列中將某個變數放在美元符號$之後,Shell就會將該變數替換成對應的變數值。我們會在第4章中詳細討論這個話題。

除此之外,Shell還會在命令列中執行檔名替換。實際上Shell,在確定要執行的程式及其引數之前,會掃描命令列,從中查詢檔名替換字元*?[...]

假設當前目錄下包含這些檔案:

$ ls
mrs.todd
prog1
shortcut
sweeney
$複製程式碼

現在讓我們在echo命令中使用檔名替換(*):

$ echo `*```               列出所有檔案
mrs.todd prog1 shortcut Sweeney
$複製程式碼

我們給echo程式傳入了幾個引數?1個還是4個?因為Shell會執行檔名替換,所以答案是4個。當Shell分析下列命令列時

echo *複製程式碼

它識別出了特殊字元*,將其替換成當前目錄下的所有檔名(甚至還會將這些檔名依字母順序排列):

echo mrs.todd prog1 shortcut sweeney複製程式碼

然後Shell決定將哪些引數傳給實際的命令。因此,echo根本不知道星號*的存在,它只知道命令列上有4個引數(見圖2.11)。

圖2.11 執行echo

2.4.3 I/O重定向

Shell還要負責處理輸入/輸出重定向。它會掃描每一個命令列,從中查詢特殊的重定向字元<>>>(如果你覺得好奇的話,還有一個重定向序列<<,你會在第12章中學到相關的內容)。

如果你輸入命令

echo Remember to record The Walking Dead > reminder複製程式碼

Shell會識別出特殊的輸出重定向字元>,然後將命令列中的下一個單詞作為輸出重定向所指向的檔名。在本例中,這個檔名為reminder。如果reminder已經存在且使用者具有寫許可權,那麼檔案中已有的內容會被覆蓋掉。如果沒有該檔案或其所在目錄的寫許可權,Shell會產生錯誤資訊。

在Shell執行程式之前,它會將程式的標準輸出重定向到指定的檔案。在大多數情況下,程式根本不知道自己的輸出被重定向了。它仍照舊向標準輸出中寫入(這通常是終端),意識不到Shell已經將資訊重定向到了檔案中。

讓我們來看兩個幾乎一樣的命令:

$ wc -l users
      5 users
$ wc -l < users
      5
$複製程式碼

在第一個例子中,Shell解析命令列,確定要執行的程式名稱是wc併為其傳入兩個引數:-lusers(見圖2.12)。

圖2.12 執行wc -l users

wc執行時,會看到傳入的兩個引數。第一個引數是-l,告訴它需要統計行數。第二個引數指定了待統計行數的檔案。因此wc會開啟檔案users,統計行數,然後列印出結果及對應的檔名。

第二個例子中的wc操作略有不同。Shell在掃描命令列時發現了輸入重定向字元<,其後的單詞就被解釋成從中重定向輸入的檔名。從命令列中提取出了“< users”之後,Shell就開始執行wc程式,將其標準輸入重定向為檔案users並傳入單個引數-l(見圖2.13)。

圖2.13 執行wc -l < users

這次當wc執行時,它會看到傳入的單個引數-l。因為沒有指定檔名,wc會轉而去統計標準輸入中內容的行數。因此wc -l在統計行數時,並不知道它實際上是在對檔案users進行統計。最後的顯示結果和平時一樣,但是缺少了檔名,因為我們並沒有為wc指定。

要理解兩條命令在執行上的不同,這一點非常重要。如果還不太清楚,那麼在繼續閱讀之前複習一下上面的內容。

2.4.4 管道

Shell在掃描命令列時,除了重定向符號之外還會查詢管道字元|。每找到一個,就會將之前命令的標準輸出連線到之後命令的標準輸入,然後執行這兩個命令。

如果你輸入

who | wc -l複製程式碼

Shell會查詢分隔了命令whowc的管道符號。它將上一個命令的標準輸出連線到下一個命令的標準輸入,然後執行兩者。who命令執行時會生成已登入使用者列表並將結果寫入標準輸出,它並不知道輸出內容並沒有出現在終端而是進入了另一個命令。

wc命令執行時,它發現並沒有指定檔名,因此就對標準輸入內容進行統計,並沒有意識到標準輸入並非來自終端,而是來自於who命令的輸出。

隨著本書內容的深入,你會看到管道中並不僅限於有兩條命令,你可以在複雜的管道中將3條、4條、5條甚至更多的命令串聯在一起。這多少有點不好理解,但卻是UNIX系統強大威力的所在。

2.4.5 環境控制

Shell提供了一些能夠定製個人環境的命令。個人環境包括主目錄、命令列提示符以及用於搜尋待執行程式的目錄列表。我們會在第10章中對此展開詳述。

2.4.6 解釋型程式語言

Shell有自己內建的程式語言。這種語言是解釋型的,也就是說,Shell會分析所遇到的每一條語句,然後執行所發現的有效的命令。這與C++及Swift這類程式語言不同,在這些語言中,程式語句在執行之前通常會被編譯成可由機器執行的形式。

相較於編譯型語言,由解釋型語言所編寫的程式一般要更易於除錯和修改。然而,所花費的時間要比實現相同功能的編譯型語言程式更長。

Shell程式語言提供了可在大多數其他程式語言中找到的其他特性。它有迴圈結構、決策語句、變數、函式,而且是程式導向的。基於IEEE POSIX標準的現代Shell還有許多其他特性,包括陣列、

相關文章