章節概覽#

本章介紹 Shell Script(也稱為 Bourne shell script)的基礎知識。Shell script 是一系列寫在檔案中的命令,shell 會像你在終端機輸入一樣逐行讀取並執行。本章涵蓋了 quoting、變數、條件判斷、迴圈、命令替換、暫存檔管理等核心概念,以及一些實用的 shell script 工具程式。


11.1 Shell Script 基礎#

Shell script 通常以 shebang 開頭,指定要用哪個程式來執行腳本:

#!/bin/sh
  • #! 稱為 shebang,告訴 kernel 使用 /bin/sh 來執行此腳本
  • 除了 shebang 之外,以 # 開頭的行為註解(comment)
  • 建立腳本後需設定執行權限:
chmod +rx script
  • 可以將腳本放在 command path 中的目錄裡,或使用 ./script 來執行

Shebang 不一定要是 #!/bin/sh,也可以用 #!/usr/bin/python#!/usr/bin/env python 等來指定其他直譯器。env 會在目前的 command path 中搜尋指定的程式。

11.1.1 Shell Script 的限制#

Shell script 擅長簡化與自動化檔案操作等任務,但有其限制:

  • 不適合複雜的字串處理、大量算術運算或存取複雜資料庫
  • 對於需要函式或複雜控制流程的場景,建議使用 Python、Perl、awk 或 C 語言
  • 應保持 shell script 簡短,Bourne shell script 不適合寫得太龐大

11.2 Quoting 與 Literals#

Shell 在執行命令前會進行變數替換glob 展開,因此正確使用引號非常重要。

11.2.1 Literals#

Literal 是你希望 shell 不做任何解析、直接傳遞的字串。需要保護的字元包括 $*; 等。

11.2.2 Single Quotes(單引號)#

  • 最簡單的方式建立 literal
  • 單引號之間的所有字元(包括空格)被視為單一參數
  • Shell 不會對單引號內的內容做任何替換
grep 'r.*t' /etc/passwd

11.2.3 Double Quotes(雙引號)#

  • 與單引號類似,但 shell 會展開變數$
  • 不會展開 glob(*
echo "There is no * in my path: $PATH"

11.2.4 Literal Single Quotes(處理單引號字面值)#

在 literal 中使用單引號本身比較棘手,有兩種方式:

  • 使用 backslash:echo I don\'t like contractions
  • 將單引號放在雙引號中:echo "I don't like contractions inside shell scripts."

如果需要引用包含單引號的完整字串且不做替換:先將所有 ' 替換為 '\'',再用單引號包住整個字串。


11.3 Special Variables(特殊變數)#

Shell script 使用特殊變數來存取命令列參數與執行狀態。

11.3.1 Individual Arguments: $1, $2, …#

  • $1$2 等代表腳本的第一個、第二個參數
  • 使用 shift 命令可移除 $1,讓 $2 變成 $1,依此類推
#!/bin/sh
echo First argument: $1
echo Third argument: $3

11.3.2 Number of Arguments: $##

  • $# 保存傳入腳本的參數數量
  • $# 為 0 時,表示沒有剩餘參數

11.3.3 All Arguments: $@#

  • $@ 代表腳本的所有參數,常用於將參數傳遞給腳本中的其他命令
#!/bin/sh
gs -q -dBATCH -dNOPAUSE -dSAFER -sOutputFile=- -sDEVICE=pnmraw $@

如果腳本中的命令行太長,可以使用 backslash(\)換行。

11.3.4 Script Name: $0#

  • $0 保存腳本名稱,適用於產生診斷訊息
  • 錯誤訊息應導向 standard error:
echo $0: bad option $BADPARM 1>&2

11.3.5 Process ID: $$#

  • $$ 保存目前 shell 的 process ID

11.3.6 Exit Code: $?#

  • $? 保存上一個命令的 exit code

11.4 Exit Codes#

當 Unix 程式結束時,會留下一個數值型的 exit code(又稱 exit value):

  • 0 表示成功
  • 非 0 通常表示錯誤
$ ls / > /dev/null
$ echo $?
0
$ ls /asdfasdf > /dev/null
$ echo $?
1

使用 $? 時必須在命令執行後立即取用或儲存,因為下一個命令會覆蓋它。

  • 在腳本中遇到錯誤需中止時,使用 exit 1 回傳非零 exit code
  • 某些程式(如 diffgrep)使用非零 exit code 表示正常狀態(例如 grep 找不到匹配回傳 1),真正的錯誤用 exit code 2

11.5 Conditionals(條件判斷)#

if/then/else#

#!/bin/sh
if [ $1 = hi ]; then
    echo 'The first argument was "hi"'
else
    echo -n 'The first argument was not "hi" -- '
    echo It was '"'$1'"'
fi
  • [ 是一個實際的程式(等同 test 命令),用於執行條件測試
  • if 執行 [ 後的命令,根據其 exit code 決定走 thenelse 分支
  • 分號 ;then 前是必要的(除非 then 另起一行)

11.5.1 空參數列的處理#

$1 可能為空時,[ = hi ] 會產生錯誤。解決方式:

if [ "$1" = hi ]; then
if [ x"$1" = x"hi" ]; then

11.5.2 使用其他命令做測試#

不限於 [ 命令,任何命令都可以用作 if 的測試條件:

if grep -q daemon /etc/passwd; then
    echo The daemon user is in the passwd file.
fi

11.5.3 elif#

使用 elif 串接多個條件判斷:

if [ "$1" = "hi" ]; then
    echo 'The first argument was "hi"'
elif [ "$2" = "bye" ]; then
    echo 'The second argument was "bye"'
else
    echo 'Neither condition matched'
fi

不要過度使用 elif,case 語法通常更適合多條件匹配。

11.5.4 Logical Constructs(邏輯運算)#

  • &&(AND):command1 && command2 — command1 成功才執行 command2
  • ||(OR):command1 || command2 — command1 失敗才執行 command2
  • [ 內可使用 -a(AND)和 -o(OR)
  • 使用 ! 反轉測試結果(logical NOT)

11.5.5 Testing Conditions(測試條件)#

test[)命令支援三類測試:

File Tests(檔案測試)

Operator測試內容
-f一般檔案(regular file)
-d目錄(directory)
-h符號連結(symbolic link)
-e檔案存在
-s檔案非空
-r可讀
-w可寫
-x可執行
  • Binary operators-nt(newer than)、-ot(older than)、-ef(同一 inode)

String Tests(字串測試)

Operator測試內容
=字串相等
!=字串不相等
-z字串為空
-n字串非空

Arithmetic Tests(算術測試)

  • = 比較的是字串相等性,不是數值。[ 1 = 1 ] 為真,但 [ 01 = 1 ] 為假
  • 數值比較使用:-eq-ne-lt-gt-le-ge

11.5.6 case#

case 語法適合做字串的 pattern matching,不執行 test 命令:

#!/bin/sh
case $1 in
    bye)
        echo Fine, bye.
        ;;
    hi|hello)
        echo Nice to see you.
        ;;
    what*)
        echo Whatever.
        ;;
    *)
        echo 'Huh?'
        ;;
esac
  • 使用 | 匹配多個字串,使用 *? 做 pattern matching
  • *) 為預設的 catch-all case
  • 每個 case 以 ;; 結尾

