Linux 桌面玩家指南:06. 優雅地使用命令行及 Bash 腳本編程語言中的美學與哲學

原文: Linux 桌面玩家指南:06. 優雅地使用命令行及 Bash 腳本編程語言中的美學與哲學

特別說明:要在個人隨筆後寫評論的小夥伴們請注意了,個人博客開啓了 MathJax 數學公式支持,MathJax 使用$標記數學公式的開始和結束。若是某條評論中出現了兩個$,MathJax 會將兩個$之間的內容按照數學公式進行排版,從而致使評論區格式混亂。若是你們的評論中用到了$,可是又不是爲了使用數學公式,就請使用\$轉義一下,謝謝。html

想從頭閱讀該系列嗎?下面是傳送門:linux

前言

雖然咱們玩的是 Linux 桌面系統,可是不少時候咱們仍然離不開命令行。有時候,是由於某些工具只有命令行版本,要解決某些問題必須使用命令行,特別是對於咱們程序猿和系統管理員來講更是這樣。有時候,是由於使用命令行解決問題確實比使用圖形界面更加高效。還有些時候,爲了自動化、批量化運行程序,咱們也不得不使用命令行。得益於 Unix 系統的傳統,在命令行中使用管道和文件重定向以及 Shell 腳本語言做爲粘合劑,能夠將許多簡單的工具組合到一塊兒完成更加複雜的任務。總之,Linux 系統中的命令行是至關舒服和優雅的。程序員

我這裏使用的終端程序就是 Gnome 3 桌面自帶的 gnome-terminal,而我使用的 Shell 就是 Bash。網上有不少人推崇 Z Shell,可是我並無改弦易轍,而是堅持使用 Bash。我認爲,Bash 的功能也是很強大的,只是我本身水平有限,不能發揮出它所有的威力而已。關於高效使用命令行這個話題,在網上已是老生常談了。我這裏主要的參考資料是 Bash 的官方文檔,使用man bash便可以閱讀,固然也能夠到 Bash 的官網上下載 pdf 版的文檔,放到手機上有空的時候慢慢看。在本文中,也有很多我本身的觀點和體會,我會提到有些快捷鍵要熟記,有些則徹底不須要記,畢竟咱們的記憶力也是有限的,我還會提到一些助記的方法。因此,本文絕對不是照本宣科,值得你們擁有,請你們必定記得點贊。正則表達式

四年前,我腦子一抽,寫了一篇 Bash 腳本編程語言中的美學與哲學,還很是洋洋得意。如今回看起來,以爲仍是幼稚了一些。可是我以爲我寫的這些也不是徹底沒有幫助,相比於長達 171 頁的詳細的 Bash 官方文檔,也許我對 Bash 腳本編程語言的定位——面向字符串的編程語言——更能讓你們理解記住並熟練使用命令行呢。shell

使用 tmux 複用控制檯窗口

高效使用命令行的首要原則就是要儘可能避免干擾,什麼意思呢?就是說一但開啓了一個控制檯窗口,就儘可能不要再在桌面上切換來切換去了,不要一下子被別的窗口擋住控制檯,一下子又讓別的窗口破壞了控制檯的背景,最好是把控制檯最大化或全屏,甚至連鼠標都不要用。可是在實際工做中,咱們又常常須要同時在多個控制檯窗口中進行工做,例如:在一個控制檯窗口中運行錄製屏幕的命令,在另一個控制檯窗口中工做;或者在一個控制檯窗口中工做,在另一個控制檯窗口中閱讀文檔。若是既想在多個控制檯窗口中工做,又不想一大堆窗口擋來擋去、換來換去的話,就能夠考慮試試 tmux 了。以下圖:
express

tmux 的功能不少,什麼 Session 啊、Atach 啊、Detach 啊等功能都很是強大。可是咱們暫時不用去關心這些,只把重點放在它的控制檯窗口複用功能上就好了。tmux 中有 window 和 pane 的概念,tmux 能夠建立多個 window,這些 window 是不會互相遮擋的,每次只顯示一個 window,其它的 window 會自動隱藏,可使用快捷鍵在 window 之間切換。同時,能夠把一個 window 切分紅多個 pane,這些 pane 同時顯示在屏幕上,可使用快捷鍵在 pane 之間切換。編程

tmux 的快捷鍵不少,要想全面瞭解 tmux 的最好辦法固然是使用man tmux命令閱讀 tmux 的文檔。可是咱們只須要記住少數幾個重要的快捷鍵就能夠了,以下表:vim

快捷鍵 功能
Ctrl+B c 建立一個 window
Ctrl+B [n][p] 切換到下一個窗口或上一個窗口
Ctrl+B & 關閉當前窗口
Ctrl+B " 將當前 window 或 pane 切分紅兩個 pane,上下排列
Ctrl+B % 將當前 window 或 pane 切分紅兩個 pane,左右排列
Ctrl+B x 關閉當前 pane
Ctrl+B [↑][↓][←][→] 在 pane 之間移動
Ctrl+[↑][↓][←][→] 調整當前 pane 的大小,一次調整一格
Alt+[↑][↓][←][→] 調整當前 pane 的大小,一次調整五格

