<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                企業??AI智能體構建引擎,智能編排和調試,一鍵部署,支持知識庫和私有化部署方案 廣告
                > 原文鏈接:?[http://www.aosabook.org/en/bash.html](http://www.aosabook.org/en/bash.html) > 作者: Chet Ramey ## 3.1 介紹 Unix Shell提供了一個接口,支持用戶通過命令與操作系統進行交互。但shell同時也算得上是一門豐富的編程語言,因為它包含了基本的流程控制結構: 替換(alternation),循環,條件判斷,還有基本的數學操作,函數定義,字符串變量,以及與命令之間的雙向通信。 shell可以在終端或終端模擬器(如xterm)中以交互的方式運行,也可以存儲在文件中作為腳本來使用。大部分現代shell環境(包括Bash)提供命令行編輯功能,用戶可以使用Emacs或Vi風格的快捷鍵來編輯命令行,或訪問命令的歷史紀錄。 Bash的處理過程類似于shell的流水線(pipe):首先由終端或腳本讀入數據,然后使用一系列變換過程依次進行處理,執行到最后一個shell命令后返回。 本章將討論Bash的主要組件:輸入處理,解析,單詞展開(word expansion)和其他命令處理,管道(pipeline)中的命令執行。這些組件構成一個流水線(pipeline),從鍵盤或腳本中獲取字符,然后逐步轉化為命令。 ![enter image description here](http://box.kancloud.cn/2015-08-20_55d580bdca4b4.jpg)圖3.1 Bash組件結構 ## 3.1 Bash Bash是一種GNU操作系統中的shell,通常在Linux內核上實現,其他操作系統(比如著名的Mac OS X)也有一些不同的實現版本。Bash在sh的歷史版本的基礎上做了一些功能上的改進,使其更便于交互式使用或編程。 Bash是Bourne-Again SHell的縮寫,為了紀念shell鼻祖Stephen Bourne(他是當代Unix Shell程序/bin/sh的創造者,該程序出現在貝爾實驗室第七版Unix上)。Bash的原作者是自由軟件基金會(Free Software Foundation)的一名雇員Brian Fox,而我是現在的開發者和維護員,同時還是俄亥俄州凱斯西儲大學(Case Western Reserve University in Cleveland, Ohio)的志愿者。 和其他GNU軟件一樣,Bash具有很好的可移植性。Bash能運行在幾乎每個版本的Unix上,它還被移植到了其他操作系統上(比如Windows上Cygwin和MingW),Bash同時還是某些類Unix操作系統(比如QNX和Minix)發行版的一部分。Bash的移植只依賴于一個Posix的編譯和運行環境,這個條件容易滿足,比如微軟公司的Unix服務(Services For Unix, SFU)就能支持Posix。 ## 3.2 句法單元和原語(primitive) **3.2.1 原語(primitive)** Bash中包含三種基本記號(token):保留關鍵字,單詞,操作符。保留關鍵字指在shell中和編程語言中有明確含義的詞語,這些關鍵字通常用來表達程序控制結構(比如`if`和`while`)。操作符由一個或多個元字符(metacharacter)構成,元字符指shell中具有特殊意義的字符,比如`|`和`>`。余下的shell的輸入都可以視為普通的單詞,但有時也會有特殊的含義,比如賦值語句和數字,這完全取決于在命令行中的位置。 **3.2.2 變量和參數** 和所有的編程語言一樣,shell也提供變量,變量是一些用來指代數據并支持數據操作的名稱。shell的變量包括用戶變量和內置變量(稱為參數)。一般,shell參數反映了shell的內部狀態,它們的值可能是自動設置的,也可能由其他操作設置。 變量的值都是字符串。根據上下文的不同,有一些值會被特殊處理(后面會有解釋)。變量使用"`name=value`"的形式來賦值,"`value`"這一項是可選的,將其省略表示賦值為空字符串。賦值時,shell將"`value`"展開并賦給"`name`"。shell能根據一個變量是否被設置(set)來執行不同的操作,但是賦值是設置變量的唯一方式。如果變量沒有賦值,即使已經聲明或已經設置屬性,也會被視為沒有設置(unset)。 以美元符(`$`)開頭的單詞表示對變量或參數的引用。帶`$`的單詞將會被變量的值所替代。shell提供了大量的展開操作符,包括簡單的值替換,根據模式匹配修改或刪除變量值的一部分。 shell還支持局部變量和全局變量,但變量都默認是全局的。如果賦值語句置于簡單命令(最常見的命令類型,可附帶一些參數和重定向)前,那么這些賦值語句定義的變量將是局部的。shell中可以定義過程(即shell函數),函數中也可以定義局部變量。 為減小代碼量,除了通常的變量之外,shell還支持整數和數組。整型變量表示數字,任何字符串賦值給整形變量時都會展開為算術表達式,計算結果并賦值。數組可以是索引數組(indexed array)或關聯數組(associative array),前者使用數字下標,后者使用字符串下標。數組元素都是字符串,必要時也可視為整數。但數組不支持嵌套,即數組元素不能是其他數組。 Bash使用哈希表(hash table)來存儲和訪問shell變量,這些哈希表被鏈接起來管理變量的作用域。shell函數中支持多種變量作用域,比如命令前置賦值語句構成一個臨時作用域。當被前置賦值語句的命令是shell內置命令時,shell就必須跟蹤變量的順序,以保證變量引用的正確性,作用域鏈表可實現這個功能。如果運行嵌套層次太多,需要遍歷的作用域數目會相當驚人。 **3.2.3 shell編程語言** 簡單的shell命令包含命令名稱(比如`echo`或`cd`),可選數目的參數和重定向(redirection)。重定向允許shell用戶控制命令的輸入和輸出。如前所述,用戶可以為簡單命令定義局部變量。 保留關鍵字可以實現復雜的shell命令。shell中包含了任何高級語言中都有的程序結構,比如`if-then-else`,?`while`, 遍歷列表的`for`循環,C風格的算術`for`循環等等。因而,shell中可構造諸如選擇性和重復執行命令的更為復雜的命令。 Unix帶給計算世界的一個重要貢獻是管道(pipeline),管道指一系列順序執行的命令,其中前一個命令的輸出構成下一個命令的輸入。任何shell結構都可以用于管道,我們甚至可以使用管道來生成其自身的輸入。 Bash支持標準輸入,標準輸出,和標準錯誤(standard error)三個數據流,命令的結果可以重定向到一個文件或一個進程(Unix中,任何進程和設備都可視為文件)。shell程序員還可以在當前shell環境中使用重定向來打開或關閉文件。 Bash支持shell編程,shell腳本可以存儲起來重復使用。shell函數和shell腳本都可以作為命令來執行,和單個命令的使用類似。shell函數定義為特殊的格式,可以在同一個shell上下文中存儲和執行。shell腳本將命令存儲在一個文件中,只能在一個新的shell進程中執行。shell函數共享了所在環境中的大部分上下文,但是shell腳本由于在新的shell進程中調用,只能共享一些進程間間的環境。 **3.2.4 注意事項** 繼續往下讀時,讀者要時刻牢記shell的實現代碼中只使用了少量的數據結構: 數組,樹,單向鏈表和雙向鏈表,以及哈希表。幾乎所有shell結構都是用這些基本結構實現的。 shell在不同階段傳輸信息并處理數據單元的數據結構是`WORD_DESC`。 ~~~ typedef struct word_desc { char *word; /* Zero terminated string. */ int flags; /* Flags associated with this word. */ } WORD_DESC; ~~~ 單詞被組合為簡單的鏈表,比如參數列表。 ~~~ typedef struct word_list { struct word_list *next; WORD_DESC *word; } WORD_LIST; ~~~ `WORD_LIST`在shell中無處不在。一個簡單的命令就是一個單詞列表,展開結果同樣是一個單詞列表,內置命令的參數還是一個單詞列表。 ## 3.3\. 輸入處理 Bash流水線(pipeline)的第一個階段是輸入處理,即從終端或文件中讀入字符,拆成行,傳遞給shell解析器,然后轉換為命令。所謂行其實就是以換行符結尾的字符串。 **3.3.1\. readline和命令行編輯** 在交互模式下,Bash的輸入來自終端,而對于腳本的執行,輸入則來自參數。在交互模式下,Bash允許用戶編輯命令行,快捷鍵類似于Unix Emacs或Vi編輯器。 Bash使用readline軟件庫來實現命令行編輯,用戶可以使用多種功能,比如保存命令行,調用前一條命令,執行csh風格的歷史記錄的展開。bash是readline庫的主要用戶,兩者也是在一起開發,但readline的實現并不依賴于Bash。許多其他項目也采用readline來支持終端命令行編輯。 readline還支持為眾多readline命令綁定不限長度的快捷鍵。readline的命令功能強大,支持光標在命令行中移動,插入或刪除文字,獲取以前的記錄,補全文本等等。基于這些命令,用于可以自定義宏,按下指定的快捷鍵就可以插入一段定制的文字。readline的宏為用戶提供簡單字符串替換和速記功能。 **readline 結構** readline是一個包含基本讀入/分發/執行/重顯示等步驟的循環結構。readline從鍵盤使用`read`或類似的函數讀入字符,或者使用宏來獲得輸入。每個字符都是鍵映射表(或分發(dispatch)表)中的索引。雖然索引都是單字節字符,映射表的的值卻可以表達更多的內容,因為它們可以指向新的映射表,這使得readline能夠支持多字符快捷鍵。快捷鍵最后會被綁定到某個readline命令上(如`beginning-of-line`),并且觸發這個命令。當敲擊鍵盤時,字符會存入編輯緩沖區,這是因為這些字符本身是已經綁定到了`self-insert`命令上。readline還支持讓一個快捷鍵綁定到一個命令,同時延長快捷鍵再綁定到另外一個命令上,映射表中有一個特殊的索引來標識這種情況(這個功能最近才開始支持)。將快捷鍵綁定到宏上帶來了極大的靈活性,用戶可以編輯任意字符,還可以定制復雜的快捷操作。readline的編輯緩沖區存儲了綁定到`self-insert`命令的那些字符,緩沖區顯示時占用一行或多行。 readline使用C語言字符來處理字符緩存和字符串,并基于這些簡單字符來構造多字節字符。出于速度和存儲方面的考慮,readline內部并不使用`wchar_t`類型,事實上,人們開始編碼的年代還不怎么廣泛支持多字符字符。如果本地設置(locale)支持多字節字符,readline自動讀入整個多字節字符并插入編輯緩沖區。因為可以用一些簡單字符序列來表達多字節字符,將多字節字符設為快捷鍵在理論上是行得通的,但是難于實現而且沒人愿意這么用。比如,Emacs和Vi的命令中就全部是單字節字符構成的快捷鍵。 當快捷鍵觸發編輯命令后,命令的執行可能會導致緩沖區中插入新的字符,或者編輯位置發生變化,或者部分乃至整行都被替換,但是readline始終會及時地將結果更新到終端上。有些編輯命令雖然可綁定到快捷鍵上,但并不會改變編輯緩沖區,比如有的只是修改歷史文件。 更新終端顯示雖然看起來簡單,實際卻非常復雜。readline必須跟蹤3個地方:當前緩沖區內容,更新后的緩沖區內容,以及實際顯示的字符。若考慮多字節字符,顯示出來的字符可能與緩沖區內容并不一定完全相符,更新顯示時必須要考慮到這個問題。當重新顯示時,readline需要比較當前緩沖區內容與更新后緩沖區內容,找出差異再決定怎樣高效地將差異更新到屏幕上。這個問題實際上已經被研究多年(即所謂的串對串校驗問題, string-to-string correction problem)。readline采用的方法是定位出差異區域的邊界點,然后計算出更新這一部分的代價,包括移動光標的代價(刪除一個字符再插入一個字符顯然不如直接覆蓋)。readline隨后以最小的代價來更新終端,刪除行尾多余的字符,并將光標置于正確的位置。 更新顯示引擎是readline中修改最為頻繁的代碼。大部分的修改都者為了增加新的功能,其中最為重要的功能是讓提示符(prompt)保持穩定(比如提示符的顏色),以及處理多字節字符的能力。 readline將編輯緩沖區中的內容返回給調用程序,然后由調用程序負責存儲到歷史列表中。 **readline擴展程序** readline不僅為用戶提供了多種定制和擴展默認行為的方式,還支持讓應用程序擴展默認功能。首先,可綁定的readline函數接受一個參數集合并返回特定類型的結果,應用程序可以很容易地使用定制的函數來擴展readline。以Bash為例,Bash綁定了30多個命令,支持Bash關鍵字的補全,調用shell內置命令等。 readling還允許應用程序使用回調函數來修改默認行為。應用程序可以傳入函數指針替換readline內部函數,干涉其運行過程,從而執行特定的操作。 **3.3.2\. 非交互式輸入處理** 如果shell不使用readline,它會使用`stdio`或自己的輸入緩沖函數來獲得輸入。如果shell是非交互式的,則Bash的緩沖輸入模塊更傾向于使用`stdio`,因為Posix標準對輸入有特別的限制條件:shell在解釋一條命令時只能占用必要的輸入,而把其它的留給運行中的其它程序。這點非常重要,特別是shell從標準輸入讀入一個腳本的情況。shell允許在輸入時盡可能多地緩沖輸入,只要它能夠在文件中回滾到解析器停止的位置。這意味著在不可隨機訪問(non-seekable)設備(如管道pipe)中shell一次只能讀一個字符,對于文件來說shell則可以緩存任意多的內容。 不考慮這些特殊情況,非交互式輸入的處理結果和readline一樣:每個緩沖區以換行符(newline)結束。 **3.3.3\. 多字節字符** 多字節字符的處理功能是在shell出現之后很久才加入進來的,因此其設計原則是盡可能小地影響已有代碼。如果本地設置(locale)支持多字節字符,shell將輸入存儲在字節緩沖區中(簡單的C語言字符),但是會將它們作為多字節字符進行處理。readline知道如何顯示多字節字符(關鍵問題是一個多字節字符占用多少屏幕空間,以及屏幕上顯示一個多字節字符需要使用多少個字節),如何在一行中移動一個字符(不是一個字節)的位置,等等。除了這些問題,多字節字符不會怎么影響輸入處理。但需要注意的是,shell的其它部分在處理輸入時需要考慮到多字節字符的影響(后面會有介紹)。 ## 3.4\. 解析 解析引擎的第一步工作是詞法分析:將字符流分割成單詞(word),然后賦予其意義。單詞(word)是解析器操作的基本單元,是由元字符分隔的字符序列。簡單的元字符如空格和制表符(tab),shell語言中的特殊字符也可構成元字符(如分號和`&`符號)。 Tom Duff曾在他的關于rc的文章(the Plan 9 shell)中說過,shell的一個歷史遺留問題是沒有人真正知道什么是Bash的語法。Posix的shell委員會最終還是發布一個Unix Shell的標準語法,雖然這個語法仍然存在大量的上下文依賴關系,而且還不能兼容以前的一些代碼,但無疑這個標準是目前最好的,它的發布值得贊賞。 Bash解析器源于早期版本的Posix語法,而且,據我所知,它也是唯一一個使用Yacc或Bison實現的Bourne風格的shell。這帶來了一些麻煩,shell語法本身并不特別適合yacc風格的語法解析,需要采用一些復雜的語法分析,而且解析器和詞法分析器之間也要通力合作。 執行過程中,詞法分析器從readline或其它輸入來源中獲取字符行,根據元字符將它們分割成記號(token),并根據上下文定位這些記號,然后將其傳給解析器組合成語句和命令。這個過程中涉及了大量的上下文,比如,獲得的單詞可能是保留字,可能是標識符,可能是賦值語句的一部分,也可能是其它單詞。下面是一個完全合法的命令: ~~~ for for in for; do for=for; done; echo $for ~~~ 這個命令的作用是打印`"for"`這個字符串。 在這里順便簡單介紹一下別名(aliasing)。Bash支持使用任意文本(稱為別名alias)替換簡單命令的第一個單詞(word),這個替換過程完全是基于文本的。因而別名可以改變shell的語法,有的別名甚至實現了一個Bash不支持的復合命令。別名完全是在Bash解析器的詞法分析階段實現的,而解析器必須告訴分析器(analyzer)什么時候允許展開別名。 和許多編程語言一樣,shell支持字符的轉義,用來改變字符的原有含義,使得一些元字符(如`&`)可以出現在命令中。Bash中三種類型的引用,相互之間稍有不同,對包含的文本的解析方式也不盡相同。第一種是反斜劃線(backslash),用來轉義后面的一個字符。第二種是單引號,它禁止對包含的文本進行解析。第三種是雙引號,它阻止部分解析,但是允許一些單詞(word)的展開(處理反斜劃線的方式也不相同)。詞法分析器解譯被引用的字符和字符串,防止它們被解析為保留字或元字符。但有兩個例外情況,`$'...'`和`$"..."`,它們處理轉義字符的方式和ANSI C一樣,允許使用標準國際化函數來處理。前者使用更為廣泛,后者則因為案例太少而不為人知。 從解析器(parser)到分析器(analyzer)剩下的工作就非常直接了。解析器對一些狀態進行編碼,然后共享給分析器以支持依賴上下文的語法分析。比如,詞法分析器根據記號的類型將單詞(word)分類為:保留字(依賴上下文),單詞,賦值語句,等等。為實現這個功能,解析器提供解析命令的進度信息,比如是否正在處理一個多行文本(有時候稱為here-document),或者一個`case`語句或條件命令,或者shell的擴展模式(extended shell pattern)或復合賦值語句。 在解析階段,識別命令替換結點位置的大部分工作被封裝在了一個函數中(`parse_comsub`),這個函數能夠處理大量的shell語法,并且包含了大量的冗余的讀取記號的代碼。而且,`parse_comsub`函數必須要知道here-document,shell命令,元字符和單詞(word)的邊界,引用(quoting),以及什么時候允許保留字存在(比如,識別`case`語句中的保留字)。正確完成這些工作是比較費時的。 在單詞展開過程中展開一個命令替換,Bash使用解析器來獲得這個結構正確的結束位置。這和`eval`命令將字符串變成命令的過程有些類似,但不同的是命令的結束位置并不在字符串的結尾。為了實現這個功能,解析器必須將右括號視為一個合法的命令終止符,這又導致了大量特殊情況的出現,并且要求詞法分析器(在恰當的上下文中)將右括號標記為EOF。在遞歸觸發`yyparse`之前,解析器還必須能夠保存和恢復解析器狀態,因為在讀取一條命令的中途,一個命令替換的解析和執行可能是展開一個提示字符過程的一部分。因為輸入函數實施了預讀(輸入來源可能是字符串,文件,或使用readline的終端),這個函數必須最后能將輸入指針(input pointer)恢復到正確的位置。這一點非常重要,不僅保證了輸入不會丟失,而且使得命令替換的展開函數能構造出正確的用于執行的字符串。 可編程的單詞展開也存在類似的問題,因為在解析一個命令時允許執行其它任意命令,解決方案是在執行前保存狀態,在執行后恢復狀態。 引用(quoting)是另外一個不兼容性和爭論的來源。在Posix的shell標準發布二十年之后,標準工作組成員們仍然在爭論引用的那些難以理解的行為是否合理。但是Bash一直都只是一個實現上的參考,對制訂標準是無能為力的。 解析器返回一個C結構體來表達一個命令(對于復合命令,這個結構體中可能還包含有其它命令),然后將其傳遞給shell的下一個階段:單詞展開。命令結構體由一系列命令對象和單詞列表組成。大部分的單詞列表對應于各種變換,其意義隨上下文的變化而變化(下面會有解釋)。 ## 3.5\. 單詞展開 在解析階段之后,在執行階段之前,解析階段產生的許多單詞對應于一個或多個單詞展開,比如`$OSTYPE`可以展開為"linux-gnu"。 **3.5.1\. 參數和變量展開** 變量展開是用戶最熟悉的。shell變量幾乎都沒有類型(少數例外),都被視為字符串。展開就是將這些字符串展開和轉變為新的單詞和單詞列表。 有的展開可以作用于變量的值上。編程人員可以獲得變量值的子字符串,獲得值的長度,刪除匹配串首或串尾的子串,根據字符串匹配替換子串,或者修改值中的大小寫。 還有一些展開依賴于變量的狀態:變量是否已經設置會導致不同的展開或賦值行為。比如,若`parameter`已經設置(并且不為空),`${parameter:-word}`會被展開為`parameter`,否則展開為`word`。 **3.5.2\. 其他展開** Bash還支持其他多種展開,它們的規則都很詭異。處理過程中,最優先的展開是括號展開(brace exapansion),括號展開把 ~~~ pre{one,two,three}post ~~~ 轉變為 ~~~ preonepost pretwopost prethreepost ~~~ 命令替換(command substitution)是shell執行命令與操縱變量的完美結合。shell運行一個命令,收集其輸出,然后將輸出作為展開的值。 命令替換的一個問題在于命令的立即執行然后等待結果,此過程中shell無法傳入輸入。Bash使用一個稱為進程替換(process substitution)的功能來彌補這些不足,進程替換實際上是命令替換和管道的組合。和命令替換類似,Bash運行一個命令,但令其運行于后臺而不再等待其完成。關鍵在于Bash為這條命令打開了一個用于讀和寫的管道,并且綁定到一個文件名,最后展開為結果。 下一個展開是波浪符展開(tilde expansion)。起初,`~alan`記號只是表示對Alan主目錄的引用,多年后,這卻漸漸變成了引用多個不同目錄的一種方式。 最后是算術展開(arithmetic expansion)。`$((expression))`會執行`expression`表達式,其規則與C語言表達式一致。`expression`的計算結果變成展開的結果。 單引號和雙引號之間最明顯的差異在于變量展開(variable expansion)。單引號禁止所有展開,被包含的字符全部原封不動,而雙引號則允許部分展開。單詞展開(word expansion),命令展開,算術展開,進程替換都可以在雙引號中進行(雙引號只處理其結果),而括號展開和波浪符展開則被禁止。 **3.5.3\. 單詞拆分** 單詞展開的結果會被拆分(split),拆分的限定符來自于shell變量`IFS`的值。shell使用這種方式將單個單詞拆分成多個。每當`$IFS`1中的一個字符在結果中出現,Bash就會將單詞一分為二。單引號和雙引號中禁止單詞拆分。 **3.5.4\. 名稱替換(globbing)** 結果拆分之后,shell將逐個處理之前展開得到的單詞,使用一個模式來匹配已知的文件名,包括一些主要的文件目錄。 **3.5.5\. 實現** 如果shell的基本架構將流水線(pipeline)并行化,單詞展開則是它自身的管道。單詞展開的每個階段處理一個單詞,經過可能的轉換,然后將其傳遞給下一個展開階段。等到所有的單詞展開都已執行,命令就可以開始執行了。 Bash的單詞展開的實現基于前面所說的基本數據結構。對解析器輸出的單詞逐個展開,由一個單詞獲得一個或多個單詞。`WORD_DESC`是一個通用的數據結構,足以封裝單個單詞展開所需的所有信息。其中,`flags`可用于編碼單詞展開階段的信息,并把信息傳遞給下一個階段。例如,解析器使用一個標志位記錄某個單詞是一個賦值語句,并通知展開階段和命令執行階段,而單詞展開代碼內部也使用標志位來禁止單詞拆分或標記空字符串(比如`"$x"`,其中`$x`未定義或為空值)。使用單個字符串來表達展開后的單詞,并且用某種字符編碼來表達其它信息,實現起來并不容易。 對于解析器來說,單詞展開的代碼還需要處理多字節字符。比如,變量長度展開(`${#variable}`)用于計算字符數目而非字節數,這些代碼能正確識別展開的結束位置或多字節字符存在情況下特殊字符的展開。 ## 3.6\. 命令執行 Bash內部流水線到命令執行階段才真正開始執行命令。大部分時候,展開后的單詞集合被分解為命令名稱和參數集合,然后作為文件傳遞給操作系統被讀入和執行,而余下的單詞則作為`argv`剩下的部分傳遞給操作系統。 到目前為止,描述的主要是簡單命令(命令名加上一些參數)的執行,它們是最常用的類型,但是Bash還提供了更多的功能。 命令執行階段的輸入是一些解析器創建的命令結構,以及一些展開單詞的集合。這是Bash編程語言真正開始起作用的時候。編程語言使用變量和前面提到的展開來實現高級語言中常見的結構:循環(looping),條件(conditionals),替換(alternation),分組(grouping),選擇(selection),基于模式匹配的條件執行,表達式求值,以及其它一些shell特有的結構。 **3.6.1\. 重定向** shell作為操作系統界面的表現之一是能夠將輸入和輸出重定向到它所調用的命令上。重定向的語法反映了shell早期用戶的復雜操作:直到最近,用戶仍然需要跟蹤正在使用的文件描述符(file descriptor),并且顯式地用數字來指定(除了標準輸入,標準輸出和標準錯誤)。 重定向語法最近增加的一個功能是允許用戶將shell定向到一個合適的文件描述符,然后賦值給一個指定的變量,因而用戶不需要再去選擇文件描述符。這個功能減少了程序員跟蹤文件描述符的負擔,但是增加了額外的處理過程:shell必須將文件描述符復制到正確的位置,還要確保它們賦值給指定的變量。這是又一個從詞法分析器到解析器再到命令執行的信息傳遞過程的例子:分析器找出包含變量賦值的重定向,解析器生成重定向對象并用一個標志位標記需要賦值,然后重定向代碼根據這個標志位確保文件描述符數字被賦值到正確的變量上。 實現重定向最難的地方是記得如何撤銷重定向。shell有意不區分文件命令和內置命令,但是,無論是哪種命令,重定向的影響范圍不應該超過命令結束的時候2。因此,shell必須跟蹤重定向,保證可以將其撤銷,否則,內置命令的輸出重定向會改變shell的標準輸出。Bash知道如何撤銷每一種重定向,要么關閉之前分配的文件描述符,要么先保存文件描述符之后再使用`dup2`恢復。這些過程使用的重定向對象和解析器創建的重定向對象是一樣的,并且用同樣的函數來處理。 因為多重重定向使用簡單對象列表來實現,用來撤銷的那些重定向也保存在一個單獨的列表中。這個列表在命令完成時進行處理,但是shell必須確認處理的時機,因為綁定到shell函數或"`.`"內置命令上的重定向必須在它們執行完之前都一直有效。如果沒有命令被執行,內置命令`exec`將直接丟棄這個撤銷列表,因為關聯到`exec`上的重定向在整個shell環境中都是有效的。 另外一個復雜的地方來自Bash本身。Bourne shell的歷史版本只允許用戶操縱文件描述符0-9,而把10和10以上保留為內部使用。后來,Bash放松了這個限制,允許用戶在不超過進程文件打開數限制的條件時使用任意文件描述符。這意味著Bash必須跟蹤內部的文件描述符,包括那些不是直接由shell打開而是由外部庫打開的那些文件描述符,這些內部文件描述符還可能時不時被移動。因此,大量的文件描述符需要跟蹤,有的使用啟發式的close-on-exec標志位,有的重定向列表在整個命令執行過程中都需要維護,然后在執行完成時處理或丟棄。 **3.6.2\. 內置命令** Bash中包含了大量的內置命令,這些命令由shell執行,但不會創建新進程。 使用內置命令最普遍的原因是維護和修改shell內部狀態。比如`cd`命令就是一個Unix世界中的經典案例,`cd`命令需要修改當前工作目錄這個內部狀態,因此不能用外部命令來實現。 Bash內置命令與shell其他部分使用相同的原語(primitive)。每個內置命令都是用C語言函數來實現的,以單詞列表作為其參數。這些單詞來自于單詞展開階段,內置命令將其視為命令名和參數。大部分情況下,內置命令使用標準的展開規則,但也有一些例外:Bash內置命令接受賦值語句作為其參數時(比如`declare`和`export`),賦值參數的展開規則與尋常變量賦值時的展開一致。`WORD_DESC`結構體中的`flags`成員在這個地方也會發揮作用,在shell內部流水線(pipeline)不同階段之間傳遞信息。 **3.6.3\. 簡單命令執行** 簡單命令是shell中最常遇到的命令。其過程無非是:搜索和執行文件命令,收集退出狀態,簡單命令覆蓋了shell當前大部分功能。 shell的變量賦值(形如`var=value`的單詞)本身也是一種簡單命令。賦值語句可以置于其它命令之前,也可以單獨成一行。如果置于命令前,變量將傳遞給該命令的局部環境(如果這個命令是內置命令或shell函數,除少數例外情況,變量賦值都將起作用)。如果單獨成一行,賦值語句將修改shell狀態的值。 當出現一個既不是shell函數又不內置命令的命令名稱,Bash會搜索文件系統來尋找同名的可執行文件。`PATH`環境變量的值是一些由冒號分隔的目錄列表,Bash根據這些目錄來搜索文件命令。命令名稱中若包含斜劃線(或其它目錄分隔符),Bash將直接執行此命令,而不會再搜索。 當一個命令通過`PATH`搜索得到,Bash將命令名稱和對應的完整路徑存于哈希表中,`PATH`搜索之前會查詢這個表來提高效率。如果命令沒有被搜到,Bash會執行一個特殊的函數,以命令名稱和參數作為這個函數的參數。一些Linux發行版通過這個方式來提示用戶安裝缺失的命令。 如果Bash搜索并執行一個文件命令,它會創建一個新的執行環境并`fork`出一個新的進程,然后讓此可執行文件在新的環境中執行。執行環境完全從shell環境復制而來,只是在信號處理和重定向的文件打開關閉等方面有少許修改。 **3.6.4\. 任務控制** shell在前臺執行命令時,必須等待命令返回才能執行下一條命令,但shell還可以在后臺執行命令,然后立刻讀入下一條命令。任務控制(Job control)指在前臺和后臺之間切換進程(正在執行的命令),以及掛起(suspend)和恢復(resume)執行的能力。為實現這個功能,Bash引入了任務(job)的概念,任務本質上指的是被一個或多個進程執行的某個命令。比如,管道為每個管道元素創建一個進程,它們共同構成一個任務。進程組可以將多個分離的進程組合成單個任務。終端綁定了一個進程組ID,這同時也是前臺進程組的ID。 shell使用一些簡單的數據結構來實現任務控制。用一個結構體就能表達一個子進程,包括了進程ID,狀態和終止時的返回狀態。管道只不過是這個進程結構體的簡單鏈表。任務(job)的結構更加簡單,包括進程列表,一些任務狀態(運行,掛起,退出,等等),以及任務進程組的ID。進程列表通常只有一個進程,只有管道才會導致一個任務包含多個進程。如果一個進程ID和任務進程組ID相同,則此ID成為這個進程組的領導(leader)。當前任務集合存于一個數組中,這種實現方式在概念上和實際的表現形式是比較接近的。任務的狀態和退出狀態由其成員進程的相應狀態組合而成。 和shell中其他部分一樣,實現任務控制的復雜性在于登記(bookkeeping)。shell必須把進程歸入正確的進程組中,并保證子進程的創建也要同步進行。因為終端的進程組決定了前臺任務,終端的進程組必須要設置正確(如果前臺任務沒有歸入shell的進程組,那么shell將無法讀入終端輸入)。因為任務如此依賴于進程,實現復雜命令并不是那么顯然,比如`while`和`for`循環可以作為一個整體來停止或啟動,其他shell很少能做到這一點。 **3.6.5\. 復合命令** 復合命令包含了一個或多個簡單的命令,里面往往還帶有一些關鍵字(比如`if`或`while`)。這保證了shell的強大編程能力。 復合命令的實現沒有什么特別的地方。解析器為各個命令構造相應的對象,然后遍歷這些對象。每個復合命令都使用一個C函數來實現,它的功能包括執行恰當的展開,執行特定的命令,根據命令的返回值來變更執行流程。以實現`for`命令的函數為例。首先它會展開`in`關鍵字后面的單詞列表,該函數隨后遍歷這些單詞,在`for`循環中賦給相應的變量并執行。`for`命令的執行不會受子命令返回值得影響,但是`break`和`continue`這兩個內置命令會改變循環的執行過程。一旦列表中所有的單詞都已經處理完,`for`命令就會返回。從這個例子可以看出來,命令的實現和語法的描述是非常相近的。 ## 3.7\. 經驗教訓 **3.7.1\. 什么是重要的** 參與到Bash項目中已經有20多年,在這期間我也獲益良多。最重要的一點是一定要保留詳細的修改日志,其重要性怎么強調都不過份。通過閱讀修改日志來回憶起當初的想法,感覺是很好的。甚至你還可以將某個修改與一個bug報告聯系起來,然后編寫一個重現bug的測試用例或提出一些建議。 如果條件允許,我建議在項目之初就考慮全面的回歸測試。Bash擁有數千個測試用例,覆蓋了差不多所有的非交互性功能。我考慮過測試交互式功能,其實Posix標準的一致性測試套件中就有交互性測試,只是并沒有將這個測試框架發布出來(我認為很有必要)。 標準很重要,Bash受益于它是一個標準的實現。參與到你正在實現的軟件的標準化過程中來也是非常重要的。在討論相關功能和行為時,標準往往是最終的參考依據。當然,前提是這是一個好的標準。 外部標準重要,內部標準同樣重要。我很幸運地接觸到了GNU項目的諸多標準,它們包含了大量關于設計和實現方面的好且實用的建議。 好的文檔同樣非常關鍵。如果你希望別人使用你的軟件,全面并清晰的文檔就是必要的。一個成功的軟件必須擁有大量的文檔,因而開發者提供權威的版本就顯得非常重要。 優秀的軟件隨處可見,那就充分利用起來吧。比如,gnulib中包含了大量的有用的函數,你盡可以把它們"摳"出來。BSD各個版本和Mac OS X就是這么干的。Picasso說過:好的藝術家靠的是偷,說的就是這個道理。 參與用戶社區,但是準備挨罵,有時候這并不好受。活躍的用戶社區好處是顯然的,但是這些人可能會非常情緒化,不要太當真就好。 **3.7.2\. 如果可以重來** Bash擁有數百萬用戶,我知道后向兼容有多么地重要。在某種意義上,后向兼容意味著永遠不用向用戶說抱歉。但是,這個世界遠不是這么簡單。事實上,我不得不一直做一些破壞兼容性的修改,比如修正一個不好的決定,修改一個錯誤的設計,或者更正shell不同部分之間的不兼容性,這些都是情有可原的修改,但是幾乎都會引起一些用戶的抱怨。我早就應該對當兼容性分級處理的。 Bash的發展一直都沒有特別的開放,我已經習慣于里程碑發布形式(比如 bash-4.2)并由個人提交補丁。我的理由是:我需要適應開發商們更長的發布周期(相對于自由軟件和開源世界),而且我也有過beta版本傳播地過于廣泛的不快回憶。當然,如果一切都要重來,我還是會考慮更快的發布頻率,比如可以使用一個公開的源碼倉庫。 不真正動手去做是完不成任何事的。有一件事我已經考慮了很久,卻一直沒去做,那就是將Bash的解析器重寫為遞歸下降(recursive-descent)的方式,以取代bison。以前,我以為為了遵守Posix標準,這件事就非得做,但是后來我只需要少量修改就解決了這個問題。如果當時就從頭寫起,大概現在我已經實現了一個新的解析器,那么很多問題都會變得簡單得多。 ## 3.8\. 結論 Bash是一個優秀和復雜的大型免費軟件。經歷了超過二十年的良性發展,已經變得成熟和強大。現在,Bash幾乎遠處不在,數百萬人每天都在使用,雖然有些人可能還渾然不知。 從Stephen Bourne寫出第七版Unix中的shell開始,Bash就一直受到多方的影響。其中最重要的影響來自于Posix標準,它形成了shell很重要的一部分行為。然而兼容性和標準化難以兩全,實現起來是非常頭痛的事。 Bash受益于GNU項目,因為GNU項目提供了一個Bash賴以生存的開發生態環境。沒有GNU就不會有Bash。Bash還得益于活躍并朝氣蓬勃的用戶社區,用戶的反饋成就了今天的Bash,這也是自由軟件的精髓所在。 ## 術語表 * alternation: 某種編程語言中的結構,翻譯成替換。在Perl中alternation指一個字符串集合中的任選一個。 * word: 指shell中的的一個字符串(以空格等非打印字符分隔),翻譯成單詞。根據上下文的不同,word具有兩種含義。廣義上講,所有連續的可打印字符串都可以視為word,但具體到shell中,word又可指狹義的不含特殊字符的字符串。兩種含義在文中都有出現,但以后者為主。 * expansion: 展開。類似于變量取值,將包含特殊字符和結構的字符序列經過一次或多次替換得到一個新的字符串 * pipe/pipeline: 管道, 將一些命令串連起來(用`|`符號),前一個命令的輸出構成后一個命令的輸出。另外一種語義是:shell的內部流程,翻譯成流水線。 * 數據與命令: 文中這兩個概念容易混淆。因為所有的shell命令或腳本都是以字符串形式存在,因此都可以視為shell解釋器的輸入數據。但是,這些shell命令或腳本從編程語言的角度來看,又是以語句為基本單元的,這樣的語句可被稱為命令。而shell中最基本的命令則包括內置命令和文件命令, 從這個意義上看, 一個語句包含了命令和命令的參數,而這些參數又可視為命令的數據。 * primitive: 原語 * token: 記號 * metacharacter: 元字符 * builtin: 內置命令 * parser: 解析器 * lexical analysis: 詞法分析 * local: 本地設置 * prompt: 提示符 **腳注** 1. 大部分情況下, 只包含了一個這樣的字符. 2. 內置命令`exec`是這條規則的例外情況
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看