11.6 Loops(迴圈)#

Bourne shell 有 forwhile 兩種迴圈。

11.6.1 for Loops#

#!/bin/sh
for str in one two three four; do
    echo $str
done
  • for 將變數依序設為 in 後面的每個值,執行 dodone 之間的命令

11.6.2 while Loops#

while 迴圈使用 exit code 判斷是否繼續:

#!/bin/sh
FILE=/tmp/whiletest.$$;
echo firstline > $FILE

while tail -10 $FILE | grep -q firstline; do
    echo -n Number of lines in $FILE:' '
    wc -l $FILE | awk '{print $1}'
    echo newline >> $FILE
done

rm -f $FILE
  • 當測試的 exit code 為非零時,迴圈結束
  • 可用 break 跳出 while 迴圈
  • 也有 until 迴圈,在 exit code 為零時跳出(與 while 相反)

如果你發現需要頻繁使用 while/until 迴圈,可能應該考慮使用 Python 或 awk 等更適合的語言。


11.7 Command Substitution(命令替換)#

使用 $() 語法將命令的 standard output 捕獲到變數中或作為另一個命令的參數:

#!/bin/sh
FLAGS=$(grep ^flags /proc/cpuinfo | sed 's/.*://' | head -1)
echo Your processor supports:
for f in $FLAGS; do
    case $f in
        fpu)    MSG="floating point unit"
                ;;
        3dnow)  MSG="3DNOW graphics extensions"
                ;;
        *)      MSG="unknown"
                ;;
    esac
    echo $f: $MSG
