寫給高年級小學生看的《Bash 指南》

若讓計算機理解個人意圖,能夠有兩種方式,說和指。這與生活中我爲了讓他人可以理解個人意圖所採用的方式類似。譬如,我想讓朋友去超市幫我買瓶飲料,我可使用祈使句,「幫我去超市買瓶可樂,如何?」我也能夠把他領到超市門口,指一下超市,而後再把他領進超市,指一下飲料櫃裏的一瓶可樂,而後再指一下他口袋裏的錢包。基於說的方式讓計算機理解個人意圖,就是向計算機輸入一些命令,計算機則經過命令解釋程序理解個人意圖。基於指的方式讓計算機理解個人意圖,就是經過鼠標、觸摸屏之類的交互設備,向計算機輸入一些座標信息,計算機則經過圖形界面程序理解個人意圖。html

在生活中,人們一般會喜歡經過說的方式讓他人理解本身的意圖,只有在難以言說的時候,纔會考慮用指的辦法。這是由於咱們已經掌握了一門與他人溝通交流所用的語言,利用這種語言讓他人理解咱們的意圖,是成本最低的方法。可是對於計算機而言,因爲大多數人一般沒有機會學習計算機能夠理解的語言,所以對於他們而言,採起指的方式讓計算機理解他們的意圖則是成本最低的方法。由於這個緣故,大多數人喜歡以圖形界面交互的方式使用計算機,由於他們以爲學習一門計算機語言,成本過高。假若將一門計算機語言列入小學課程,或許孩子們長大後與計算機的溝通便再也不像現代人這樣過分依賴圖形界面交互方式。git

圖形界面與命令並不衝突。咱們教孩子說話,一開始也是以指物的方式,讓孩子得到了基本的語言交流能力,在這個基礎上再教他們識文斷字。圖形界面程序爲計算機的普及做出了很大貢獻,可是在這個基礎上,若想讓計算機更準確、高效地理解咱們的意圖,同時也能讓他人更準確、高效地理解咱們的意圖,便須要學習一門計算機語言了。github

計算機語言不像人類語言那樣豐富。所以,不要期望咱們衝着計算機喊幾嗓子,或者在運行着命令解釋器的終端(命令行窗口)裏輸入「幫我寫畢業論文」這樣的句子,計算機就可以充分理解咱們的意圖,轉而毫無怨言地去工做。至少直至目前,咱們的計算機尚不具有這樣的功能。不過,咱們能夠經過付費的方式,對那些擅長計算機語言的人喊幾嗓子,或者給他們發幾條微信。假若想親歷親爲,並且認爲與計算機這樣的機器進行交流像是在玩一種文字意義上的遊戲,那麼即可以從學習一種命令解釋程序入手,學習這個程序所支持的語言以及一些經常使用的命令。數據庫

Bash 語言

若打算學習計算機語言,不妨從 Bash 語言入手,把它做爲「母語」。編程

Bash 是一個命令解釋程序,用於執行從標準輸入或文件中讀取的命令。能被 Bash 理解的語言即 Bash 語言。像 Bash 這樣的程序一般稱爲 Shell,做爲計算機用戶操做計算機時的基本交互界面,亦即計算機用戶經過 Shell 使用操做系統內核所提供的種種功能。Bash 是衆多 Shell 中的一種,但它倒是流傳最爲普遍的 Shell。在 Windows 系統中,Windows 10 之前的版本能夠經過安裝 Cygwin 即可得到 Bash,而在 Windows 10 中,只需開啓 WSL(Windows Subsystem for Linux)便有了 Bash。至於 Linux 和 Mac OS X 系統,Bash 則是它們的核心標配組件。Android 手機上,經過 F-Droid 安裝 Termux 也能獲得 Bash。安全

由於 Bash 幾乎無處不在,並且它可以幫助咱們處理許多計算機裏的平常任務,因此不妨把它做爲計算機世界裏的母語去學習。計算機語言雖然種類繁多,可是用這些語言寫的程序,一般能夠做爲命令在 Bash 或其餘某種 Shell 環境中運行,這種作法頗相似於咱們在母語的基礎上學會了一些專業裏的「行話」。bash

事實上,在熟悉了 Bash 語言以及一些經常使用的命令以後,常常能夠發現,不少人用其餘編程語言所寫的那些程序實際上不必寫,由於這些程序頗有可能只須要一條 Bash 命令即可以實現了。譬如,你在撰寫文檔的時候,有時可能會想使用直角引號,而非 ,可是本身所用的中文輸入法裏可能並非很方便輸入 ,這時該怎麼辦呢?沒有多少好辦法,除非你對這個輸入法足夠了解,去修改它的碼錶,爲鍵盤上的 " 鍵與 創建映射。對於 Bash 而言,若是你的文檔可以表現爲純文本的格式,假設文件名爲 foo.txt ,那麼你大能夠繼續使用 ,只需在文檔定稿後,使用 sed 命令將 替換爲 ,即微信

$ sed -i 's/「/「/g; s/」/」/g' foo.txt

已經習慣了圖形界面交互方式與計算機溝通又抗拒學習計算機語言的人可能會擡槓,「我在微軟 Word 裏也能夠用『查找/替換』的方式來完成你這條命令所能完成的任務啊!」誠然如此,可是若是手上不止有一份文件,而是有一組文件 foo-1.txt、foo-2.txt、foo-3.txt……須要做引號替換處理呢?難道要用微軟 Word 逐一打開這些文件,而後做「查找/替換」處理,再逐一保存麼?假若這樣作,就至關因而計算機在使用人,而非人使用計算機。對於 sed 命令而言,將某項文本處理過程做用於多份文件與做用於一份文件並沒有太大區別。若對 foo-1.txt、foo-2.txt、foo-3.txt……進行引號替換,只需編程語言