tmux 的快捷鍵比較特殊,除了調整 pane 大小的快捷鍵以外,其它的都是先按 Ctrl+B,再按一個字符。先按 Ctrl+B,再按 c,就會建立一個 window,這裏 c 就是 create window。先按 Ctrl+B,再按 n 或者 p,就能夠在窗口之間切換,它們是 next window 和 previous window 的意思。關閉窗口是先按 Ctrl+B,再按 &,這個只能死記。先按 Ctrl+B,再按 " ,表示上下拆分窗口,能夠想象成單引號和雙引號在鍵盤上是上下鋪關係。先按 Ctrl+B,再按 % 表示左右拆分窗口,大概是由於百分數都是左右書寫的吧。至於在 pane 之間移動和調整 pane 大小的方向鍵,就不用多說了吧。數組

在命令行中快速移動光標

在命令行中輸入命令時,常常要在命令行中移動光標。這個很簡單嘛,使用左右方向鍵就能夠了,可是有時候咱們輸入了很長一串命令,卻忽然要修改這個命令最開頭的內容,若是使用向左的方向鍵一個字符一個字符地把光標移到命令的開頭,是否太慢了呢?有時咱們須要直接在命令的開頭和結尾之間切換,有時又須要可以一個單詞一個單詞地移動光標,在命令行中,其實這都不是事兒。以下圖:
bash

這幾種移動方式都是有快捷鍵的。其實一個字符一個字符地移動光標也有快捷鍵 Ctrl+B 和 Ctrl+F,可是這兩個快捷鍵咱們不須要記,有什麼能比左右方向鍵更方便的呢?咱們真正要記的是下面這幾個:

快捷鍵 功能
Ctrl + A 將光標移動到命令行的開頭
Ctrl + E 將光標移動到命令行的結尾
Alt + B 將光標向左移動一個單詞
Alt + F 將光標向右移動一個單詞

這幾個快捷鍵太好記了,A 表明 ahead,E 表明 end,B 表明 back,F 表明 forward。爲何按單詞移動光標的快捷鍵都是以 Alt 開頭呢?那是由於按字符移動光標的快捷鍵把 Ctrl 佔用了。可是按字符移動光標的快捷鍵咱們用不到啊,由於咱們有左右方向鍵啊。

在命令行中快速刪除文本

對輸入的內容進行修改也是咱們常常要乾的事情,對命令行進行修改就涉及到先刪除一部份內容,再輸入新內容。咱們碰到的狀況是有時候只須要修改個別字符,有時候須要修改個別單詞,而有時候,輸入了半天的很長的一段命令,咱們說不要就全都不要了,要整行刪除。經常使用的刪除鍵固然是 BackSpace 和 Delete 啦,不過一次刪除一個字符,仍是太慢了些。要在命令行中快速刪除文本,請熟記如下幾個快捷鍵吧:

快捷鍵 功能
Ctrl + U 刪除從光標到行首的全部內容,若是光標在行尾,天然就整行都刪除了啊
Ctrl + K 刪除從光標到行尾的全部內容,若是光標在行首,天然也是整行都刪除了啊
Ctrl + W 刪除光標前的一個單詞
Alt + D 刪除光標後的一個單詞
Ctrl + Y 將剛刪除的內容粘貼到光標處,有時候刪錯了能夠用這個快捷鍵恢復刪除的內容

效果請看下圖:

這幾個快捷鍵也是蠻好記的,U 表明 undo,K 表明 kill,W 表明 word,D 表明 delete, Y 表明 yank。其中比較奇怪的是 Alt+D 又是以 Alt 開頭的,那是由於 Ctrl+D 又被佔用了。Ctrl+D 有兩個意思,一是在編輯命令行的時候它表明刪除一個字符,固然,這個快捷鍵其實咱們用不到,由於 BackSpace 和 Delete 方便多了;二是在某些程序從 stdin 讀取數據的時候,Ctrl+D 表明 EOF,這個咱們偶爾會用到。

快速查看和搜索歷史命令

對於曾經運行過的命令,除非特別短,咱們通常不會重複輸入,從歷史記錄中找出來用天然要快得多。咱們用得最多的就是 ↑ 和 ↓,特別是不久前纔剛剛輸入過的命令,使用 ↑ 向上翻幾行就找到了,按一下 Enter 就執行,多舒服。可是有時候,明明記得是不久前才用過的命令,可是向上翻了半天也沒找到,怎麼辦?那隻好使用history命令來查看全部的歷史記錄了。歷史記錄又特別長,怎麼辦?可使用 history | lesshistory | grep '...'。除此以外,還有終極大殺招,那就是按 Ctrl+R 從歷史記錄中進行搜索。按了 Ctrl+R 以後,每輸入一個字符,都會和歷史記錄中進行增量匹配,輸入得越多,匹配越精確。固然,有時候含有相同搜索字符串的命令特別多,怎麼辦?繼續按 Ctrl+R,就會繼續搜索下一條匹配的歷史記錄。以下圖:

這裏,須要記住的命令和快捷鍵以下表:

命令或快捷鍵 功能
history 查看歷史記錄
history | less 分頁查看歷史記錄
history | grep '...' 在歷史記錄中搜索匹配的命令,並顯示
Ctrl + R 逆向搜索歷史記錄,和輸入的字符進行增量匹配
Esc 中止搜索歷史記錄,並將當前匹配的結果放到當前輸入的命令行上
Enter 中止搜索歷史記錄,並將當前匹配的結果當即執行
Ctrl + G 中止搜索歷史記錄,並放棄當前匹配的結果
Alt + > 將歷史記錄中的位置標記移動到歷史記錄的尾部

這裏須要注意的是,當咱們在歷史記錄中搜索的時候,是有位置標記的,Ctrl+R 是指從當前位置開始,逆向搜索,R 表明的是 reverse,每搜索一條記錄,位置標記都會向歷史記錄的頭部移動,下次搜索又從這裏開始繼續向頭部搜索。因此,咱們必定要記住快捷鍵 Alt+>,它能夠把歷史記錄的位置標記還原。另外須要注意的是中止搜索歷史記錄的快捷鍵有三個,若是按 Enter 鍵,匹配的命令就當即執行了,若是你還想有修改這條命令的機會的話,必定不要按 Enter,而要按 Esc。若是什麼都不想要,就按 Ctrl+G,它會還你一個空白的命令行。

快速引用和修飾歷史命令

除了查看和搜索歷史記錄,咱們還能夠以更靈活的方式引用歷史記錄中的命令。常見的簡單的例子有!!表明引用上一條命令,!$表明引用上一條命令的最後一個參數,^oldstring^newstring^表明將上一條命令中的 oldstring 替換成 newstring。這些操做是咱們平時使用命令行的時候的一些經常使用技巧,其實它們的本質,是由 history 庫提供的 history expansion 功能。Bash 使用了 history 庫,因此也能使用這些功能。其完整的文檔能夠查看man history手冊頁。知道了 history expansion 的理論,咱們還能夠作一些更加複雜的操做,以下圖:

引用和修飾歷史命令的完整格式是這樣的:

![!|[?]string|[-]number]:[n|x-y|^|$|*|n*|%]:[h|t|r|e|p|s|g]

能夠看到,一個對歷史命令的引用被 : 分爲了三個部分,第一個部分決定了引用哪一條歷史命令;第二部分決定了選取該歷史命令中的第幾個單詞,單詞是從0開始編號的,也就是說第0個單詞表明命令自己,第1個到最後一個單詞表明命令的參數;第三部分決定了對選取的單詞如何修飾。下面我列出完整表格:

表格1、引用哪一條歷史命令:

操做符 功能
! 全部對歷史命令的引用都以 ! 開始,除了 ^oldstring^newstring^ 形式的快速替換
!n 引用第 n 條歷史命令
!-n 引用倒數第 n 條歷史命令
!! 引用上一條命令,等於 !-1
!string 逆向搜索歷史記錄,第一條以 string 開頭的命令
!?string[?] 逆向搜索歷史記錄,第一條包含 string 的命令
^oldstring^newstring^ 對上一條命令進行快速替換,將 oldstring 替換爲 newstring
!# 引用當前輸入的命令

表格2、選取哪個單詞:

操做符 功能
0 第0個單詞,在 shell 中就是命令自己
n 第n個單詞
^ 第1個單詞,使用 ^ 時能夠省略前面的冒號
$ 最後一個單詞,使用 $ 時能夠省略前面的冒號
% 和 ?string? 匹配的單詞,能夠省略前面的冒號
x-y 從第 x 個單詞到第 y 個單詞,-y 表明 0-y
* 除第 0 個單詞外的全部單詞,等於 1-$
x* 從第 x 個單詞到最後一個單詞,等於 x-$,能夠省略前面的冒號
x- 從第 x 個單詞到倒數第二個單詞

表格3、對選取的單詞作什麼修飾:

操做符 功能
h 選取路徑開頭,不要文件名
t 選取路徑結尾,只要文件名
r 選取文件名,不要擴展名
e 選取擴展名,不要文件名
s/oldstring/newstring/ 將 oldstring 替換爲 newstring
g 全局替換,和 s 配合使用
p 只打印修飾後的命令,不執行

這幾個命令其實挺好記的,h 表明 head,只要路徑開頭不要文件名,t 表明 tail,只要路徑結尾的文件名,r 表明 realname,只要文件名不要擴展名,e 表明 extension,只要擴展名不要文件名,s 表明 substitute,執行替換功能,g 表明 global,全局替換,p 表明 print,只打印不執行。有時候光使用 :p 還不夠,咱們還能夠把這個通過引用修飾後的命令直接在當前命令行上展開而不當即執行,它的快捷鍵是:

操做符 功能
Ctrl + Alt + E 在當前命令行上展開歷史命令引用,展開後不當即執行,能夠修改,按 Enter 後纔會執行
Alt + ^ 和上面的功能同樣

這兩個快捷鍵,記住一個就行。這樣,當咱們對歷史命令的引用修飾完成後,能夠先展開來看一看,若是正確再執行。眼見爲實嘛,反正我是每次都展開看看才放心。

使用 Tab 鍵進行補全