done
  • 可在 command substitution 中使用單引號和 pipeline
  • 避免在腳本中使用 $(ls),直接用 shell glob(*)更快
  • 傳統語法使用 backtick(`),但 $() 是較新的 POSIX 標準,更易讀

如果需要對多個檔案執行命令,建議使用 pipeline 搭配 xargs,或 find -exec,而非 command substitution。


11.8 Temporary File Management(暫存檔管理)#

使用 mktemp 建立唯一的暫存檔名,避免檔名衝突:

#!/bin/sh
TMPFILE1=$(mktemp /tmp/im1.XXXXXX)
TMPFILE2=$(mktemp /tmp/im2.XXXXXX)

cat /proc/interrupts > $TMPFILE1
sleep 2
cat /proc/interrupts > $TMPFILE2
diff $TMPFILE1 $TMPFILE2
rm -f $TMPFILE1 $TMPFILE2
  • mktemp 會將模板中的 XXXXXX 替換為唯一字串
  • 如果腳本被中斷,暫存檔可能會遺留。使用 trap 來處理清理:
trap "rm -f $TMPFILE1 $TMPFILE2; exit 1" INT

使用 trap 時必須在 handler 中加上 exit,否則腳本會在執行完 signal handler 後繼續運行。


11.9 Here Documents#

Here document 用於在腳本中嵌入大段文字,避免使用多個 echo

#!/bin/sh
DATE=$(date)
cat <<EOF
The output above is from the Unix date command.
It's not a very interesting command.
Date: $DATE
EOF
  • <<EOF 將後續內容重導向到前面命令的 standard input
  • 遇到獨立一行的 EOF 時停止重導向
  • marker 可以是任意字串,但慣例使用全大寫
  • Shell 會展開 here document 中的變數

11.10 Important Shell Script Utilities#

11.10.1 basename#

basename 用於從路徑中去除目錄或副檔名:

$ basename example.html .html    # 輸出: example
$ basename /usr/local/bin/example  # 輸出: example
  • 常用於批次轉換檔案格式的腳本中

11.10.2 awk#

awk 是一個強大的程式語言,但常見用途是從輸入串流中擷取特定欄位:

$ ls -l | awk '{print $5}'
  • 上述命令印出 ls -l 輸出的第五個欄位(檔案大小)

11.10.3 sed#

sed(stream editor)是自動化文字編輯器,從輸入串流讀取、修改並輸出結果:

  • 替換文字:sed 's/exp/text/'
  • 全域替換:sed 's/:/%/g' /etc/passwd
  • 刪除特定行:sed 3,6d /etc/passwd
  • 刪除匹配行:sed '/exp/d'
  • 不指定檔案時,sed 從 standard input 讀取,常用於 pipeline 中

11.10.4 xargs#

xargs 用於對大量檔案逐一執行命令:

$ find . -name '*.gif' -print | xargs file
  • 搭配 find -print0xargs -0 可正確處理含空格或換行的檔名:
$ find . -name '*.gif' -print0 | xargs -0 file
  • 替代方案:find -exec
$ find . -name '*.gif' -exec file {} \;

使用 xargs 時,如果目標檔名可能以 - 開頭,在 xargs 命令後加上 -- 以避免被誤認為選項。

11.10.5 expr#

expr 用於在 shell script 中做算術運算:

$ expr 1 + 2    # 輸出: 3
  • expr 速度慢且笨拙,如果頻繁使用算術運算,建議改用 Python

11.10.6 exec#

exec 是一個 built-in shell 命令,用當前 shell 的 process 替換為指定程式:

  • 執行 exec 後,原本的 shell 和腳本就消失了,被新命令取代
  • 適用於節省系統資源的場景

11.11 Subshells#

Subshell 是在括號中建立的一個新 shell process,擁有父 shell 環境的副本。當 subshell 結束時,其中的環境變更會消失,不影響父 shell:

$ (cd uglydir; uglyprogram)
  • 常用於臨時切換目錄或修改環境變數:
$ (PATH=/usr/confusing:$PATH; uglyprogram)
  • 有一個簡化語法可以在不使用 subshell 的情況下臨時修改環境變數:
$ PATH=/usr/confusing:$PATH uglyprogram
  • Pipe 和背景行程也可與 subshell 搭配使用
  • 範例:使用 tar 搭配 subshell 複製目錄並保留權限:
$ tar cf - orig | (cd target; tar xvf -)

使用此類目錄複製命令前,請確認 target 目錄存在且與 orig 完全不同。


11.12 Including Other Files in Scripts#

使用 dot (.) operator 在腳本中引入其他檔案的內容:

. config.sh
  • 這種方式也稱為 sourcing 一個檔案
  • 適用於讀取共用的設定變數或定義
  • 注意:這與執行另一個腳本不同 — sourcing 是在同一個 shell 中執行,而執行腳本會啟動新的 shell process

11.13 Reading User Input#

read 命令從 standard input 讀取一行文字並存入變數:

$ read var
  • 可用於建立簡單的互動式腳本,例如提示使用者輸入或在危險操作前要求確認

11.14 何時(不)使用 Shell Scripts#

Shell 功能豐富,本章僅涵蓋其中重要的部分。以下是使用建議:

  • Shell script 適合簡化和自動化日常檔案操作任務
  • 對於複雜的字串處理、算術運算、資料庫存取等,應使用 Python、Perl、awk 或 C 語言
  • 如果 shell script 變得過長或過於複雜,這通常是一個信號:你應該換用更合適的程式語言