Shell程式設計——極簡教程

阿基米東發表於2018-04-08

Shell的基礎

在 Linux 下,我們通常會出於以下原因或優點而使用 Shell 指令碼:

  • Shell 指令碼在處理自動迴圈或大的任務方面可節省大量的時間,且功能強大;
  • 建立一個指令碼,在使用一系列系統命令的同時,可以使用變數、條件、算術和迴圈快速建立指令碼以完成相應的工作(這可比在命令列下一個個敲入要節省大量的時間呢);
  • Shell指令碼可以在行命令中接收資訊,並使用它作為另一個命令的輸入。

Shell指令碼的一般格式

  Shell指令碼通常不是複雜的程式,並且它是按行解釋的。指令碼第一行通常會以類似於 #!/bin/bash 開始,這段指令碼用於通知 Shell 使用系統上的 Bourne Shell 直譯器。
  為什麼說“類似於”呢?因為,實際上我們不僅可以使用 bash 直譯器,還可以使用其他一些直譯器,甚至是以命令開頭,後面緊跟其引數。例如:

#!/usr/bin/awk
#!/bin/sed

Shell命令的萬用字元

  星號“ * ”可以匹配檔名中的任何字串。例如我們給出檔名模式 file*,它的意思是檔名以 file 開頭,後面可以跟隨任何字串,包括空字串。

$ ls file*
file    file1   file2   file3   file_test

  注意:在萬用字元裡,一個星號“ * ”可以代表0個或多個任意字元。

  問號“ ? ”可以匹配任何單個字元。例如我們給出檔名模式 file?,它的意思是檔名以 file 開頭,以任意1個字元結尾的檔案:

$ ls file?
file1   file2   file3

  注意:一個問號“ ? ”要匹配1個任意字元。

  方括號“ [ ] ”可以匹配任意單個指定的字元。下面的例子將列出檔名以 file 開頭,以任意1個數字結尾的檔案:

$ ls file[0-9]
file1  file2  file3

  方括號“ [! ] ”可以匹配任意除指定的字元之外的單個字元。下面的例子中將列出檔名以 file 開頭,不以數字結尾的檔案:

$ ls file*[!0-9]
file_test

Shell命令的輸入和輸出

  在 Shell 指令碼中,可以用幾種不同的方式讀入資料,可以使用標準輸入(預設為鍵盤),或者指定一個檔案作為輸入。
  對於輸出也一樣,如果不指定某個檔案作為輸出,標準輸出總是和終端螢幕相關聯。如果所使用的命令出現了什麼錯誤,它也會預設輸出到螢幕上,如果不想把這些資訊輸出到螢幕上,也可以把這些資訊指定到一個檔案中。
  使用 echo 命令可以顯示文字行或變數,或者把字串輸入到檔案。它的一般形式如下:

echo string

  echo 命令支援轉義字元,比如:

  • 不換行:“ \c ”
  • 跳格:“ \t ”
  • 換行:“ \n ”
echo -e "hello\tworld\n"

  可以使用 read 語句從鍵盤或檔案的某一行文字中讀入資訊,並將其賦給一個變數。如果只指定了一個變數,那麼 read 將會把所有的輸入賦給該變數,直至遇到第一個檔案結束符或回車。它的一般形式如下:

read var1 var2 ... ...

  通常,我們希望在讀取輸入的同時給出一些提示資訊:

read -p "Please input your name: " name

UNIX從來都不是為人機互動而設計的,而是為程式之間的互動而設計的。

  上面這句話是 Unix 的一個設計哲學,我們通常也會想到 Unix 的另一個設計哲學——一個程式只做好一件事。
  好啦,說那麼多,其實都是為了引出“管道”的概念。在 Unix 中,程式可以被看成是過濾器,程式之間的互動就是輸入和輸出。Unix 從很早以前就提供了管道機制,使得一個程式的輸出可以通過一根管子(管道)與另一個程式的輸入聯絡起來。
  管道在 Shell 中被廣泛使用,可以用豎槓“ | ”表示。它的一般形式如下:

命令1 | 命令2

  表示把命令1的輸出通過管道傳遞給命令2作為輸入。
  舉個栗子:我們先執行命令 ls,列出當前檔名,然後將結果送入管道中,進而 wc 從管道讀出這些資訊,並計算總共有幾個單詞:

$ ls | wc -w

  下面再來扯一下標準輸入、標準輸出和錯誤輸出,每個程式開始執行時都會預設開啟這三個檔案,其檔案描述符(fd)分別為0、1、2。

  • 標準輸入(stdin)的檔案描述符為0,它是命令的輸入,預設是鍵盤,也可以是檔案或其他命令的輸出;
  • 標準輸出(stdout)的檔案描述符為1,它是命令的輸出,預設是螢幕,也可以是檔案;
  • 標準錯誤(stderr)的檔案描述符為2,它是命令的錯誤輸出,預設是螢幕,同樣也可以是檔案。

那我們怎麼重新指定命令的標準輸入、標準輸出和錯誤輸出呢?要實現這一點就需要使用檔案重定向

  在對標準錯誤進行重定向時,必須要使用檔案描述符,但是對於標準輸入和輸出來說,這不是 necessary。比如:
  把標準輸出重定向到一個新檔案:command 1 > file 或者 command > file
  把標準錯誤重定向到一個新檔案:command 2 > file
  以 file 檔案作為標準輸入:command 0 < file
  如果希望以追加的方式重定向到一個檔案,則把“ > ”替換為“ >> ”。

Shell命令的執行順序

  在執行某個命令的時候,有時需要依賴於前一個命令是否執行成功。例如,你希望將一個目錄中的檔案全部拷貝到另一個目錄中後,再刪除源目錄中的全部檔案。因此,再刪除之前,你希望能夠確保拷貝成功,否則就可能丟失所有的檔案。
  那麼,你可以這麼做:

cp -rf dir1/* dir2/* && rm -rf dir1/*

  使用“ && ”的一般形式為:

命令1 && 命令2

  這種命令執行方式相當地直接:&& 左邊的命令(命令1)返回真(即返回0,成功被執行)後,&& 右邊的命令(命令2)才能夠被執行。
  換句話說,使用 && 的意思就是:

如果這個命令(命令1)執行成功 && 那麼執行這個命令(命令2)

  相應的還有“ || ”,它的一般形式為:

命令1 || 命令2

  與 && 相反,|| 的作用是:如果 || 左邊的命令(命令1)未執行成功,那麼就執行 || 右邊的命令(命令2)。
  換句話說,使用 || 的意思就是:

如果這個命令(命令1)執行失敗了 || 那麼就執行這個命令(命令2)

  舉個例子:我們希望從一個審計檔案中抽取第1個和第5個域,並將其輸出到一個臨時檔案中,如果這一操作未能成功,我們希望能夠看到錯誤提示。

awk '{print $1 $5}' acc > a.tmp || echo "operation failed"

grep命令

  grep 是 Unix/Linux 中使用最廣泛的命令之一。grep(Globally search a Regular Expression and Print)是一個強大的文字搜尋工具,它能使用特定模式匹配搜尋文字,並預設輸出匹配行。grep 支援基本正規表示式,也支援其擴充套件集,Unix 的 grep 家族還包括 egrep 和 fgrep。
  grep 一般格式為:

grep [選項] 基本正規表示式 [檔案]

  這裡基本正規表示式可為字串。在 grep 命令中輸入字串引數時,最好將其用雙引號括起來,這樣做有兩個好處:一是以防被誤解為 shell 命令,二是可以用來查詢多個單片語成的字串。

  以下是一些常用的 grep 選項:

選項 描述
-c 只輸出匹配行的計數
-i 不區分大小寫(只適用於單字元)
-h 查詢多檔案時不顯示檔名
-l 查詢多檔案時只輸出包含匹配字元的檔名
-n 顯示匹配行及行號
-s 不顯示不存在或無匹配文字的錯誤資訊
-v 顯示不包含匹配文字的所有行

正規表示式

  隨著對 Unix 和 Linux 熟悉程度的不斷加深,需要經常接觸到正規表示式這個領域。
  使用 shell 時,從一個檔案中抽取多於一個字串將會很麻煩。例如:在一個文字中抽取一個詞,它的頭兩個字元是大寫的,後面緊跟四個數字。如果不使用某種正規表示式,在 shell 中將無法實現這個操作。
  當從一個檔案或命令輸出中抽取或過濾文字時,可以使用正規表示式(RE),正規表示式是一些特殊或不很特殊的字串模式的集合。
  為了抽取或獲得資訊,我們給出抽取操作應遵守的一些規則。這些規則由一些特殊字元或進行模式匹配操作時使用的元字元組成。

選項 描述
^ 只匹配行首
$ 只匹配行尾
* 一個單字元後緊跟 *,匹配0個或多個此單字元
[] 匹配 [] 內字元,可以是一個單字元,也可以是字元序列
\ 用來遮蔽一個元字元的特殊含義
. 匹配任意單字元
pattern\{n\} 用來匹配前面 pattern 出現次數,n 為次數
pattern\{n, \} 含義同上,但次數最少為 n
pattern\{n, m\} 含義同上,但 pattern 出現次數在 n 與 m 之間

上古神器:awk 與 sed

awk命令

有3種方法呼叫 awk
(1)命令列方式:

awk [-F 分隔符] 'awk命令' <待處理檔案>

  注意:[-F 分隔符] 是可選的,awk 使用空格作為預設的分隔符。

(2)將所有 awk 命令插入一個單獨的檔案,然後呼叫:

awk -f '包含awk命令的檔案' 待處理檔案

(3)將所有 awk 命令插入一個檔案,並使 awk 程式可執行,然後用 awk 命令直譯器作為指令碼的首行,以便通過鍵入指令碼名稱來呼叫它。

awk 的模式和動作

  任何 awk 語句都由模式和動作組成。在一個 awk 指令碼中可能有許多語句。模式部分決定動作語句何時觸發及觸發事件。處理即對資料進行的操作,如果省略模式部分,動作將時刻保持執行狀態。
  實際動作在大括號 { } 內指明。動作大多數用來列印,但是還有些更長的程式碼諸如 if 和迴圈語句及迴圈退出結構。如果不指明採取動作,awk將列印出所有瀏覽出來的記錄。

awk 的域和記錄

  awk 執行時,其瀏覽域標記為 $1, $2, ..., $n ,這種方法稱為域標識。使用這些域標識將更容易對域進行更進一步的處理。
  例如,如下命令的作用是列印檔案 file 中的第一個域:

awk '{print $1}' file

  (注意:上面示例中沒有模式,只有動作,即 { } 裡面的語句。)

awk 的條件操作符示例

操作符 含義 命令 描述
< 小於 awk '$7<30 {print $0}' file $7小於30的行列印出來
<= 小於等於 awk '$7<=30 {print $0}' file $7小於等於30的行列印出來
== 等於 awk '$7==30 {print $0}' file $7等於30的行列印出來
!= 不等於 awk '$7!=30 {print $0}' file $7不等於30的行列印出來
>= 大於等於 awk '$7>=30 {print $0}' file $7大於等於30的行列印出來
~ 匹配 awk '$0~/48/ {print $0}' file 將能匹配48的行列印出來
!~ 不匹配 awk '$0!~/48/ {print $0}' file 將不能匹配48的行列印出來

sed命令

  sed 是一個非互動性文字流編輯器,它編輯檔案或標準輸入匯出的文字拷貝。標準輸入可能是來自鍵盤,檔案重定向,字串或變數,又或者是一個管道的文字。
  使用 sed 需要記住的一個重要事實是:無論命令是什麼,sed 並不與初始化檔案打交道,它操作的只是一個拷貝,然後所有的改動如果沒有重定向到一個檔案,將輸出到螢幕。
  跟 grep 和 awk 一樣,sed 是一個重要的文字過濾工具,或者使用一行命令或者使用管道與 grep 和 awk 相結合。
  呼叫 sed 有三種方式:
  (1)在命令列鍵入命令

sed [選項] 'sed命令' 輸入檔案

  注意:在命令列使用 sed 命令時,實際命令要加單引號,sed 也允許加雙引號。

  (2)將 sed 命令插入指令碼檔案,然後呼叫 sed

sed [選項] -f sed指令碼檔案 輸入檔案 

  (3)將 sed 命令插入指令碼檔案,並使 sed 指令碼可執行

sed指令碼檔案 [選項] 輸入檔案

Shell指令碼控制

結構化命令

  使用 if-then 語句,語法如下:

if [ command ]; then
    other commands
fi

或者:

if [ command ]; then
    other commands
else
    other commands
fi

  注意:當 command 退出碼為0時(即正常退出),執行 if 語句,否則執行 else 語句。
  在 shell 程式設計中,我們常常需要判斷各種條件,以便執行不同路徑。test 命令提供了在 if-then 語句中測試不同條件的途徑,如果 test 命令中列出的條件成立,那麼 test 命令將會退出且返回0。格式如下:

test condition

  Bash 提供了另一種在 if-then 語句中使用 test 的方法:

if [ confition ]; then
    ...
fi

  其中的條件判斷可分為3類:
  (1)數值比較

比較 描述
n1 -eq n2 檢查n1是否等於n2
n1 -ge n2 檢查n1是否大於或等於n2
n1 -gt n2 檢查n1是否大於n2
n1 -le n2 檢查n1是否小於或等於n2
n1 -lt n2 檢查n1是否小於n2
n1 -ne n2 檢查n1是否不等於n2

  (2)字串比較

比較 描述
str1 = str2 檢查str1與str2是否相同
str1 != str2 檢查str1與str2是否不同
str1 < str2 檢查str1是否小於str2
str1 > str2 檢查str1是否大於str2
-n str 檢查str的長度是否為非0
-z str 檢查str的長度是否為0

  (3)檔案比較

比較 描述
-d file 檢查file是否存在且是一個目錄
-e file 檢查file是否存在
-f file 檢查file是否存在且是一個普通檔案
-r file 檢查file是否存在且可讀
-s file 檢查file是否存在且非空
-w file 檢查file是否存在且可寫
-x file 檢查file是否存在且可執行

  雖然 if-then 語句可以勝任條件判斷/分支執行的工作,但是如果分支過多,則會導致程式碼臃腫。因此,可以使用 case 語句來替代,避免過長的 if-then 語句。shell 中的 case 語句類似於 C 語言的 switch 語句,一般形式如下:

case varible in
    pattern1 | pattern2) command1;;
    pattern3) command2;;
    *) command3;;
esac

  Shell 同樣支援迴圈語句,包括 for 命令和 while 命令。
  其中 for 迴圈格式如下:

for varible in list
do
    commands
done

  在 list 引數中,需要提供迭代中一系列要使用的值,在每個迭代中,varible 會包含列表中的當前值,一次使用一個值,以此類推。
  while 語句可以看成是 if-then 語句和 for 迴圈的混合。while 語句允許你定義一個要測試的命令,如果測試命令返回的退出狀態碼是0,則迴圈執行一組命令。格式如下:

while test command
do
    other commands
done

使用者輸入

  Shell 指令碼允許你在執行它的同時給它傳遞引數,例如:

./somescript.sh abcd 100

  這兩個引數,在指令碼里面可以使用 $1$2 來獲取,而 $0 代表的是指令碼名字本身。
  需要注意的一個細節是:當命令列引數超過 9 個,比如第 10 個引數,引用的時候必須使用花括號括起來,例如:${10},這種技術使得可以向指令碼新增任意多個引數。
  此外,我們還應該記住下面這些特殊的引數變數。
  $# 特殊變數代表指令碼執行時帶有的命令列引數個數(不包含指令碼名在內),對於上面的命令,$# 的值為2。
  這樣的話,如果我們想知道最後一個引數的值,就可以利用這個特殊變數,而不需要知道總共有多少個引數。噔!噔!噔!—— ${$#}
  $*$@ 變數的含義是相同的,它們會將命令列上提供的所有引數當作同一個字串中的多個獨立的單詞。這樣就可以使用 for 來遍歷所有的值:

#!/bin/bash

count=1
for param in $*
do
    echo "\$* : #$count = $param"
    count=$(($count + 1))
done

  我們輸入 ./test.sh a b c ,其輸出結果如下:

$* : #1 = a
$* : #2 = b
$* : #3 = c

指令碼函式

  函式的定義格式如下:

function_name()
{
    commands
}

需要注意的是:

  • 函式沒有返回值(事實上,所有的值都是字串);
  • 函式名後面有一對圓括號,括號裡面為空;
  • 函式的定義必須要在函式的呼叫之前;

雖然函式的定義中沒有出現引數列表,但是在呼叫函式的時候,依然可以傳參,像這樣:

function_name 12 34

  這樣的話,在函式定義內部,我們就可以使用 $1$2 等等來表示傳遞過來的引數裡。類似的,$0 表示函式本身的名字。


相關文章