在使用命令行的時候,可使用 Tab 鍵對命令和文件名進行補全。通常若是你輸入一條命令的前面幾個字符後,按 Tab 鍵兩次,將會提示全部可用的命令。輸入命令後,在輸入參數的位置,若是輸入了一個文件名的前幾個字符,按 Tab 鍵,Shell 會查找當前目錄下的文件,對文件名進行補全。或者在輸入參數的位置直接按兩次 Tab 鍵,將提示全部可用的文件名。效果以下:

快速切換當前目錄

在使用命令行時,可使用cd命令切換當前目錄,可是,若是每次都輸入一個超長的目錄名,則會嚴重影響效率,特別是在多個目錄之間快速切換的時候。例如,在我前面幾篇中,常常須要進入/usr/share/backgrounds/contest目錄和/etc/fonts/conf.d目錄查看配置文件,也會進入/usr/src/linux-source-4.15.0目錄查看內核源代碼,這些目錄名都比較長,若是每次都本身輸入,效率低不說,還容易出錯。這時,能夠經過 Bash 提供的pushd命令和popd命令維護一個目錄堆棧,並使用dirs命令查看目錄堆棧,使用pushd命令在目錄之間切換。效果以下圖:

這三個命令的具體參數以下:

一、dirs——顯示當前目錄棧中的全部記錄(不帶參數的dirs命令顯示當前目錄棧中的記錄)

格式:dirs  [-clpv]  [+n]  [-n]
選項
-c    刪除目錄棧中的全部記錄
-l     以完整格式顯示
-p    一個目錄一行的方式顯示
-v    每行一個目錄來顯示目錄棧的內容,每一個目錄前加上的編號
+N  顯示從左到右的第n個目錄,數字從0開始
-N   顯示從右到左的第n個日錄,數字從0開始

二、pushd——pushd命令經常使用於將目錄加入到棧中,加入記錄到目錄棧頂部,並切換到該目錄;若pushd命令不加任何參數,則會將位於記錄棧最上面的2個目錄對換位置

格式:pushd  [目錄 | -N | +N]   [-n]
選項
目錄   將該目錄加入到棧頂,並執行"cd 目錄",切換到該目錄
+N   將第N個目錄移至棧頂(從左邊數起,數字從0開始)
-N    將第N個目錄移至棧頂(從右邊數起,數字從0開始)
-n    將目錄入棧時,不切換目錄

三、popd——popd用於刪除目錄棧中的記錄;若是popd命令不加任何參數,則會先刪除目錄棧最上面的記錄,而後切換到刪除事後的目錄棧中的最上面的目錄

格式:popd  [-N | +N]   [-n]
選項
+N   將第N個目錄刪除(從左邊數起,數字從0開始)
-N    將第N個目錄刪除(從右邊數起,數字從0開始)
-n    將目錄出棧時,不切換目錄

Bash 腳本編程語言的本質:一切都是字符串

下面,我將探討 Bash 腳本語言中的美學與哲學。 這不是一篇 Bash 腳本編程的教程,可是卻能讓人更加深刻地瞭解 Bash 腳本編程,更加快速地學習 Bash 腳本編程。 閱讀如下內容,不須要你有 Bash 編程的經驗,但必定要和我同樣熱衷於探索各類編程語言的本質,感悟它們的魅力。

咱們平時喜歡對編程語言進行分類,把編程語言分爲面向過程的編程語言、面向對象的編程語言、函數式編程語言等等。在我心中,我認爲 Bash 就是一個面向字符串的編程語言。Bash 腳本語言的本質:一切皆是字符串。 Bash 腳本語言的一切哲學都圍繞着字符串:它們從哪裏來?到哪裏去?使命是什麼? Bash 腳本語言的一切美學都源自字符串: 由鍵盤上幾乎全部的符號 「$ ~ ! # & ( ) [ ] { } | > < - . , ; * @ ' " ` \ ^」 排列組合而成的極富視覺衝擊力的、功能極其複雜的字符串。

Bash 是一個 Shell,Shell 出現的初衷是爲了將系統中的各類工具粘合在一塊兒,因此它最根本的功能是調用各類命令。而命令以及命令的參數都是由字符串組成的,因此 Bash 腳本語言最終進化成一個面向字符串的語言。 Bash 語言的本質就是:一切都是字符串。 看看下圖中的這些變量:

上圖是我在交互式的 Bash 命令行中作的一些演示。在上圖中,我對變量分別賦值,無論等號右邊是一個不帶引號的字符串,仍是帶有引號的字符串,甚至數字,或者數學表達式,最終的結果,變量裏面存儲的都是字符串。我使用一個 for 循環顯示全部的變量,能夠看到數學表達式也只是以字符串的形式儲存,沒有被求值。

Bash 腳本編程語言中的引號、元字符和反斜槓

若是一切都是沒有特殊功能的平凡的字符串,那就沒法構成一門編程語言。在 Bash 中,有不少符號具備特殊含義,如$符號被用於字符串展開,&符號用於讓命令在後臺執行, |用做管道,> <用於輸入輸出重定向等等。因此在 Bash 中,雖然一樣是字符串,可是被引號包圍的字符串和不被引號包圍的字符串使用起來是不同的,被單引號包圍的字符串和被雙引號包圍起來的字符串也是不同的。

