天天看點

Shell腳本程式設計初體驗

通常,當人們提到“shell腳本語言”時,浮現在他們腦海中是bash,ksh,sh或者其它相類似的linux/unix腳本語言。腳本語言是與計算機交流的另外一種途徑。使用圖形化視窗界面(不管是windows還是linux都無所謂)使用者可以移動滑鼠并點選各種對象,比如按鈕、清單、選框等等。但這種方式在每次使用者想要計算機/伺服器完成相同任務時(比如說批量轉換照片,或者下載下傳新的電影、mp3等)卻是十分不友善。要想讓所有這些事情變得簡單并且自動化,我們可以使用shell腳本。

某些程式設計語言,像pascal、foxpro、c、java之類,在執行前需要先進行編譯。它們需要合适的編譯器來讓我們的代碼完成某個任務。

而其它一些程式設計語言,像php、javascript、visualbasic之類,則不需要編譯器,是以它們需要解釋器,而我們不需要編譯代碼就可以運作程式。

shell腳本也像解釋器一樣,但它通常用于調用外部已編譯的程式。然後,它會捕獲輸出結果、退出代碼并根據情況進行處理。

linux世界中最為流行的shell腳本語言之一,就是bash。而我認為(這是我自己的看法)原因在于,預設情況下bash shell可以讓使用者便捷地通過曆史指令(先前執行過的)導航,與之相反的是,ksh則要求對.profile進行一些調整,或者記住一些“魔術”組合鍵來查閱曆史并修正指令。

好了,我想這些介紹已經足夠了,剩下來哪個環境最适合你,就留給你自己去判斷吧。從現在開始,我将隻講bash及其腳本。在下面的例子中,我将使用centos 6.6和bash-4.1.2。請確定你有相同版本,或者更高版本。

<a target="_blank"></a>

shell腳本語言就跟和幾個人聊天類似。你隻需把所有指令想象成能幫你做事的那些人,隻要你用正确的方式來請求他們去做。比如說,你想要寫文檔。首先,你需要紙。然後,你需要把内容說給某個人聽,讓他幫你寫。最後,你想要把它存放到某個地方。或者說,你想要造一所房子,因而你需要請合适的人來清空場地。在他們說“事情幹完了”,那麼另外一些工程師就可以幫你來砌牆。最後,當這些工程師們也告訴你“事情幹完了”的時候,你就可以叫油漆工來給房子粉飾了。如果你讓油漆工在牆砌好前就來粉飾,會發生什麼呢?我想,他們會開始發牢騷了。幾乎所有這些像人一樣的指令都會說話,如果它們完成了工作而沒有發生什麼問題,那麼它們就會告訴“标準輸出”。如果它們不能做你叫它們做的事——它們會告訴“标準錯誤”。這樣,最後,所有的指令都通過“标準輸入”來聽你的話。

快速執行個體——當你打開linux終端并寫一些文本時——你正通過“标準輸入”和bash說話。那麼,讓我們來問問bash shell who am i(我是誰?)吧。

<code>root@localhost ~]# who am i &lt;--- 你通過标準輸入對 bash shell 說</code>

<code>root pts/0 2015-04-22 20:17 (192.168.1.123) &lt;--- bash shell通過标準輸出回答你</code>

現在,讓我們說一些bash聽不懂的問題:

<code>[root@localhost ~]# blablabla &lt;--- 哈,你又在和标準輸入說話了</code>

<code>-bash: blablabla: command not found &lt;--- bash通過标準錯誤在發牢騷了</code>

“:”之前的第一個單詞通常是向你發牢騷的指令。實際上,這些流中的每一個都有它們自己的索引号(lctt 譯注:檔案句柄号):

标準輸入(stdin) - 0

标準輸出(stdout) - 1

标準錯誤(stderr) - 2

如果你真的想要知道哪個輸出指令說了些什麼——你需要将那次發言重定向到(在指令後使用大于号“&gt;”和流索引)檔案:

<code>[root@localhost ~]# blablabla 1&gt; output.txt</code>

<code>-bash: blablabla: command not found</code>

在本例中,我們試着重定向流1(stdout)到名為output.txt的檔案。讓我們來看對該檔案内容所做的事情吧,使用cat指令可以做這事:

<code>[root@localhost ~]# cat output.txt</code>

<code>[root@localhost ~]#</code>

看起來似乎是空的。好吧,現在讓我們來重定向流2(stderr):

<code>[root@localhost ~]# blablabla 2&gt; error.txt</code>

好吧,我們看到牢騷話沒了。讓我們檢查一下那個檔案:

<code>[root@localhost ~]# cat error.txt</code>

果然如此!我們看到,所有牢騷話都被記錄到errors.txt檔案裡頭去了。

有時候,指令會同時産生stdout和stderr。要重定向它們到不同的檔案,我們可以使用以下語句:

<code>command 1&gt;out.txt 2&gt;err.txt</code>

要縮短一點語句,我們可以忽略“1”,因為預設情況下stdout會被重定向:

<code>command &gt;out.txt 2&gt;err.txt</code>

好吧,讓我們試試做些“壞事”。讓我們用rm指令把file1和folder1給删了吧:

<code>[root@localhost ~]# rm -vf folder1 file1 &gt; out.txt 2&gt;err.txt</code>

現在來檢查以下輸出檔案:

<code>[root@localhost ~]# cat out.txt</code>

<code>removed `file1'</code>

<code>[root@localhost ~]# cat err.txt</code>

<code>rm: cannot remove `folder1': is a directory</code>

正如我們所看到的,不同的流被分離到了不同的檔案。有時候,這也不是很友善,因為我們想要檢視出現錯誤時,在某些操作前面或後面所連續發生的事情。要實作這一目的,我們可以重定向兩個流到同一個檔案:

<code>command &gt;&gt;out_err.txt 2&gt;&gt;out_err.txt</code>

注意:請注意,我使用“&gt;&gt;”替代了“&gt;”。它允許我們附加到檔案,而不是覆寫檔案。

我們也可以重定向一個流到另一個:

<code>command &gt;out_err.txt 2&gt;&amp;1</code>

讓我來解釋一下吧。所有指令的标準輸出将被重定向到out_err.txt,錯誤輸出将被重定向到流1(上面已經解釋過了),而該流會被重定向到同一個檔案。讓我們看這個執行個體:

<code>[root@localhost ~]# rm -fv folder2 file2 &gt;out_err.txt 2&gt;&amp;1</code>

<code>[root@localhost ~]# cat out_err.txt</code>

<code>rm: cannot remove `folder2': is a directory</code>

<code>removed `file2'</code>

看着這些組合的輸出,我們可以将其說明為:首先,rm指令試着将folder2删除,而它不會成功,因為linux要求-r鍵來允許rm指令删除檔案夾,而第二個file2會被删除。通過為rm提供-v(詳情)鍵,我們讓rm指令告訴我們每個被删除的檔案或檔案夾。

這些就是你需要知道的,關于重定向的幾乎所有内容了。我是說幾乎,因為還有一個更為重要的重定向工具,它稱之為“管道”。通過使用|(管道)符号,我們通常重定向stdout流。

比如說,我們有這樣一個文本檔案:

<code>[root@localhost ~]# cat text_file.txt</code>

<code>this line does not contain h e l l o word</code>

<code>this lilne contains hello</code>

<code>this also containd hello</code>

<code>this one no due to hello all capital</code>

<code>hello bash world!</code>

而我們需要找到其中某些帶有“hello”的行,linux中有個grep指令可以完成該工作:

<code>[root@localhost ~]# grep hello text_file.txt</code>

當我們有個檔案,想要在裡頭搜尋的時候,這用起來很不錯。當如果我們需要在另一個指令的輸出中查找某些東西,這又該怎麼辦呢?是的,當然,我們可以重定向輸出到檔案,然後再在檔案裡頭查找:

<code>[root@localhost ~]# fdisk -l&gt;fdisk.out</code>

<code>[root@localhost ~]# grep "disk /dev" fdisk.out</code>

<code>disk /dev/sda: 8589 mb, 8589934592 bytes</code>

<code>disk /dev/mapper/volgroup-lv_root: 7205 mb, 7205814272 bytes</code>

<code>disk /dev/mapper/volgroup-lv_swap: 855 mb, 855638016 bytes</code>

如果你打算grep一些雙引号引起來帶有空格的内容呢!

注意:fdisk指令顯示關于linux作業系統磁盤驅動器的資訊。

就像我們看到的,這種方式很不友善,因為我們不一會兒就把臨時檔案空間給搞亂了。要完成該任務,我們可以使用管道。它們允許我們重定向一個指令的stdout到另一個指令的stdin流:

<code>[root@localhost ~]# fdisk -l | grep "disk /dev"</code>

如你所見,我們不需要任何臨時檔案就獲得了相同的結果。我們把fdisk stdout重定向到了grep stdin。

注意 : 管道重定向總是從左至右的。

還有幾個其它重定向,但是我們将把它們放在後面講。

正如我們所知道的,通常,與shell的交流以及shell内的交流是以對話的方式進行的。是以,讓我們建立一些真正的腳本吧,這些腳本也會和我們講話。這會讓你學到一些簡單的指令,并對腳本的概念有一個更好的了解。

假設我們是某個公司的總服務台經理,我們想要建立某個shell腳本來注冊呼叫資訊:電話号碼、使用者名以及問題的簡要描述。我們打算把這些資訊存儲到普通文本檔案data.txt中,以便今後統計。腳本它自己就是以對話的方式工作,這會讓總服務台的從業人員的小日子過得輕松點。那麼,首先我們需要顯示提問。對于顯示資訊,我們可以用echo和printf指令。這兩個都是用來顯示資訊的,但是printf更為強大,因為我們可以通過它很好地格式化輸出,我們可以讓它右對齊、左對齊或者為資訊留出專門的空間。讓我們從一個簡單的例子開始吧。要建立檔案,請使用你慣用的文本編輯器(kate,nano,vi,……),然後建立名為note.sh的檔案,裡面寫入這些指令:

<code>echo "phone number ?"</code>

在儲存檔案後,我們可以使用bash指令來運作,把我們的檔案作為它的參數:

<code>[root@localhost ~]# bash note.sh</code>

<code>phone number ?</code>

實際上,這樣來執行腳本是很不友善的。如果不使用bash指令作為字首來執行,會更舒服一些。要讓腳本可執行,我們可以使用chmod指令:

<code>[root@localhost ~]# ls -la note.sh</code>

<code>-rw-r--r--. 1 root root 22 apr 23 20:52 note.sh</code>

<code>[root@localhost ~]# chmod +x note.sh</code>

<code>-rwxr-xr-x. 1 root root 22 apr 23 20:52 note.sh</code>

注意 : ls指令顯示了目前檔案夾内的檔案。通過添加-la鍵,它會顯示更多檔案資訊。

如我們所見,在chmod指令執行前,腳本隻有讀(r)和寫(w)權限。在執行chmod +x後,它就獲得了執行(x)權限。(關于權限的更多細節,我會在下一篇文章中講述。)現在,我們隻需這麼來運作:

<code>[root@localhost ~]# ./note.sh</code>

在腳本名前,我添加了 ./ 組合。.(點)在unix世界中意味着目前位置(目前檔案夾),/(斜線)是檔案夾分隔符。(在windows系統中,我們使用反斜線 \ 表示同樣功能)是以,這整個組合的意思是說:“從目前檔案夾執行note.sh腳本”。我想,如果我用完整路徑來運作這個腳本的話,你會更加清楚一些:

<code>[root@localhost ~]# /root/note.sh</code>

它也能工作。

如果所有linux使用者都有相同的預設shell,那就萬事ok。如果我們隻是執行該腳本,預設的使用者shell就會用于解析腳本内容并運作指令。不同的shell的文法、内部指令等等有着一丁點不同,是以,為了保證我們的腳本會使用bash,我們應該添加#!/bin/bash到檔案首行。這樣,預設的使用者shell将調用/bin/bash,而隻有在那時候,腳本中的指令才會被執行:

<code>[root@localhost ~]# cat note.sh</code>

<code>#!/bin/bash</code>

直到現在,我們才100%确信bash會用來解析我們的腳本内容。讓我們繼續。

在顯示資訊後,腳本會等待使用者回答。有個read指令用來接收使用者的回答:

<code>read phone</code>

在執行後,腳本會等待使用者輸入,直到使用者按[enter]鍵結束輸入:

<code>12345 &lt;--- 這兒是我輸入的内容</code>

你輸入的所有東西都會被存儲到變量phone中,要顯示變量的值,我們同樣可以使用echo指令:

<code>echo "you have entered $phone as a phone number"</code>

<code>123456</code>

<code>you have entered 123456 as a phone number</code>

在bash shell中,一般我們使用$(美元)符号來表明這是一個變量,除了讀入到變量和其它為數不多的時候才不用這個$(将在今後說明)。

好了,現在我們準備添加剩下的問題了:

<code>echo "phone number?"</code>

<code>echo "name?"</code>

<code>read name</code>

<code>echo "issue?"</code>

<code>read issue</code>

<code>phone number?</code>

<code>123</code>

<code>name?</code>

<code>jim</code>

<code>issue?</code>

<code>script is not working.</code>

太完美了!剩下來就是重定向所有東西到檔案data.txt了。作為字段分隔符,我們将使用/(斜線)符号。

注意 : 你可以選擇任何你認為是最好的分隔符,但是確定檔案内容不會包含這些符号在内,否則它會導緻在文本行中産生額外字段。

别忘了使用“&gt;&gt;”來代替“&gt;”,因為我們想要将輸出内容附加到檔案末!

<code>[root@localhost ~]# tail -2 note.sh</code>

<code>echo "$phone/$name/$issue"&gt;&gt;data.txt</code>

<code>987</code>

<code>jimmy</code>

<code>keybord issue.</code>

<code>[root@localhost ~]# cat data.txt</code>

<code>987/jimmy/keybord issue.</code>

注意 : tail指令顯示了檔案的最後的n行。

搞定。讓我們再來運作一次看看:

<code>556</code>

<code>janine</code>

<code>mouse was broken.</code>

<code>556/janine/mouse was broken.</code>

我們的檔案在增長,讓我們在每行前面加個日期吧,這對于今後擺弄這些統計資料時會很有用。要實作這功能,我們可以使用date指令,并指定某種格式,因為我不喜歡預設格式:

<code>[root@localhost ~]# date</code>

<code>thu apr 23 21:33:14 eest 2015 &lt;---- date指令的預設輸出</code>

<code>[root@localhost ~]# date "+%y.%m.%d %h:%m:%s"</code>

<code>2015.04.23 21:33:18 &lt;---- 格式化後的輸出</code>