$ sed -i 's/「/「/g; s/」/」/g' foo-*.txt

莊子說,「指窮於爲薪。火傳也,不知其盡也。」假若將「引號替換」視爲「火」,那麼用微軟 Word 逐一打開文件,進行引號替換處理,再將處理後的文件保存起來,這種作法就是「指窮於爲薪」,即便你很敬業,很努力,可是這樣去作一生,在 sed 看來也只是完成了它在一瞬間就能作到的事。莊子若活到如今,必定是一個善於用計算機語言編程的人。編輯器

終端

內心至少要有一團光,哪怕它很微弱,只是來自一根火柴。在這樣的微光裏,看到的是世界是一片寂靜的混沌,至少也是全貌。個人桌子上老是有一盒火柴,做爲對童年愛放野火的冬天的懷念。我取出一根火柴,讓它的頭部迅捷有力地擦過磷紙,小小的火焰噴射而出,引燃了它的身體。我要帶着這團火焰,進入一個詭異的黑暗世界,它的名字叫終端。

終端是 Shell 的界面,人經過終端輸入命令。Shell 從終端接到命令,而後理解命令,執行命令。不要忘記,Bash 是諸多 Shell 中的一種。

在這個世界裏,持着光,我注意的第一個東西是「$」,它的右面是一個不斷明滅的矩形塊,頂部是「~」。「$」稱做命令提示符。閃爍的矩形塊表示能夠在此輸入命令。「~」是我當前所在的位置——硬盤裏的某個目錄,亦即當前目錄或當前工做目錄。實際上 ~ 不過是 Bash 對個人我的主目錄給出的簡寫,它的全稱是「/home/garfileo」。假若我此時正處於/tmp 目錄,那麼$ 的頂部應當是「/tmp」。

對於終端的初始印象,你與我或許不一樣。若想看到我所看到的,須要在 ~/.bashrc 文件裏添加

PS1="\e[0;32m\w\e[0m\n$ "