究竟帶引號的字符串和不帶引號的字符串使用起來有什麼不同呢?下圖是我構建的一些比較典型的例子:

在上圖中,我展現了 Bash 中生成字符串的 7 種方法:大括號展開、波浪符展開、參數展開、命令替換、算術展開、單詞分割和文件路徑展開。還有歷史命令展開沒有在上圖展現,可是歷史命令展開在前面快速引用和修飾歷史命名那一節有展現,能夠看到歷史命令展開都是使用!開頭的。在使用 Bash 腳本編程的時候,瞭解以上 7 種字符串生成的方式就夠了。在交互式使用 Bash 命令行的時候,才須要瞭解歷史命令展開,熟練使用歷史命令展開可讓人事半功倍。

在上面的圖片中能夠看到,有一些展開方式在被雙引號包圍的字符串中是不起做用的,如大括號展開、波浪符展開、單詞分割、文件路徑展開,而只有參數展開、命令替換和算術展開是起做用的。從圖片中還能夠看出,字符串中的參數展開、命令替換和算術展開都是由$符號引導,命令替換還能夠由` 引導。因此,能夠進一步總結爲,在雙引號包圍的字符串中,只有$ \ `這三個字符具備特殊含義。

若是想讓任何一個字符都不具備特殊含義,可使用單引號將字符串包圍,例如使用正則表達式的時候。還有就是在使用 sed、awk 等工具的時候,因爲 sed 和 awk 本身執行的命令中每每包含有不少特殊字符,因此它們的命令最好用單引號包圍。 例如使用 awk 命令顯示/etc/passwd文件中的每一個用戶的用戶名和全名,可使用這個命令awk -e '{print $1,$5}',其中,傳遞給 awk 的命令用單引號包圍,說明 bash 不執行其中的任何替換或展開。

另一個特殊的字符是\,它也是引用的一種。它能夠解除緊跟在它後面的一個特殊字符的特殊含義(引用)。之因此須要\的存在,是由於在 Bash 中,有些字符稱爲元字符,這些字符一旦出現,就會將一個字符串分割爲多個子串。若是須要在一個字符串中包含這些元字符自己,就必須對它們進行引用。以下圖:

最多見的元字符就是空格。 從上面幾張圖片能夠看出,若是要將一個含有空格的字符串賦值給一個變量,要麼把這個字符串用雙引號包圍,要麼使用\對空格進行引用。 從上圖中能夠看出,Bash 中只有9個元字符,它們分別是| & ( ) ; < > space tab,而在其它編程語言中常常出現的元字符. { } [ ]以及做爲數學運算的加減乘除,在 Bash 中都不是元字符。

字符串從哪裏來,到哪裏去

介紹完字符串、介紹完引用和元字符,下一個目標就是來探討這一個哲學問題:字符串從哪裏來、到哪裏去?經過該哲學問題的探討,能夠推導出 Bash 腳本語言的整個語法。字符串從哪裏來?很顯然,其中一個很直接的來源就是咱們從鍵盤上敲上去的。除此以外,就是我前面提到的七八九種字符串展開的方法了。

字符串展開的流程以下:

1.先用元字符將一個字符串分割爲多個子串;

2.若是字符串是用來給變量賦值,則無論它是否被雙引號包圍,都認爲它被雙引號包圍;

3.若是字符串不被單引號和雙引號包圍,則進行大括號展開,即將 {a,b}c 展開爲 ab ac;

以上三個流程能夠經過下圖證實:

4.若是字符串不被單引號或雙引號包圍,則進行波浪符展開,即將 ~/ 展開爲用戶的主目錄,將 ~+/ 展開爲當前工做目錄(PWD),將 ~-/ 展開爲上一個工做目錄(OLDPWD);

5.若是字符串不被單引號包圍,則進行參數和變量展開;這一類的展開全都以$開頭,這是整個 Bash 字符串展開中最複雜的,其中包括用戶定義的變量,包括全部的環境變量,以上兩種展開方式都是$後跟變量名,還包括位置變量$1 $2 ... $9,其它特殊變量:$@ $* $# $- $! $0 $? $_,甚至還有數組:${var[i]}, 還能夠在展開的過程當中對字符串進行各類複雜的操做,如:${parameter:-word} ${parameter:=word} ${parameter:+word} ${parameter:?word} ${parameter:offset} ${parameter:offset:length} ${!prefix*} ${!prefix@} ${name[@]} ${!name[*]} ${#parameter} ${parameter#word} ${parameter##word} ${parameter%word} ${parameter%%word} ${parameter/pattern/string} ${parameter^pattern} ${parameter^^pattern} ${parameter,pattern} ${parameter,,pattern}

6.若是字符串不被單引號包圍,則進行命令替換;命令替換有兩種格式,一種是 $(...),一種是 `...`;也就是將命令的輸出做爲字符串的內容;

7.若是字符串不被單引號包圍,則進行算術展開;算術展開的格式爲 $((...))

8.若是字符串不被單引號或雙引號包圍,則進行單詞分割;

9.若是字符串不被單引號或雙引號包圍,則進行文件路徑展開;