有幾種方式可以讀取指令的輸出到變量,在這種簡單的情況下,我們将使用`(是反引号,不是單引号,和波浪号~在同一個鍵位):

<code>now=`date "+%y.%m.%d %h:%m:%s"`</code>

<code>echo "$now/$phone/$name/$issue"&gt;&gt;data.txt</code>

<code>script hanging.</code>

<code>2015.04.23 21:38:56/123/jim/script hanging.</code>

嗯…… 我們的腳本看起來有點醜啊,讓我們來美化一下。如果你要手動讀取read指令,你會發現read指令也可以顯示一些資訊。要實作該功能,我們應該使用-p鍵加上資訊:

<code>read -p "phone number: " phone</code>

<code>read -p "name: " name</code>

<code>read -p "issue: " issue</code>

你可以直接從控制台查找到各個指令的大量有趣的資訊,隻需輸入:man read, man echo, man date, man ……

同意嗎?它看上去是舒服多了!

<code>phone number: 321</code>

<code>name: susane</code>

<code>issue: mouse was stolen</code>

<code>2015.04.23 21:43:50/321/susane/mouse was stolen</code>

光标在消息的後面(不是在新的一行中),這有點意思。(lctt 譯注:如果用 echo 指令輸出顯示的話,可以用 -n 參數來避免換行。)

是時候來改進我們的腳本了。如果使用者一整天都在接電話,如果每次都要去運作,這豈不是很麻煩?讓我們讓這些活動都永無止境地循環去吧:

<code>while true</code>

<code>do</code>

<code>done</code>

我已經交換了read phone和now=<code>date</code>行的位置。這是因為我想要在輸入電話号碼後再獲得時間。如果我把它放在循環的首行,那麼循環一次後,變量 now 就會在資料存儲到檔案中後馬上獲得時間。而這并不好,因為下一次呼叫可能在20分鐘後,甚至更晚。

<code>phone number: 123</code>

<code>name: jim</code>

<code>issue: script still not works.</code>

<code>phone number: 777</code>

<code>name: daniel</code>

<code>issue: i broke my monitor</code>

<code>phone number: ^c</code>

<code>2015.04.23 21:47:55/123/jim/script still not works.</code>

<code>2015.04.23 21:48:16/777/daniel/i broke my monitor</code>

注意: 要從無限循環中退出,你可以按[ctrl]+[c]鍵。shell會顯示^表示 ctrl 鍵。

讓我們添加更多功能到我們的“弗蘭肯斯坦(frankenstein)”,我想要腳本在每次呼叫後顯示某個統計資料。比如說,我想要檢視各個号碼呼叫了我幾次。對于這個,我們應該cat檔案data.txt:

<code>2015.04.23 22:02:14/123/jimmy/new script also not working!!!</code>

現在,所有輸出我們都可以重定向到cut指令,讓cut來把每行切成一塊一塊(我們使用分隔符“/”),然後列印第二個字段:

<code>[root@localhost ~]# cat data.txt | cut -d"/" -f2</code>

<code>321</code>

<code>777</code>

現在,我們可以把這個輸出重定向打另外一個指令sort:

<code>[root@localhost ~]# cat data.txt | cut -d"/" -f2|sort</code>

然後隻留下唯一的行。要統計唯一條目,隻需添加-c鍵到uniq指令:

<code>[root@localhost ~]# cat data.txt | cut -d"/" -f2 | sort | uniq -c</code>

<code>3 123</code>

<code>1 321</code>

<code>1 777</code>

隻要把這個添加到我們的循環的最後:

<code>echo "===== we got calls from ====="</code>

<code>cat data.txt | cut -d"/" -f2 | sort | uniq -c</code>

<code>echo "--------------------------------"</code>

運作:

<code>phone number: 454</code>

<code>name: malini</code>

<code>issue: windows license expired.</code>

<code>===== we got calls from =====</code>

<code>1 454</code>

<code>--------------------------------</code>

目前場景貫穿了幾個熟知的步驟:

顯示消息

擷取使用者輸入

存儲值到檔案

處理存儲的資料

但是,如果使用者有點責任心,他有時候需要輸入資料,有時候需要統計,或者可能要在存儲的資料中查找一些東西呢?對于這些事情,我們需要使用switches/cases,并知道怎樣來很好地格式化輸出。這對于在shell中“畫”表格的時候很有用。

<b>原文釋出時間為:2015-06-09</b>

<b>本文來自雲栖社群合作夥伴“linux中國”</b>

繼續閱讀