\e[0;32m\e[0m 皆爲 Bash 世界裏的顏色值,前者表示綠色,後者表示無色;當兩者之間出現 \w 時,所產生的效果是設定 \w 爲綠色,可是不影響 \w 之外的字符。\w 表示當前目錄。\n 表示換行。這種咒語級的代碼,足以令不少人望 Bash 而生畏或生厭了。沒必要擔憂,這樣的咒語並不常常須要念。

假若我在 ~/.bashrc 文件裏刪除上述對 PS1 的設定,那麼我對終端的初始印象應當是 $ 的頂部沒有東西,左側是「garfileo@zero ~」,右側是那個不斷明滅的矩形塊。或許你會喜歡我對 PS1 所做出的上述設定,特別是在你企圖在命令提示符的右側輸入很長的命令之時。

echo

初次使用 Bash,也許尚不知如何向 ~/.bashrc 文件添加

PS1="\e[0;32m\w\e[0m\n$ "

不過,如今運行 Bash 的操做系統大多提供了圖形界面。能夠圖形界面式的文件管理器裏找到 .bashrc 文件,而後使用一種圖形界面式的文本編輯器打開這份文件,添加上述內容,而後再保存文件。這是咱們剛開始使用計算機的時候就已經學會了的方法。可是,從如今開始可沒必要如此,由於相似的任務經過一些簡單的命令即可完成。例如,要完成上述任務,只需在終端的命令提示符後輸入

echo 'PS1="\e[0;32m\w\e[0m\n$ "' >> ~/.bashrc

而後回車。以後,Bash 便會對咱們輸入的這一行文本予以理解和執行。咱們輸入的這行文本即是命令。

echo 命令可將一行文本顯示於終端。例如,

$ echo 'PS1="\e[0;32m\w\e[0m\n$ "'

Bash 執行這條命令以後,終端緊接着便會顯示

PS1="\e[0;32m\w\e[0m\n$ "

就像是對空曠寂靜的山谷喊了一聲「PS1="\e[0;32m\w\e[0m\n$ "」,而後山谷給出了迴音,echo 命令得名於此。亦即,echo 只是將它所接受的文本不加改變地輸出。

echo 自己是無用的。可是,當它的後面出現了 >> ~/.bashrc 時,它的輸出便會被 >> 強行導入 ~/.bashrc 文件,此時沒用的 echo 便發揮了做用。天生我材必有用。

>> 是輸出重定向符,由於它能夠將一個命令的輸出導向到指定的文件。當 >>echo 命令以後出現時,它是如何得知 echo 命令的輸出的呢?對咱們而言,echo 命令的輸出是在終端裏呈現出來的,難道 >> 也能像咱們這樣「看到」終端裏的內容麼?

咱們看到的,並不是所有。咱們所看到的 echo 的輸出,其實是 echo 在一份文件裏寫入的內容,這份文件的名字叫 stdout(標準輸出)。終端從 stdout 讀取內容並顯示於屏幕。>> 也能從 stdout 讀取內容並寫入其餘文件。這些是咱們看不到的部分。

輸入/輸出重定向

在終端的命令提示符後輸入

echo 'PS1="\e[0;32m\w\e[0m\n$ "' >> ~/.bashrc

而後回車。Bash 是如何獲得這行文本並將其做爲命令予以執行的呢?依然是經過一份文件。這份文件的名字叫 stdin(標準輸入)。咱們在終端裏輸入任何信息,本質上都是在向 stdin 寫入內容。當咱們輸入一行文本回車時,Bash 便開始讀取 stdin 裏的內容,把它們理解爲一條命令或一組命令的組合,而後予以執行。

對於 >> 指向的文件(命令中位於 >> 右側的文件),>> 是不改變其原有內容的前提下將 stdout 中的內容寫入該文件,亦即向該文件尾部追加信息。若指望用 stdout 中的內容替換該文件的原有內容,可以使用 >。例如(僅僅是個例子,不要真的去這樣作):

$ echo 'PS1="\e[0;32m\w\e[0m\n$ "' > ~/.bashrc

執行這條命令以後,~/.bashrc 文件的所有內容便會被替換爲「\e[0;32m\w\e[0m\n$ "」。

任何命令,只要它可以向 stdout 輸出信息,其輸出皆能經過 >>> 寫入到指定文件。假若所指定的文件不存在,Bash 會自動建立。所以,不妨將 Bash 的輸出重定向符視爲對各類文本編輯器或字處理軟件的「文件(File)」菜單裏的「另存爲(Save as)」功能的抽象。

Bash 也提供了輸入重定向符 <,可將其視爲對各類文本編輯器或字處理軟件的「文件(File)」菜單裏的「打開(Open)」功能的抽象。任何命令,只要它以 stdin 中的信息做爲輸入,皆能經過 < 將指定文件中的信息做爲輸入,由於 < 可將指定文件中的信息寫入 stdin。例如,Bash 將 stdin 中的信息視爲命令予以執行,假若將某條命令寫入一份文件,而後經過 < 將該文件做爲 Bash 的輸入,所產生的效果是,Bash 會將這份文件中的內容視爲命令並予以執行。如下命令模擬了這一過程:

$ echo 'echo "Hello world!"' > /tmp/foo.sh
$ bash < /tmp/foo.sh
Hello wrold!

在 Bash 中,能夠執行 bash 命令,這一點細想起來會有些奇怪。不過,人類不是也常常將「自我」做爲一種事物去思考麼?

實際上,上述 bash 命令中的輸入重定向符是沒必要要的。由於 bash 命令本來便支持直接讀取指定文件中的內容並將其視爲命令予以執行,即

$ bash /tmp/foo.sh

輸入重定向符主要面向那些只支持從 stdin 獲取輸入信息的程序,例如,用於計算凸包的程序 qhull 即是這樣的程序。假若機器上已經安裝了 qhull 軟件包,可使用 rbox 命令生成含有三維點集的數據文件,而後經過 < 將數據文件中的三維點集信息傳遞於 qhull 程序:

$ rbox c > points.asc
$ qhull s n < points.asc

以後,qhull 便會輸出三維點集的凸包信息。

管道

認真觀察

$ echo 'echo "Hello world!"' > /tmp/foo.sh
$ bash < /tmp/foo.sh

$ rbox c > points.asc
$ qhull s n < points.asc

這兩組命令是否類似?在形式上,它們都是一條命令經過輸出重定向將本身本來要寫入 stdout 的輸出信息被重定向到了一份文件,另外一條命令則是經過輸入重定向將本身本來要從 stdin 裏獲取的輸入信息變成了從指定文件中獲取。這個過程相似於,你將信息寫到了紙條上,而後又將紙條扔給我看。可是在生活中,咱們一般不會這樣麻煩,你會用嘴巴發出信息,我則用耳朵接受信息。Bash 也有相似的機制,名曰管道。基於管道,上述兩組命令可簡化爲

$ echo 'echo "Hello world!"' | bash
$ rbox c | qhull s n

只要一條命令能經過 stdout 輸出信息,而另外一條命令能經過 stdin 獲取信息,那麼這兩條命令即可以藉助管道鏈接起來使用。說話要比寫字快,並且可以節省紙張,管道的意義與之相似,不只提升了信息的傳遞速度,並且不消耗硬盤。

cat, mv 和 sed

經過 >> 可以實現向一份文本文件尾部追加信息。有沒有辦法向一份文本文件的首部追加信息呢?這個問題並不簡單。在生活中,有許多事須要咱們排隊。來得晚了,應當主動站在隊尾,這樣作不會影響對於早來的人。可是,若來得晚了,反而強行站到隊伍的前面去了,可能會被捱打。也許咱們都喜歡插隊,可是卻不多有人喜歡插隊的人。所謂文件,事實上不過是一組排好了隊的字節。所以,經過 >> 向一份文本文件尾部追加信息容易,可是若將信息插入到文件的首部,原有的文件內容在硬盤裏的相應位置必然要發生變化。

假設文件 foo.tex 的內容爲

\starttext
這是一份 ConTeXt 文稿。
\stoptext

若想將「\usemodule[zhfonts]」增長到這份文件的首部,可經過如下命令實現:

$ echo '\usemodule[zhfonts]' > foo-tmp.tex
$ cat foo.tex >> foo-tmp.tex
$ mv foo-tmp.tex foo.tex

cat 命令用於將多份文本文件的內容鏈接起來,並將結果寫入 stdout。當 cat 只處理一份文本文件時,產生的效果是將這份文件的內容寫入 stdout。因爲 stdout 中的內容可經過 >> 導入到指定文件,所以上述的 cat 命令所起到的做用至關於把 foo.tex 的內容複製出來,並追加到 foo-tmp.tex 的尾部。

mv 命令可將文件從其當前所在目錄移動到另外一個指定目錄,假若這個指定目錄依然是當前目錄,那麼 mv 命令便起到了文件重命名的效果。上述 mv 命令將 foo-tmp.tex 重命名爲 foo.tex。最終獲得的 foo.tex,便等價於在其原有的內容首部插入了「\usemodule[zhfonts]」。

不過,可以運行 Bash 的環境,大多也提供了擅長處理文本編輯任務的 sed 程序。與 Bash 類似,sed 也能執行它可以理解的一組命令,這組命令專事於文本的編輯。例如,若是將

'1 i \
\\usemodule[zhfonts]'

傳遞於 sed,並指使 sed 將此命令做用於 foo.tex,即

$ sed -i '1 i \
> \\usemodule[zhfonts]' foo.tex

sed 便會這條命令理解爲,在 foo.tex 的第 1 行插入「\\usemodule[zhfonts]」。

注意,上述的 > 符號並不是輸出重定向符,它是終端的二級命令提示符。在終端中輸入多行文本構成的命令時,終端自動給出二級命令提示符。還記得 $ 吧,以前將其稱爲命令提示符,實際上它是一級命令提示符,這也正是爲什麼用於設定它的格式時是經過 PS1 的緣由。相似地,能夠經過 PS2 來定製二級命令提示符的格式。Bash 如何知道咱們要輸入多行文本呢,亦即在咱們輸完 sed -i 'i 1 \ 並摁了回車鍵以後,爲什麼 Bash 不認爲咱們已經將命令輸入完畢呢?這是由於在上述命令中,在第一行的末尾出現的 \,會被 Bash 理解爲續行符。回車鍵產生的換行符位於續航符以後,會被 Bash 忽略。

利用續行符,可將較長的命令分紅多行輸入,Bash 會將最後一行的換行符做爲命令輸入完畢的信號。例如,

$ echo 'echo "Hello world!"' \
>  | bash

這條命令雖然不長,可是足以說明續行符的用法。

不過,上述的 sed 命令的第一行雖然在末尾出現了續行符,但實際上 Bash 是沒有機會得知該續行符的存在。由於 sed 程序所接受的命令文本是拘禁在一對單引號中的,這種形式的文本叫作單引號字串。單引號字串中的續行符和換行符,Bash 會不予理睬。所以,上述的 sed 命令的第一行末尾的 \,實際上並不是面向 Bash 而存在,「i \」其實是 sed 程序的一個指令,用於在指定的文本行以前插入一行或多行給定的文本。由於 \ 對於 sed 程序有着特殊含義,所以在經過 sed 命令在 foo.tex 的首部添加「\usemodule[zhfonts]」時,必需對文本中的 \ 進行轉義,因爲 sed 程序是以 \ 爲轉義符,所以在 sed 命令中,「\usemodule[zhfonts]」必須寫成「\\usemodule[zhfonts]」。

至此,是否覺察到了 Bash 語言的混亂之處?

引號

命令的輸入數據和輸出數據只有兩種,文本與存儲文本的文件。Bash 如何對它們予以區分呢?Bash 規定,凡出如今一對雙引號 " 或出如今一對單引號 ' 以內的文字即是文本,不然即是文件。不過,咱們向 Bash 提交的一切皆爲文本。爲了進行區分,應當將做爲命令的輸入或輸出數據的文本稱爲字串,而且出如今一對 " 以內的字串可稱爲雙引號字串,出如今 ' 以內的字串則稱爲單引號字串。

問爲何會有兩種字串,不如問爲何人類發明的文字裏要有兩種引號。在與他人的對話中,彼此所說的每句話能夠用雙引號包圍起來。例如:

我:「最近我在讀《齊物論》。」
你:「它講了什麼?」
我:「莊子建議咱們應當‘爲是不用而寓諸庸’。」

單引號能夠出如今雙引號裏,這意味着什麼?意味着雙引號更爲寬鬆,而單引號則比雙引號更爲緊緻。寬鬆的能夠包含緊緻的。對於 Bash 而言,雙引號字串的寬鬆體現爲,雙引號字串中若是含有一些對於 Bash 有特殊含義的字符,Bash 會使用這些字符的含義代替這些字符。例如,

$ echo "$PS1"

假若指望這條 echo 命令可以輸出 $PS1,那麼結果就會使人失望了。這條命令輸出的並不是 $PS1,而是一開始咱們在 ~/.bashrc 文件中爲 PS1 所設定的「\e[0;32m\w\e[0m\n$ 」。

單引號比雙引號更爲緊緻,它能夠阻止 Bash 對 $PS1 有其本身的理解。所以

$ echo '$PS1'

的輸出結果方爲「$PS1」。

前文中的示例

$ echo 'echo "Hello world!"' | bash

其中,雙引號字串是放在單引號字串以內的,這與上述我虛擬的關於《齊物論》的對話中所用的引號的用法有所不一樣,可是在 Bash 語言中,只能這樣去寫。假若將上述命令寫成

$ echo "echo 'Hello world!'" | bash

Bash 對字串中出現的 ! 會有特殊的理解,由於 ! 是 Bash 命令,想不到吧?

字串中若出現引號,須要使用 \ 對引號進行轉義。例如,

$ echo "I say: \"Hello world!\""

Bash 會認爲 echo 的輸入數據是「I say: "$hello"」。假若去掉轉移符 \,即

$ echo "I say: "$hello""

Bash 會認爲 echo 的輸入數據爲

  1. I say:
  2. $hello
  3. 空字串

在雙引號字串中,爲避免 Bash 對某些咱們指望保持本義的字符產生誤解,一般須要用轉義符 \ 讓 Bash 放棄這樣的嘗試。

變量

PS1 是一個名字,「\e[0;32m\w\e[0m\n$ 」是它指代的對象,$PS1 是引用這個對象。Bash 容許咱們以這樣的方式對數據予以命名,然後以名字指代數據。例如

$ hello="Hello world!"
$ echo $hello

$ echo "Hello world!"

等價。

名字,這是咱們很熟悉的概念,可是在數學和編程中,它再也不叫名字,而是叫「變量」。爲數據取一個名字,就是「定義一個變量」,而使用這個名字,叫作「變量的展開」或者「變量解引用」。很尋常的作法,一旦換了名目,就馬上使人以爲莫測高深了起來。

名字之因此被視爲變量,確定不是由於老子說過「名可名,非恆名」。變量的存在,首先是爲了便於數據的重複使用。譬如,一旦定義了變量 hello,即可以在命令中重複地使用它。

其次,變量便於實現數據的複合,例如

$ echo "I say: \"$hello\""
I say: "Hello world!"

這條命令利用了雙引號字串的寬鬆特性,實現了在字串中對變量進行解引用,從而起到了言語簡約但意義完備的效果。

可是,變量最重要的用途應該是執一發而動全身。若是有許多條命令使用了 hello 這個變量,當我對它指代的數據進行更換時,全部使用 hello 變量的命令皆會受到影響。

注意,在定義變量時,等號兩側不能出現空白字符。例如

$ my_var = "3"

Bash 不會認爲這條語句是在定義變量 my_var,反而會認爲 my_var 是一條命令,而 ="3" 是這條命令的輸入數據。

條件

對於一個變量 foo,若不知它是否已定義,可在終端裏喚它一下試試,

$ echo $foo

假若 echo 只是輸出一個空行,即可以肯定 foo 未定義。上述命令中,echo 輸出了空行,所以 foo 未定義。

若是 foo 未定義,就給它一個定義,不然便對 foo 進行展開。像這樣的任務,單憑查看 echo 的輸出沒法完成。不過,Bash 支持與「若……則……不然……」相似的語法。例如

$ if [ "$foo" = "" ]; then foo="FOO"; else echo $foo; fi

$ if [ "$foo" = "" ]
> then
>     foo="FOO"
> else
>     echo $foo
> fi

[ "$foo" = "" ] 是一條命令,用於測試 "$foo""" 是否相同。if 可根據 [ "$foo" = "" ] 的結果控制 Bash 是執行 foo="FOO" 仍是執行 echo $foo

假設變量 foo 未定義,那麼 [ "$foo" = "" ] 的結果是什麼呢?爲 0。假設變量 foo 已定義,那麼 [ "$foo" = "" ] 的結果是什麼呢?爲非 0。如何得知 [ "$foo" = "" ] 的結果呢?顯然這個結果不可能寫在 stdout 裏,不然咱們能夠從終端裏看到這個結果,但事實上咱們並不知道這個結果,而 if 卻能知道。

事實上,每條命令在被 Bash 執行後,都會給出一個稱做「命令的退出狀態」的結果。Bash 內定的變量 ? 便指代這個結果。所以,要查看一條命令的退出狀態,只需在它結束後,馬上對 ? 進行展開。例如

$ [ "$foo" = "" ]
$ echo $?
0

[ "$foo" = "" ] 的退出狀態爲 0,意味着 "$foo""" 相同。假若定義了 foo

$ foo="FOO"
$ [ "$foo" = "" ]
$ echo $?
1

那麼 [ "$foo" = "" ] 的退出狀態爲非 0,意味着 "$foo""" 不一樣。

經常使用的測試命令

因爲 [ "$foo" = "" ] 是一條命令,所以必須注意,[ 與其後的字符之間至少要空出一格,不然這條命令便寫錯了,Bash 會拒絕執行。[] 所囊括的文本稱爲條件表達式。

在測試命令的條件表達式中, = 可用於比較兩個字串是否相同,比較結果表現爲測試命令的退出狀態。相似地,!= 可用於比較兩個字串是否不一樣。=!= 皆爲雙目運算符,即參與運算的對象是兩個。對於字串,也有單目運算符,例如 -z,用於肯定字串長度是否爲 0。事實上,[ "$foo" = "" ][ -z "$foo" ] 等價。

命令的輸入/輸出數據除了字串以外,還有文件。不少時候,也須要對文件進行一些測試。最爲經常使用的是肯定一份文件是否存在,單目運算符 -e 可知足這一要求。例如,

$ if [ -e foo.txt ]; then rm foo.txt; else touch foo.txt; fi

的含義是,若當前目錄中存在文件 foo.txt,便將其刪除,不然便建立一份空文件做爲 foo.txt。rm 命令可用於刪除文件或目錄,touch 命令可用於建立給定文件名的空文件。相似命令有,-d 可用於肯定文件或目錄是否存在。-s 可用於肯定文件存在而且內容爲空。雙目運算符 -nt-ot 分別用於判斷一個文件是否比另外一個文件更新或更舊。

Bash 提供了許多測試運算,詳情可查閱 test 命令的手冊,方法是:

$ man 1 test

之因此要查閱 test 命令的手冊,是由於 [ 條件表達式 ] 只是 test 命令的一種簡潔的寫法。事實上,

$ [ -e foo.txt ]

$ test -e foo.txt

等價。

在得知了 man 命令的存在以後,也許你會想查閱 rmtouch 等命令的手冊。

循環

作 10 個俯臥撐。如何用 Bash 語言描述呢?有兩種方式,一種是

$ for ((i = 1; i <= 10; i++)); do echo "第 $i 個俯臥撐"; done

也能夠寫爲

$ for ((i = 1; i <= 10; i++))
> do 
>     echo "第 $i 個俯臥撐"
> done

$ for ((i = 1; i <= 10; i++)); do 
>     echo "第 $i 個俯臥撐"
> done

執行這條命令後,繼而終端便會顯示

第 1 個俯臥撐
第 2 個俯臥撐
第 3 個俯臥撐
第 4 個俯臥撐
第 5 個俯臥撐
第 6 個俯臥撐
第 7 個俯臥撐
第 8 個俯臥撐
第 9 個俯臥撐
第 10 個俯臥撐

另外一種方式是

$ i=1
$ while ((i <= 10)); do echo "第 $i 個俯臥撐"; ((i++)); done

也可寫爲

$ i=1
$ while ((i <= 10))
> do
>     echo "第 $i 個俯臥撐"
>     ((i++))
> done

$ i=1
$ while ((i <= 10)); do
>     echo "第 $i 個俯臥撐"
>     ((i++))
> done

算術表達式

forwhile 以後出現的 ((...)) 稱爲算術表達式。算術表達式可獨立存在,也能夠與 ifforwhile 配合使用。

((a = 1))a=1 等價;算術表達式中的 = 兩側容許出現空格。

算術表達式中的比較運算可用於 if 語句。例如

$ if ((3 > 1)); then echo "Yes"; else echo "No"; fi
Yes
$ if ((3 < 1)); then echo "Yes"; else echo "No"; fi
No

注意,for 循環結構中的算術表達式是由三個算術表達式構成,即

((表達式 1; 表達式 2; 表達式 3;))

這種算術表達式只能在 for 循環結構中使用。表達式 1 在循環開始時被求值。表達式 2 是在每一輪循環以前被求值,求值結果能夠控制循環中止的時機:若求值結果爲 0,則循環中止,不然開始新一輪循環。表達式 3 是在每一輪循環結束後被求值。上一節的 while 循環即是對 for 循環表達式很好的解釋。

能夠像變量展開那樣得到算術表達式的求值結果。例如

$ echo $((3 > 1))
1
$ echo $((3 < 1))
0

務必弄清楚上述算術表達式的求值結果與命令退出狀態的區別。

test[ 命令中,數字之間的相等以及大小比較,能夠用 -eq-lt-gt-le-ge,分別表示相等、小於、大於、不大於以及不小於。所以,在使用 test 命令時,要清楚是數字的比較仍是字串的比較。

函數

與變量是數據的名字類似,函數是過程的名字。所謂過程,即按照時間順序給出一組命令,讓 Bash 依序執行每條命令。

md5sum 命令能夠算出給定文件的 MD5 碼。例如,

$ md5sum foo.jpg
95e25f85ee3b71cd17c921d88f2326bf  foo.jpg

文件的 MD5 碼,相似於咱們的指紋。不一樣的人,指紋相同的機率很小。不一樣的文件,MD5 碼相同的機率也很小。許多網站在收到用戶上傳的圖片以後,會以圖片文件的 MD5 碼做爲圖片文件的名字,以此避免一樣的圖片存入數據庫中,從而達成節省硬盤空間的目的。咱們能夠構造一個過程,將一份文件以它的 MD5 碼從新命名,亦即咱們要寫一個函數。

先來考慮,這個過程應當分爲哪些步驟。首先可使用 md5sum 算出文件的 MD5 碼。可是觀察上述 md5sum 的輸出,須要設法將 MD5 碼以後的空格以及文件名去除,可是要保留文件名的後綴(例如 .jpg),繼而將剩餘的 MD5 碼以及文件名的後綴組合爲文件名,用這個名字對文件進行從新命名。

若實現上述過程,現有的 Bash 知識尚且不夠。譬如,md5sum 命令輸出的信息,如何將其做爲數據,爲其命名,從而獲得一個變量?Bash 容許在變量的定義中臨時啓用自身的一個複本即子 Shell 去執行一些命令,然後將命令的輸出到 stdout 的信息做爲數據出如今變量的定義中。例如,

$ md5_info="$(md5sum foo.jpg)"
$ echo $md5_info
95e25f85ee3b71cd17c921d88f2326bf foo.jpg

(...) 即是子 Shell,括號以內的文本即是要交由 Bash 的子 Shell 執行的命令。若讓子 Shell 所執行命令的輸出做爲數據,須要使用 $ 對子 Shell 予以展開。

持有變量 md5_info 以後,可使用 AWK 實現 MD5 碼和文件名的後綴的組合。AWK 值得投入一些時間去學習它的基本用法,尤爲是尚不知有什麼好方法基於

95e25f85ee3b71cd17c921d88f2326bf foo.jpg

生成

95e25f85ee3b71cd17c921d88f2326bf.jpg

的此刻。

如下命令可從 $md5_info 中提取 MD5 碼和文件名稱後綴:

$ echo $md5_info | awk '{print $1}'
95e25f85ee3b71cd17c921d88f2326bf
$ echo $md5_info | awk 'BEGIN{FS="."} {print $NF}'
jpg

利用 Bash 的子 Shell,即可以將上述兩條命令合併到一個新的變量的定義中,即

$ new_name="$(echo $md5_info | awk '{print $1}').$(echo $md5_info | awk 'BEGIN{FS="."} {print $NF}')"
$ echo $new_name
95e25f85ee3b71cd17c921d88f2326bf.jpg

上述 new_name 的定義很長,不便理解,能夠像下面這樣多用兩個變量對較長的變量定義以予以拆分:

$ md5_code="$(echo $md5_info | awk '{print $1}')"
$ suffix_name="$(echo $md5_info | awk 'BEGIN{FS="."} {print $NF}')"
$ new_name="$md5_code.$suffix_name"
$ echo $new_name
95e25f85ee3b71cd17c921d88f2326bf.jpg

有了 new_name 變量,接下來只需使用 mvfoo.jpg 從新命名:

$ mv foo.jpg $new_name

大功告成……能夠將上述命令所造成的過程以函數對其命名了,即

$ function rename_by_md5 {
>     md5_info="$(md5sum foo.jpg)"
>     md5_code="$(echo $md5_info | awk '{print $1}')"
>     suffix_name="$(echo $md5_info | awk 'BEGIN{FS="."} {print $NF}')"
>     new_name="$md5_code.$suffix_name"
>     mv foo.jpg $new_name
> }
:假若你想親手在終端裏輸入上述代碼,不要忘記, $ 是一級命令提示符, > 是二級命令提示符,它們沒必要輸入。

rename_by_md5 即是 {} 所包圍的這組命令的名字。在終端裏,能夠像命令那樣使用這個名字,

$ rename_by_md5

結果便會將當前目錄的 foo.jpg 從新命名爲 95e25f85ee3b71cd17c921d88f2326bf.jpg。若是接下來再次使用這個名字,Bash 便會抱怨,沒有 foo.jpg 這個文件:

$ rename_by_md5
md5sum: foo.jpg: No such file or directory
mv: cannot stat 'foo.jpg': No such file or directory

這是由於 rename_by_md5 所指代的過程只能對 foo.jpg 文件從新命名。若已經對 foo.jpg 完成了從新命名,那麼 foo.jpg 就不存在了,因此再次使用 rename_by_md5,便失效了。這樣很差。函數應當可以變量那樣,一經定義,即可重複使用。函數的重複使用,對於 rename_by_md5 意味着什麼呢?意味着它所指代的過程不該當僅依賴 foo.jpg,而應當將這個過程所處理的文件名視爲一個未知數。學過中學數學的咱們應當很容易理解,函數的自變量就是未知數。上面定義的 rename_by_md5 裏沒有自變量,所以它雖然是函數,但其實是一個常函數。

在 Bash 語言裏,函數的自變量不像咱們在數學裏所熟悉的 xyz 這些 ,而是 123……它們皆爲變量,若得到它們指代的數據,須要用 $。掌握了這一知識,可對上述的 rename_by_md5 予以修改

$ function rename_by_md5 {
>     md5_info="$(md5sum $1)"
>     md5_code="$(echo $md5_info | awk '{print $1}')"
>     suffix_name="$(echo $md5_info | awk 'BEGIN{FS="."} {print $NF}')"
>     new_name="$md5_code.$suffix_name"
>     mv $1 $new_name
> }

亦即,將 rename_by_md5 原定義中出現那的 foo.jpg 所有更換爲 $1,值得注意的是,這個 $1awk 命令中的 $1 並不相同,並且 awk 命令也不會理睬前者。

如今的 rename_by_md5 能夠用於任何文件的從新命名,例如:

$ rename_by_md5 foo.jpg
$ rename_by_md5 bar.jpg

rename_by_md5 的定義中的 $1 所指代的值即是在上述命令中的輸入數據。這樣的輸入數據稱爲函數的參數。會寫支持一個參數的函數,想必寫支持兩個、三個或更多個參數的函數,並不難吧?不難。

腳本

上一節所討論的函數 rename_by_md5,是在一個終端裏定義的。若終端關閉,再開啓,這個函數的定義便不復存在。爲了讓它恆久地存在,可將其添加到 ~/.bashrc 文件裏,而後

$ source ~/.bashrc

或者在下次啓動終端以後,Bash 便知道 rename_by_md5 是已定義的函數。不過,另有更好的方法可令 rename_by_md5 恆久存在。

爲什麼放在 ~/.bashrc 中,Bash 便能獲得函數的定義?不止如此,在上文中,咱們也是在這份文件中定義了變量 PS1。想必在每次打開終端之時,終端裏運行的 Bash 必定是讀取了這份文件,並執行了文件中的命令。像 ~/.bashrc 這樣的文件稱爲 Bash 腳本。咱們也能夠寫與之相似的腳本,只是沒法像 ~/.bashrc 那樣特殊,在打開終端時就被 Bash 讀取。不過也不必那樣特殊,由於已經有了 ~/.bashrc,並且咱們也能夠向它寫入信息。

本身寫的 Bash 腳本,若它具有可執行權限,而且所在的目錄位於 PATH 變量定義中的目錄列表中,這份腳本文件的名字能夠做爲命令使用。寫腳本的過程,一般稱爲腳本編程,意思是用腳本編寫程序。或許你還不知道什麼是文件的可執行權限以及 PATH 變量又是什麼。

變量是數據的名字。函數是過程的名字。那麼,命令是誰的名字?是可執行文件的名字。何謂可執行文件?具備可執行權限的文件。例如,在 /tmp 目錄建立一份空文件 foo,而後把它做爲命令去執行:

$ cd /tmp
$ touch foo
$ ./foo

cd 命令用於從當前目錄跳轉至指定目錄。./foo 的意思是將當前目錄(即 ./) 中的文件 foo 做爲命令執行,結果獲得的是 Bash 冷冰冰的拒絕:

bash: ./foo: Permission denied

此時,若查看 $? 的值,結果爲 126:

$ echo $?
126

按照 Bash 的約定,命令的退出狀態爲非 0,意味着命令對應的程序出錯,爲 0 則意味着命令對應的程序成功地完成了本身的任務。這一約定也決定了 if 語句是以命令的退出狀態爲 0 時表示條件爲真,不然條件爲假。

接下來,能夠用 ls 命令查看 foo 文件所具備的權限:

$ ls -l foo
-rw-r--r-- 1 garfileo users 0 Dec  2 11:17 foo

即便看不懂 ls 命令輸出的信息的含義也不要緊,接下來,使用 chmod 命令爲 foo 增長可執行權限,而後再用 ls 命令查看它的權限:

$ chmod +x foo
$ ls -l foo
-rwxr-xr-x 1 garfileo users 0 Dec  2 11:17 foo

此次 ls 命令輸出的結果與上一次有何不一樣?

如今,再次執行 ./foo,並查看其退出狀態:

$ ./foo
$ echo $?
0

雖然執行 ./foo 以後,終端什麼也沒有輸出,可是這條命令的退出狀態爲 0,這代表 foo 是一個程序,而且成功地完成了本身什麼也沒有作的任務。

如今,將 foo 文件從 /tmp 目錄移動到 ~/.myscript 目錄,若後者不存在,可以使用 mkdir 命令建立。還記得 -d 嗎?

$ dest=~/.myscript
$ if [ ! -d $dest ]; then mkdir $dest; fi
$ mv /tmp/foo $dest

在此,能夠複習一下條件語句。-d $dest 表示「$dest 存在」,其前加上 ! 便表示「$dest 不存在」,前面再出現 if 便表示「若是 $dest 不存在」。若是 $dest 不存在,當如何?「then mkdir $dest」。事實上,這裏不必使用條件語句。由於 mkdir 有一個選項 -p,假若欲建立的目錄已存在,-p 選項可讓 mkdir 中止建立這個目錄的行爲。所以,上述命令可等價地寫爲

$ dest=~/.myscript
$ mkdir -p $dest
$ mv /tmp/foo $dest

如今,foo 文件位於 ~/.myscript 目錄。只需將 ~/.myscript 添加到 PATH 所指代的目錄列表,而後即可以將 foo 文件的名字 foo 做爲命令使用:

$ echo "PATH=~/.myscript:$PATH" >> ~/.bashrc
$ source ~/.bashrc
$ echo $PATH
/home/garfileo/.myscript:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/bin
$ foo
$ echo $?
0

在個人機器裏,~/.myscript 即是 /home/garfileo/.myscript,將這個目錄添加到 PATH 所指代的目錄列表以後,每當我在終端中輸入 foo,Bash 便會從 $PATH 裏的目錄查找與命令 foo 同名的具備可執行權限的文件,而後將其做爲程序運行。

如今能夠試着在 ~/.myscript 目錄寫一份名爲 rename_by_md5 的腳本了!

Here Document

不過,在你打開一個文本編輯器,打算在 ~/.myscript 建立一份名爲 rename_by_md5 的腳本文件以前,我以爲有必要給出使用 cat 命令寫簡單文件的方法:

$ cat << 'EOF' > ~/.myscript/rename_by_md5
> #!/bin/bash
> function rename_by_md5 {
>     md5_info="$(md5sum $1)"
>     md5_code="$(echo $md5_info | awk '{print $1}')"
>     suffix_name="$(echo $md5_info | awk 'BEGIN{FS="."} {print $NF}')"
>     new_name="$md5_code.$suffix_name"
>     mv $1 $new_name
> }
> rename_by_md5 $1
> EOF

Bash 將這種寫文件的方法稱爲 Here Document。命令中的第一個 EOF,用於設定文件的結束標誌。第二個 EOF 意味着寫文件過程至此終止。可使用本身喜歡的標誌代替 EOF。例如,

$ cat << '很任性地結束' > ~/.myscript/rename_by_md5
> #!/bin/bash
> ... ... ...
> rename_by_md5 $1
> 很任性地結束

注意,設定文件結束標誌時,單引號字串形式的標誌並不是必須,可是單引號可以阻止 Bash 對正要寫入文件的內容中的一些對它具備特殊含義的字符自做聰明地予以替換。

執行上述寫文件的命令以後,可使用 cat 查看 ~/.myscript/rename_by_md5:

$ cat ~/.myscript/rename_by_md5
#!/bin/bash
function rename_by_md5 {
    md5_info="$(md5sum $1)"
    md5_code="$(echo $md5_info | awk '{print $1}')"
    suffix_name="$(echo $md5_info | awk 'BEGIN{FS="."} {print $NF}')"
    new_name="$md5_code.$suffix_name"
    mv $1 $new_name
}
rename_by_md5 $1

四兩撥千斤

若是當前目錄裏有幾千份圖片文件須要用 rename_by_md5 命令進行從新命名,該如何作呢?如今,對於咱們而言,只費吹灰之力而已,

$ for i in *; do rename_by_md5 $i; done

這個例子展現了 for 循環的另外一種形式。* 名曰「通配符」,表示當前目錄全部的文件或目錄。因此,ls * 可在終端裏顯示當前目錄的全部文件。mv * /tmp 則可將當前目錄裏的全部文件移動到 /tmp 目錄。for i in * 的意思是「對當前目錄中的任一份文件 i」。對當前目錄中的任一份文件 i 作什麼?「rename_by_md5 $i」。

陷阱

與人類語言相似,稍有不甚,所說的話就會出現語病。Bash 語言亦如此。假設,當前目錄有一份名爲「a b.txt」的文件,若使用 md5sum 命令生成該文件的 MD5 碼,命令若寫成

$ md5sum a b.txt

即是錯的。由於 md5sum 會覺得咱們讓它爲文件 a 和文件 b.txt 生成 MD5 碼,並且 md5sum 的確支持這樣作。對於名字含有空格的文件,在命令中,請務必使用雙引號囊括起來:

$ md5sum "a b.txt"

如此便不會令 md5sum 產生誤解。所以,上文中給出的

md5_info="$(md5sum $1)"

安全起見,應當將其寫爲

md5_info="$(md5sum "$1")"

同理,

$ for i in *; do rename_by_md5 $i; done

應當寫爲

$ for i in *; do rename_by_md5 "$i"; done

有人已將 Bash 的常見陷阱總結成文,詳見《Bash Pitfalls》,可待熟悉 Bash 語言並用它編寫較爲重要的程序時再行觀摩。

結語

如今,請允許我華麗地退場:

$ exit
相關文章
相關標籤/搜索