10.以上流程所有完成後,最後去掉字符串外面的引號(若是有的話)。以上流程只按以上順序進行一遍。不會在變量展開後再進行大括號展開,更不會在第 10 步去除引用後執行前面的任何一步。若是須要將流程再走一遍,請使用 eval。

探討完了字符串從哪裏來,下面來看看字符串到哪裏去。也就是怎麼使用這些字符串。使用字符串有如下幾種方式:

1.把它當命令執行;這是 Bash 中的最根本的用法,畢竟 Shell 的存在就是爲了粘合各類命令。若是一個字符串出如今本該命令出現的地方(一行的開頭,或者關鍵字 then、do 等的後面),它將會被當成命令執行,若是它不是個合法的命令,就會報錯;

2.把它當成表達式;Bash 中本沒有表達式,可是有了 ((...))[[...]],就有了表達式;((...)) 能夠把它裏面的字符串當成算術表達式,而 [[...]] 會把它裏面的字符串當邏輯表達式,僅此兩個特例;

3.給變量賦值;這也是一個特例,有點破壞 Bash 編程語言語法哲學的完整性。爲何這麼說呢?由於=即不是一個元字符,也不容許兩邊有空格,並且只有第 1 個等號會被當成賦值運算符。

下面圖片爲以上觀點給出證據:

再加上一點點的定義,就能夠推導出整個 Bash 腳本編程語言的語法了

前面我已經展現了我對字符串從哪裏來、到哪裏去這個問題的理解。關於字符串的去向,除了兩個表達式和一個爲變量賦值這三個特例,剩下的就只有當命令來執行了。在前面,我提到了元字符和引用的概念,這裏,還得再增長一點點定義:

定義1:控制操做符(Control Operator) 前面提到元字符是爲了把一個字符串分割爲多個子串,而控制操做符就是爲了把一系列的字符串分割成多個命令。舉例說明,在 Bash中,一個字符串 cat /etc/passwd 就是一個命令,第一個單詞 cat 是命令,第 2 個單詞 /etc/passwd 是命令的參數,而字符串 cat /etc/passwd | grep youxia 就是兩個命令,這兩個命令分別是 catgrep,它們之間經過|分割,因此這裏的|是控制操做符。熟悉 Shell 的朋友確定知道|表明的是管道,因此它的做用是:1.把一個字符串分割爲兩個命令,2.將第一個命令的輸出做爲第二個命令的輸入。在 Bash 中,總共只有 10 個控制操做符,它們分別是|| & && | ; ;; () |& <newline>。只要看到這些控制操做符,就能夠認爲它前面的字符串是一個完整的命令。

定義2:關鍵字(Reserved Words) 我沒有將其翻譯成保留字,很顯然,做爲編程語言來講,它們應該叫作關鍵字。一門編程語言確定必須得提供選擇、循環等流程控制語句,還得提供定義函數的功能。這些功能只能經過關鍵字實現。在 Bash 中,只有 22 個關鍵字,它們是「! case coproc do done elif else esac fi for function if in select then until while { } time [[ ]]」。這其中有很多的特別之處,好比「! { } [[ ]]」等符號都是關鍵字,也就是說它們當關鍵字使用時至關於一個單詞,也就是說它們和別的單詞必須以元字符分開(不然沒法成爲獨立的單詞)。這也是爲何在 Bash 中使用「! { } [[ ]]」時常常要在它們周圍留空格的緣由。(再一次證實=是一個很變態的特例,由於它既不是元字符,也不是控制操做符,更加不是關鍵字,它究竟是什麼?)

下面開始推導 Bash 腳本語言的語法:

推導1:簡單命令(Simple command) 就是一條簡單的命令,它能夠是一個以上述控制操做符結尾的字符串。好比單獨放在一行的 uname -r 命令(單獨放在一行的命令實際上是以<newline>結尾,<newline>是控制操做符),或者雖然不單獨放在一行,可是以;&結尾,好比 uname -r; who; pwd; gvim& 其中每個命令都是一個簡單命令(固然,這四個命令放在一塊兒的這行代碼不叫簡單命令),;就是簡單地分割命令,而&還有讓命令在後臺執行的功能。這裏比較特殊的是雙分號;;,它只用在 case 語句中。

推導2:管道(Pipe Line) 管道是 Shell 中的精髓,就是讓前一個命令的輸出成爲後一個命令的輸入。管道的完整語法是這樣 [time [-p]] [ ! ] command1 | command2 或這樣 [time [-p]] [ ! ] command1 |& command2 的。其中 time 關鍵字和 ! 關鍵字都是可選的(使用[...]指出哪些部分是可選的),time 關鍵字能夠計算命令運行的時間,而 ! 關鍵字是將命令的返回狀態取反。看清楚 ! 關鍵字周圍的空格哦。若是使用|,就是把第一個命令的標準輸出做爲第二個命令的標準輸入,若是使用|&,則將第一個命令的標準輸出和標準錯誤輸出都當成第二個命令的輸入。

推導3:命令序列(List) 若是多個簡單命令或多個管道放在一塊兒,它們之間以; & <newline> || &&等控制操做符分開,就稱之爲一個命令序列。關於||&&,熟悉 C、C++、Java 等編程語言的朋友們確定也不會陌生,它們遵循一樣的短路求值的思想。好比 command1 || command2 只有當 command1 執行不成功的時候才執行 command2,而 command1 && command2 只有當 command1 執行成功的時候才執行 command2。

