Shell腳本編程初體驗

jopen 9年前發布 | 21K 次閱讀 Shell

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

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

Shell腳本編程初體驗

而其它一些編程語言,像php、javascript、visualbasic之類,則不需要編譯器,因此它們需要解釋器,而我們不需要編譯代碼就可以運行程序。

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

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

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

Shell腳本流

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

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

root@localhost ~]# who am i                                <--- 你通過標準輸入對 bash shell 說
root     pts/0        2015-04-22 20:17 (192.168.1.123)     <--- bash shell通過標準輸出回答你

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

[root@localhost ~]# blablabla           <--- 哈,你又在和標準輸入說話了
-bash: blablabla: command not found     <--- bash通過標準錯誤在發牢騷了

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

  • 標準輸入(stdin) – 0
  • 標準輸出(stdout) – 1
  • 標準錯誤(stderr) – 2
  • </ul>

    如果你真的想要知道哪個輸出命令說了些什么——你需要將那次發言重定向到(在命令后使用大于號“>”和流索引)文件:

    [root@localhost ~]# blablabla 1> output.txt
    -bash: blablabla: command not found

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

    [root@localhost ~]# cat output.txt
    [root@localhost ~]#

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

    [root@localhost ~]# blablabla 2> error.txt
    [root@localhost ~]#

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

    [root@localhost ~]# cat error.txt
    -bash: blablabla: command not found
    [root@localhost ~]#

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

    有時候,命令會同時產生stdoutstderr。要重定向它們到不同的文件,我們可以使用以下語句:

    command 1>out.txt 2>err.txt

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

    command >out.txt 2>err.txt

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

    [root@localhost ~]# rm -vf folder1 file1 > out.txt 2>err.txt

    現在來檢查以下輸出文件:

    [root@localhost ~]# cat out.txt
    removed `file1'
    [root@localhost ~]# cat err.txt
    rm: cannot remove `folder1': Is a directory
    [root@localhost ~]#

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

    command >>out_err.txt 2>>out_err.txt

    注意:請注意,我使用“>>”替代了“>”。它允許我們附加到文件,而不是覆蓋文件。

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

    command >out_err.txt 2>&1

    讓我來解釋一下吧。所有命令的標準輸出將被重定向到out_err.txt,錯誤輸出將被重定向到流1(上面已經解釋過了),而該流會被重定向到同一個文件。讓我們看這個實例:

    [root@localhost ~]# rm -fv folder2 file2 >out_err.txt 2>&1
    [root@localhost ~]# cat out_err.txt
    rm: cannot remove `folder2': Is a directory
    removed `file2'
    [root@localhost ~]#

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

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

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

    [root@localhost ~]# cat text_file.txt
    This line does not contain H e l l o  word
    This lilne contains Hello
    This also containd Hello
    This one no due to HELLO all capital
    Hello bash world!

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

    [root@localhost ~]# grep Hello text_file.txt
    This lilne contains Hello
    This also containd Hello
    Hello bash world!
    [root@localhost ~]#

    當我們有個文件,想要在里頭搜索的時候,這用起來很不錯。當如果我們需要在另一個命令的輸出中查找某些東西,這又該怎么辦呢?是的,當然,我們可以重定向輸出到文件,然后再在文件里頭查找:

    [root@localhost ~]# fdisk -l>fdisk.out
    [root@localhost ~]# grep "Disk /dev" fdisk.out
    Disk /dev/sda: 8589 MB, 8589934592 bytes
    Disk /dev/mapper/VolGroup-lv_root: 7205 MB, 7205814272 bytes
    Disk /dev/mapper/VolGroup-lv_swap: 855 MB, 855638016 bytes
    [root@localhost ~]#

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

    注意:fdisk命令顯示關于Linux操作系統磁盤驅動器的信息。

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

    [root@localhost ~]# fdisk -l | grep "Disk /dev"
    Disk /dev/sda: 8589 MB, 8589934592 bytes
    Disk /dev/mapper/VolGroup-lv_root: 7205 MB, 7205814272 bytes
    Disk /dev/mapper/VolGroup-lv_swap: 855 MB, 855638016 bytes
    [root@localhost ~]#

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

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

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

    在shell中顯示自定義信息

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

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

    echo "Phone number ?"

    如何運行/執行腳本?

    在保存文件后,我們可以使用bash命令來運行,把我們的文件作為它的參數:

    [root@localhost ~]# bash note.sh
    Phone number ?

    實際上,這樣來執行腳本是很不方便的。如果不使用bash命令作為前綴來執行,會更舒服一些。要讓腳本可執行,我們可以使用chmod命令:

    [root@localhost ~]# ls -la note.sh
    -rw-r--r--. 1 root root 22 Apr 23 20:52 note.sh
    [root@localhost ~]# chmod +x note.sh
    [root@localhost ~]# ls -la note.sh
    -rwxr-xr-x. 1 root root 22 Apr 23 20:52 note.sh
    [root@localhost ~]#

    Shell腳本編程初體驗

    注意 : ls命令顯示了當前文件夾內的文件。通過添加-la鍵,它會顯示更多文件信息。

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

    [root@localhost ~]# ./note.sh
    Phone number ?

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

    [root@localhost ~]# /root/note.sh
    Phone number ?
    [root@localhost ~]#

    它也能工作。

    如果所有linux用戶都有相同的默認shell,那就萬事OK。如果我們只是執行該腳本,默認的用戶shell就會用于解析腳本內容并運行命令。不同的shell的語法、內部命令等等有著一丁點不同,所以,為了保證我們的腳本會使用bash,我們應該添加#!/bin/bash到文件首行。這樣,默認的用戶shell將調用/bin/bash,而只有在那時候,腳本中的命令才會被執行:

    [root@localhost ~]# cat note.sh

    !/bin/bash

    echo "Phone number ?"</pre>

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

    讀取輸入

    在顯示信息后,腳本會等待用戶回答。有個read命令用來接收用戶的回答:

    #!/bin/bash
    echo "Phone number ?"
    read phone

    在執行后,腳本會等待用戶輸入,直到用戶按[ENTER]鍵結束輸入:

    [root@localhost ~]# ./note.sh
    Phone number ?
    12345                               <--- 這兒是我輸入的內容
    [root@localhost ~]#

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

    [root@localhost ~]# cat note.sh

    !/bin/bash

    echo "Phone number ?" read phone echo "You have entered $phone as a phone number" [root@localhost ~]# ./note.sh Phone number ? 123456 You have entered 123456 as a phone number [root@localhost ~]#</pre>

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

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

    #!/bin/bash
    echo "Phone number?"
    read phone
    echo "Name?"
    read name
    echo "Issue?"
    read issue
    [root@localhost ~]# ./note.sh
    Phone number?
    123
    Name?
    Jim
    Issue?
    script is not working.
    [root@localhost ~]#

    使用流重定向

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

    注意 : 你可以選擇任何你認為是最好的分隔符,但是確保文件內容不會包含這些符號在內,否則它會導致在文本行中產生額外字段。

    別忘了使用“>>”來代替“>”,因為我們想要將輸出內容附加到文件末!

    [root@localhost ~]# tail -2 note.sh
    read issue
    echo "$phone/$name/$issue">>data.txt
    [root@localhost ~]# ./note.sh
    Phone number?
    987
    Name?
    Jimmy
    Issue?
    Keybord issue.
    [root@localhost ~]# cat data.txt
    987/Jimmy/Keybord issue.
    [root@localhost ~]#

    注意tail命令顯示了文件的最后的n行。

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

    [root@localhost ~]# ./note.sh
    Phone number?
    556
    Name?
    Janine
    Issue?
    Mouse was broken.
    [root@localhost ~]# cat data.txt
    987/Jimmy/Keybord issue.
    556/Janine/Mouse was broken.
    [root@localhost ~]#

    我們的文件在增長,讓我們在每行前面加個日期吧,這對于今后擺弄這些統計數據時會很有用。要實現這功能,我們可以使用date命令,并指定某種格式,因為我不喜歡默認格式:

    [root@localhost ~]# date
    Thu Apr 23 21:33:14 EEST 2015                     <---- date命令的默認輸出
    [root@localhost ~]# date "+%Y.%m.%d %H:%M:%S"
    2015.04.23 21:33:18                               <---- 格式化后的輸出

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

    [root@localhost ~]# cat note.sh

    !/bin/bash

    now=date "+%Y.%m.%d %H:%M:%S" echo "Phone number?" read phone echo "Name?" read name echo "Issue?" read issue echo "$now/$phone/$name/$issue">>data.txt [root@localhost ~]# ./note.sh Phone number? 123 Name? Jim Issue? Script hanging. [root@localhost ~]# cat data.txt 2015.04.23 21:38:56/123/Jim/Script hanging. [root@localhost ~]#</pre>

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

    [root@localhost ~]# cat note.sh

    !/bin/bash

    now=date "+%Y.%m.%d %H:%M:%S" read -p "Phone number: " phone read -p "Name: " name read -p "Issue: " issue echo "$now/$phone/$name/$issue">>data.txt</pre>

    你可以直接從控制臺查找到各個命令的大量有趣的信息,只需輸入:man read, man echo, man date, man ……

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

    [root@localhost ~]# ./note.sh
    Phone number: 321
    Name: Susane
    Issue: Mouse was stolen
    [root@localhost ~]# cat data.txt
    2015.04.23 21:38:56/123/Jim/Script hanging.
    2015.04.23 21:43:50/321/Susane/Mouse was stolen
    [root@localhost ~]#

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

    循環

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

    [root@localhost ~]# cat note.sh

    !/bin/bash

    while true do read -p "Phone number: " phone now=date "+%Y.%m.%d %H:%M:%S" read -p "Name: " name read -p "Issue: " issue echo "$now/$phone/$name/$issue">>data.txt done</pre>

    我已經交換了read phonenow=date行的位置。這是因為我想要在輸入電話號碼后再獲得時間。如果我把它放在循環的首行,那么循環一次后,變量 now 就會在數據存儲到文件中后馬上獲得時間。而這并不好,因為下一次呼叫可能在20分鐘后,甚至更晚。

    [root@localhost ~]# ./note.sh
    Phone number: 123
    Name: Jim
    Issue: Script still not works.
    Phone number: 777
    Name: Daniel
    Issue: I broke my monitor
    Phone number: ^C
    [root@localhost ~]# cat data.txt
    2015.04.23 21:38:56/123/Jim/Script hanging.
    2015.04.23 21:43:50/321/Susane/Mouse was stolen
    2015.04.23 21:47:55/123/Jim/Script still not works.
    2015.04.23 21:48:16/777/Daniel/I broke my monitor
    [root@localhost ~]#

    注意: 要從無限循環中退出,你可以按[Ctrl]+[C]鍵。Shell會顯示^表示 CTRL 鍵

    使用管道重定向

    讓我們添加更多功能到我們的“弗蘭肯斯坦(Frankenstein)”,我想要腳本在每次呼叫后顯示某個統計數據。比如說,我想要查看各個號碼呼叫了我幾次。對于這個,我們應該cat文件data.txt:

    [root@localhost ~]# cat data.txt
    2015.04.23 21:38:56/123/Jim/Script hanging.
    2015.04.23 21:43:50/321/Susane/Mouse was stolen
    2015.04.23 21:47:55/123/Jim/Script still not works.
    2015.04.23 21:48:16/777/Daniel/I broke my monitor
    2015.04.23 22:02:14/123/Jimmy/New script also not working!!!
    [root@localhost ~]#

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

    [root@localhost ~]# cat data.txt | cut -d"/" -f2
    123
    321
    123
    777
    123
    [root@localhost ~]#

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

    [root@localhost ~]# cat data.txt | cut -d"/" -f2|sort
    123
    123
    123
    321
    777
    [root@localhost ~]#

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

    [root@localhost ~]# cat data.txt | cut -d"/" -f2 | sort | uniq -c
        3 123
        1 321
        1 777
    [root@localhost ~]#

    只要把這個添加到我們的循環的最后:

    #!/bin/bash
    while true
    do
            read -p "Phone number: " phone
            now=`date "+%Y.%m.%d %H:%M:%S"`
            read -p "Name: " name
            read -p "Issue: " issue
            echo "$now/$phone/$name/$issue">>data.txt
            echo "===== We got calls from ====="
            cat data.txt | cut -d"/" -f2 | sort | uniq -c
            echo "--------------------------------"
    done

    運行:

    [root@localhost ~]# ./note.sh
    Phone number: 454
    Name: Malini
    Issue: Windows license expired.
    ===== We got calls from =====
        3 123
        1 321
        1 454

    1 777
    

    Phone number: ^C</pre>

    Shell腳本編程初體驗

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

    • 顯示消息
    • 獲取用戶輸入
    • 存儲值到文件
    • 處理存儲的數據
    • </ul>

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

       來源: linux.cn

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!