章節概覽#
本章介紹 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/passwd11.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: $311.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>&211.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 - 某些程式(如
diff、grep)使用非零 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 決定走 then 或 else 分支 - 分號
;在then前是必要的(除非then另起一行)
11.5.1 空參數列的處理#
當 $1 可能為空時,[ = hi ] 會產生錯誤。解決方式:
if [ "$1" = hi ]; then
if [ x"$1" = x"hi" ]; then11.5.2 使用其他命令做測試#
不限於 [ 命令,任何命令都可以用作 if 的測試條件:
if grep -q daemon /etc/passwd; then
echo The daemon user is in the passwd file.
fi11.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 有 for 和 while 兩種迴圈。
11.6.1 for Loops#
#!/bin/sh
for str in one two three four; do
echo $str
donefor將變數依序設為in後面的每個值,執行do和done之間的命令
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 $TMPFILE2mktemp會將模板中的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 -print0和xargs -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 # 輸出: 3expr速度慢且笨拙,如果頻繁使用算術運算,建議改用 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 變得過長或過於複雜,這通常是一個信號:你應該換用更合適的程式語言