推導4:複合命令(Compound Commands) 若是將前面的簡單命令、管道或者命令序列以更復雜的方式組合在一塊兒,就能夠構成複合命令。在 Bash 中,有 4 種形式的複合命令,它們分別是 (list){ list; }((expression))[[ expression ]] 。請注意第 2 種形式和第 4 種形式大括號和中括號周圍的空格,也請注意第 2 種形式中 list 後面的;,不過若是}另起一行,則不須要;,由於<newline>;是起一樣做用的。在以上4種複合命令中, (list) 是在一個新的Shell中執行命令序列,這些命令的執行不會影響當前Shell的環境變量,而 { list; } 只是簡單地將命令序列分組。後面兩種表達式求值前面已經講過,這裏就不講了。後面可能會詳細列出邏輯表達式求值的選項。

上面的4步推導是一步更進一步的,是由簡單逐漸到複雜的,最簡單的命令能夠組合成稍複雜的管道,再組合成更復雜的命令序列,最後組成最複雜的複合命令。

下面是 Bash 腳本語言的流程控制語句,以下:

  1. for name [ [ in [ word ... ] ] ; ] do list ; done

  2. for (( expr1 ; expr2 ; expr3 )) ; do list ; done

  3. select name [ in word ] ; do list ; done

  4. case word in [ [(] pattern [ | pattern ] ... ) list ;; ] ... esac

  5. if list; then list; [ elif list; then list; ] ... [ else list; ] fi

  6. while list-1; do list-2; done

  7. until list-1; do list-2; done

上面的公式你們看得懂吧,我相信你們確定看得懂。其中的 [...] 表明的是能夠有也能夠真沒有的部分。在以上公式中,請注意第 2 個公式 for 循環中的雙括號,它執行的是其中的表達式的算術運算,這是和其它高級語言的 for 循環最像的,可是很遺憾,Bash 中的算術表達式目前只能計算整數。再請注意第 3 個公式,select 語法,和 for...in... 循環的語法比較相似,可是它能夠在屏幕上顯示一個菜單。若是我沒有記錯的話,Basic 語言中應該有這個功能。其它的控制結構在別的高級語言中都很常見,就不須要我在這裏囉嗦了。

最後,再來展現一下如何定義函數:

   name () compound-command [redirection]

  或者

   function name [()] compound-command [redirection]

能夠看出,若是有 function 關鍵字,則()是可選的,若是沒有 function 關鍵字,則()是必須的。這裏須要特別指出的是:函數體只要求是 compound-command,我前面總結過 compound-command 有四種形式,因此有時候定義一個函數並不會出現{ }哦。以下圖,這樣的函數也是合法的:

That's all。這就是 Bash 腳本語言的所有語法。就這麼簡單。

好像忘了點什麼?對了,還有輸入輸出重定向沒有講。輸入輸出重定向是 Shell 中又一個偉大的發明,它的存在有着它獨特的哲學意義。這個請看下一節。

輸入輸出重定向

Unix 世界有一個偉大的哲學:一切皆是文件。(這個扯得有點遠。) Unix 世界還有一個偉大的哲學:建立進程比較方便。(這個扯得也有點遠。)並且,每個進程一建立,就會自動打開三個文件,它們分別是標準輸入、標準輸出、標準錯誤輸出,普通狀況下,它們鏈接到用戶的控制檯。在 Shell 中,使用數字來標識一個打開的文件,稱爲文件描述符,並且數字 0、 一、 2 分別表明標準輸入、標準輸出和標準錯誤輸出。在 Shell 中,能夠經過><將命令的輸入、輸出進行重定向。結合 exec 命令,能夠很是方便地打開和關閉文件。須要注意的是,當文件描述符出如今><右邊的時候,前面要使用&符號,這多是爲了和數學表達式中的大於和小於進行區別吧。使用&-能夠關閉文件描述符。

> < & 數字 exec -,這就是輸入輸出重定向的所有。下面的公式中,我使用 n 表明數字,若是是兩個不一樣的數字,則使用 n一、n2,使用 [...] 表明可選參數。輸入輸出重定向的語法以下:

[n]> file        #重定向標準輸出(或 n)到file。
[n]>> file       #重定向標準輸出(或 n)到file,追加到file末尾。
[n]< file        #將file重定向到標準輸入(或 n)。
[n1]>&n2         #重定向標準輸出(或 n1)到n2。
2> file >&2      #重定向標準輸出和錯誤輸出到file。
| command        #將標準輸出經過管道傳遞給command。
2>&1 | command   #將標準輸出和錯誤輸出一塊兒經過管道傳遞給command,等同於|&。

請注意,數字和><符號之間是沒有空格的。結合 exec,能夠很是方便地使用一個文件描述符來打開、關閉文件,以下:

echo Hello >file1
exec 3<file1 4>file2  #打開文件
cat <&3 >&4           #重定向標準輸入到 3,標準輸出到 4,至關於讀取file1的內容而後寫入file2
exec 3<&- 4>&-        #關閉文件
cat file2
#顯示結果爲 Hello
 
#還能夠暫存和恢復文件描述符,以下:
exec 5>&2            #把原來的標準錯誤輸出保存到文件描述符5上
exec 2> /tmp/$0.log  #重定向標準錯誤輸出
...
exec 2>&5            #恢復標準錯誤輸出
exec 5>&-            #關閉文件描述符5,由於不須要了

還能夠將<>一塊兒使用,表示打開一個文件進行讀寫。

除了 exec,輸入輸出重定向和 read 命令配合也很好用,read 命令每次讀取文件的一行。可是要注意的是,輸入輸出重定向放到 for、while 等循環的循環體和循環外,效果是不同的。以下圖:

另外,輸入輸出重定向符號><還能夠和()一塊兒使用,表示進程替換(Process substitution),如>(list)<(list)。結合前面提到的<>(list)的含義,進程替換的做用是很容易猜到的哦。

Bash 腳本編程語言的美學:大道至簡

若是你問我 Bash 腳本語言哪裏美?我會回答:簡潔就是美。請看下面逐條論述:

1.使用了簡潔的抽象的符號。Bash 腳本語言幾乎使用到了鍵盤上可以找到的全部符號,$用做字符串展開,|用做管道,<>用做輸入輸出重定向,一點都不浪費;

2.只使用了 9 個元字符、10 個控制操做符和 22 個關鍵字,就構建了一個完整的、面向字符串編程的語言;

3.概念上具備很好的一致性;例如 (list) 複合命令的功能是執行括號內的命令序列,而$用於引導字符串展開,因此 $(list) 用於命令替換(因此我前面說$()形式的命令替換比`...`形式的命令替換更加具備一致性)。再例如 ((expresion)) 用於數學表達式求值,因此 $((expression)) 表明算術展開。再例如{},配合使用,且中間沒有空格時,表明大括號展開,可是當須要使用{ }來定義複合命令時,必須把{ }當關鍵字,它們和它裏面的內容必須以空格隔開,並且}和它前面的一條命令之間必須有一個;或者<newline>。這些概念上的一致性設計得很是精妙,使用起來天然而然可讓人體會到一種美感;

4.完美解決了一個命令執行時的輸出和運行狀態的分離。有其它編程語言經歷的人也常常會遇到這樣的問題:當咱們調用一個函數的時候,函數可能會產生兩個結果,一個是函數的返回值,一個是函數調用是否成功。在 C# 和 Java 等高級語言中,每每使用 try...catch 等捕獲異常的方式來判斷函數調用是否成功,但仍然有程序員讓函數返回 null 表明失敗,而 C 語言這種沒有異常機制的語言,實在是難以判斷一個函數的返回值究竟如何表示該函數調用是否成功(好比就有不少 API 讓函數返回 -1 表明失敗,而有的函數運行失敗是會設置 errno 全局變量)。在 Bash 中,命令運行的狀態和命令的標準輸出區分很明確,若是你須要命令的標準輸出,使用命令替換來生成字符串,若是你只須要命令的運行狀態,直接將命令寫在 if 語句之中便可,或者使用 $? 特殊變量來檢查上一條命令的運行狀態。若是不想在檢查命令運行狀態的時候讓命令的標準輸出影響用戶,能夠把它重定向到 /dev/null,像這樣:

if cat /etc/passwd | grep youxia > /dev/null; then echo 'youxia is exist'; fi

5.使用管道和輸入輸出重定向讓文件的讀寫變得簡單。想想在 C 語言中怎麼讀文件吧,除了麻煩的 open、close 不說,每讀一個字符串還得先準備一個 buffer,準備長了怕浪費空間,準備短了怕緩衝區溢出,虐心啦。使用 Bash,那真的是太方便了。

6.它還有邪惡的 eval 哦,eval 命令實在是太強大了,請看下圖,模擬指針進行查表:

固然,自從 Bash 3 以後,Bash 自己就提供了間接引用的功能(使用「${!var}」)。

例外:

  Bash 語言也並非在全部的方面都是完美的,還存在幾個特別的例外,像前面說的=。除了=以外,()也有一個使用不一致的地方,那就是對數組的初始化,例如 array=(a b c d e f) ,這和前面講的()用於在子 Shell 中執行命令序列還真的是不一致。

總結

以上內容是個人胡言亂語,由於以上內容即沒法教會你們完整的 Bash 語法,也沒法教會你們用 Bash 作任何一點有意義的工做。若是想用 Bash 乾點實事,建議你們閱讀 O'Reilly 出的《Shell腳本學習指南》。

求打賞

我對此次寫的這個系列要求是很是高的:首先內容要有意義、夠充實,信息量要足夠豐富;其次是每個知識點要講透徹,不能模棱兩可含糊不清;最後是包含豐富的截圖,讓那些不想裝 Linux 系統的朋友們也能夠領略到 Linux 桌面的風采。若是個人努力獲得你們的承認,能夠掃下面的二維碼打賞一下:

版權申明

該隨筆由京山遊俠在2018年10月04日發佈於博客園,引用請註明出處,轉載或出版請聯繫博主。QQ郵箱:1841079@qq.com

相關文章
相關標籤/搜索