標籤:
重點看下清單7
現在,很多 Linux® 和 UNIX® 系統上都有 bash shell,它是 Linux 上常見的預設 shell。通過本文,您將瞭解到如何在 bash 指令碼中處理參數和選項,以及如何使用 shell 的參數擴充檢查或修改參數。本文重點介紹 bash,文中的樣本都是在以 bash 為 shell 的 Linux 系統上運行。但是,很多其他的 shell 中也有這些擴充,比如 ksh、ash 或 dash,您可以在其他 UNIX 系統或者甚至是 Cygwin 之類的環境中使用這些 shell 和擴充。早前的一篇文章 Linux 技巧:Bash 測試和比較函數 已經對本文中的構建工具進行了介紹。 本文中的某些材料摘錄自 developerWorks 教程 LPI 102 考試準備,主題 109: Shell、指令碼、編程和編譯,該教程介紹了很多基本的指令碼編程技術。
傳遞的參數
函數和 shell 指令碼的妙處之一是,通過向單個函數或指令碼傳遞參數 能夠使它們表現出不同的行為。在本節中,您將瞭解到如何識別和使用傳遞的參數。
在函數或指令碼中,您可以使用表 1 中列出的 bash 特殊變數來引用參數。您可以給這些變數附上 $ 符號的首碼,然後像引用其他 shell 變數那樣引用它們。
表 1. 函數的 Shell 參數
| 參數 |
目的 |
| 0, 1, 2, ... |
位置參數從參數 0 開始。參數 0 引用啟動 bash 的程式的名稱,如果函數在 shell 指令碼中運行,則引用 shell 指令碼的名稱。有關該參數的其他資訊,比如 bash 由 -c 參數啟動,請參閱 bash 手冊頁面。由單引號或雙引號包圍的字串被作為一個參數進行傳遞,傳遞時會去掉引號。 如果是雙引號,則在調用函數之前將對 $HOME 之類的 shell 變數進行擴充。對於包含嵌入空白或其他字元(這些空白或字元可能對 shell 有特殊意義)的參數,需要使用單引號或雙引號進行傳遞。 |
| * |
位置參數從參數 1 開始。如果在雙引號中進行擴充,則擴充就是一個詞,由 IFS 特殊變數的第一個字元將參數分開,如果 IFS 為空白,則沒有間隔空格。IFS 的預設值是空白、定位字元和分行符號。如果沒有設定 IFS,則使用空白作為分隔字元(僅對預設 IFS 而言)。 |
| @ |
位置參數從參數 1 開始。如果在雙引號中進行擴充,則每個參數都會成為一個詞,因此 “[email protected]” 與 “$1” “$2” 等效。如果參數有可能包含嵌入空白,那麼您將需要使用這種形式。 |
| # |
參數數量(不包含參數 0)。 |
注意:如果您擁有的參數多於 9 個,則不能使用 $10 來引用第十個參數。首先,您必須處理或儲存第一個參數($1),然後使用 shift 命令刪除參數 1 並將所有剩餘的參數下移一位,因此 $10 就變成了 $9,依此類推。$# 的值將被更新以反映參數的剩餘數量。在實踐中,最常見的情況是將參數迭代到函數或 shell 指令碼,或者迭代到命令替換使用 for 語句建立的列表,因此這個約束基本不成問題。
現在,您可以定義一個簡單函數,其用途只是告訴您它所擁有的參數數量並顯示這些參數,如清單 1 所示。
清單 1. 函數參數
[[email protected] ~]$ testfunc () { echo "$# parameters"; echo "[email protected]"; }[[email protected] ~]$ testfunc0 parameters[[email protected] ~]$ testfunc a b c3 parametersa b c[[email protected] ~]$ testfunc a "b c"2 parametersa b c
Shell 指令碼處理參數的方式與函數處理參數的方式相同。實際上,您會經常發現,指令碼往往由很多小型的函數裝配而成。清單 2 給出了一個 shell 指令碼 testfunc.sh,用於完成相同的簡單任務,結果是要使用上面的一個輸入來運行這個指令碼。記住使用 chmod +x 將指令碼標記為可執行。
清單 2. Shell 指令碼參數
[[email protected] ~]$ cat testfunc.sh#!/bin/bashecho "$# parameters"echo "[email protected]";[[email protected] ~]$ ./testfunc.sh a "b c"2 parametersa b c
在表 1 中您會發現,shell 可能將傳遞參數的列表引用為 $* 或 [email protected],而是否將這些運算式用引號引用將影響它們的解釋方式。對於上面的函數而言,使用 $*、“$*”、[email protected] 或 “[email protected]” 輸出的結果差別不大,但是如果函數更複雜一些,就沒有那麼肯定了,當您希望分析參數或將一些參數傳遞給其他函數或指令碼時,使用或不用引號的差別就很明顯。清單 3 給出了一個函數,用於列印參數的數量然後根據這四種可選方案列印參數。清單 4 給出了使用中的函數。IFS 預設變數使用一個空格作為它的第一個字元,因此清單 4 添加了一條豎線作為 IFS 變數的第一個字元,更加清楚地顯示了在 “$*” 擴充中的何處使用這個字元。
清單 3. 一個探究參數處理差別的函數
[[email protected] ~]$ type testfunc2testfunc2 is a functiontestfunc2 (){ echo "$# parameters"; echo Using ‘$*‘; for p in $*; do echo "[$p]"; done; echo Using ‘"$*"‘; for p in "$*"; do echo "[$p]"; done; echo Using ‘[email protected]‘; for p in [email protected]; do echo "[$p]"; done; echo Using ‘"[email protected]"‘; for p in "[email protected]"; do echo "[$p]"; done}清單 4. 使用 testfunc2 列印參數資訊
[[email protected] ~]$ IFS="|${IFS}" testfunc2 abc "a bc" "1 2> 3"3 parametersUsing $*[abc][a][bc][1][2][3]Using "$*"[abc|a bc|1 23]Using [email protected][abc][a][bc][1][2][3]Using "[email protected]"[abc][a bc][1 23]
仔細研究二者的差別,尤其要注意加引號的形式和包含空白(如空白字元和分行符號)的參數。在一個 [] 字元對中,注意:“$*” 擴充實際上是一個詞。
回頁首
選項和 getopts
傳統的 UNIX 和 Linux 命令將一些傳遞的參數看作選項。過去,這些參數是單個的字元開關,與其他參數的區別在於擁有一個前置的連字號或負號。為方便起見,若干個選項可以合并到 ls -lrt 命令中,它提供了一個按修改時間(-t 選項)反向(-r 選項)排序的長(-l 選項)目錄清單。
您可以對 shell 指令碼使用同樣的技術,getopts 內建命令可以簡化您的任務。要查看此命令的工作原理,可以考慮清單 5 所示的樣本指令碼 testopt.sh。
清單 5. testopt.sh 指令碼
#!/bin/bashecho "OPTIND starts at $OPTIND"while getopts ":pq:" optname do case "$optname" in "p") echo "Option $optname is specified" ;; "q") echo "Option $optname has value $OPTARG" ;; "?") echo "Unknown option $OPTARG" ;; ":") echo "No argument value for option $OPTARG" ;; *) # Should not occur echo "Unknown error while processing options" ;; esac echo "OPTIND is now $OPTIND" done
getopts 命令使用了兩個預先確定的變數。OPTIND 變數開始被設為 1。之後它包含待處理的下一個參數的索引。如果找到一個選項,則getopts 命令返回 true,因此常見的選項處理範例使用帶 case 語句的 while 迴圈,本例中就是如此。getopts 的第一個參數是一列要識別的選項字母,在本例中是 p 和 r。選項字母后的冒號 (:) 表示該選項需要一個值;例如,-f 選項可能用於表示檔案名稱,tar 命令中就是如此。此例中的前置冒號告訴 getopts 保持靜默(silent)並抑制正常的錯誤訊息,因為此指令碼將提供它自己的錯誤處理。
此例中的第二個參數 optname 是一個變數名,該變數將接收找到選項的名稱。如果預期某個選項應該擁有一個值,而且目前存在該值,則會把該值放入 OPTARG 變數中。在靜默模式下,可能出現以下兩種錯誤情況。
- 如果發現不能識別的選項,則 optname 將包含一個 ? 而 OPTARG 將包含該未知選項。
- 如果發現一個選項需要值,但是找不到這個值,則 optname 將包含一個 : 而 OPTARG 將包含丟失參數的選項的名稱。
如果不是在靜默模式,則這些錯誤將導致一條診斷錯誤訊息而 OPTARG 不會被設定。指令碼可能在 optname 中使用 ? 或 : 值來檢測錯誤(也可能處理錯誤)。
清單 6 給出了運行此簡單指令碼的兩個樣本。
清單 6. 運行 testopt.sh 指令碼
[[email protected] ~]$ ./testopt.sh -p -qOPTIND starts at 1Option p is specifiedOPTIND is now 2No argument value for option qOPTIND is now 3[[email protected] ~]$ ./testopt.sh -p -q -r -s tuvOPTIND starts at 1Option p is specifiedOPTIND is now 2Option q has value -rOPTIND is now 4Unknown option sOPTIND is now 5
如果您需要這樣做,可以傳遞一組參數給 getopts 計算。如果您在指令碼中已經使用一組參數調用了 getopts,現在要用另一組參數來調用它,則需要親自將 OPTIND 重設為 1。有關更多詳細內容,請參閱 bash 手冊或資訊頁面。
回頁首
參數擴充
您已經瞭解了如何將參數傳遞給函數或指令碼以及如何識別選項,現在開始處理選項和參數。如果在處理選項後可以知道留下了哪些參數,那應該是一種不錯的事情。接下來您可能需要驗證參數值,或者為丟失的參數指派預設值。本節將介紹一些 bash 中的參數擴充。當然,您仍然擁有 Linux 或 UNIX 命令(如 sed 或 awk)的全部功能來執行更複雜的工作,但是您也應該瞭解如何使用 殼層延伸。
我們開始使用上述的選項分析和參數分析函數來構建一個指令碼。清單 7 中給出了 testargs.sh 指令碼。
清單 7. testargs.sh 指令碼
#!/bin/bashshowopts () { while getopts ":pq:" optname do case "$optname" in "p") echo "Option $optname is specified" ;; "q") echo "Option $optname has value $OPTARG" ;; "?") echo "Unknown option $OPTARG" ;; ":") echo "No argument value for option $OPTARG" ;; *) # Should not occur echo "Unknown error while processing options" ;; esac done return $OPTIND}showargs () { for p in "[email protected]" do echo "[$p]" done}optinfo=$(showopts "[email protected]")argstart=$?arginfo=$(showargs "${@:$argstart}")echo "Arguments are:"echo "$arginfo"echo "Options are:"echo "$optinfo"
嘗試運行幾次這個指令碼,看看它如何運作,然後再對它進行詳細考察。清單 8 給出了一些範例輸出。
清單 8. 運行 testargs.sh 指令碼
[[email protected] ~]$ ./testargs.sh -p -q qoptval abc "def ghi"Arguments are:[abc][def ghi]Options are:Option p is specifiedOption q has value qoptval[[email protected] ~]$ ./testargs.sh -q qoptval -p -r abc "def ghi"Arguments are:[abc][def ghi]Options are:Option q has value qoptvalOption p is specifiedUnknown option r[[email protected] ~]$ ./testargs.sh "def ghi"Arguments are:[def ghi]Options are:
注意這些參數與選項是如何分開的。showopts 函數像以前一樣分析選項,但是使用 return 語句將 OPTIND 變數的值返回給調用語句。調用處理將這個值指派給變數 argstart。 然後使用這個值來選擇原始參數的子集,原始參數包括那些沒有被作為選項處理的參數。這個過程使用參數擴充
${@:$argstart} 來完成。
記住在這個運算式的兩側使用引號使參數和嵌入空格保持在一起,如清單 2 後部所示。
如果您是使用指令碼和函數的新手,請記住以下說明:
return 語句返回 showopts 函數的 exit 值,調用方使用 $? 來訪問該函數。您也可以將函數的傳回值和 test 或 while 之類的命令結合使用來控制分支和迴圈。
- Bash 函數包括一些可選的詞 —— “函數”,例如:
function showopts ()
這不是 POSIX 標準的一部分,不受 dash 之類的 shell 的支援,因此如果您要使用它,就不要將 shebang 行設為
#!/bin/sh
因為這會給您提供系統的預設 shell,而它可能不按您希望的方式運行。
- 函數輸出,例如此處兩個函數的
echo 語句所產生的輸出,不會被列印出來,但是調用方可以訪問該輸出。如果沒有將該輸出指派給一個變數,或者沒有在別的地方將它用作調用語句的一部分,則 shell 將會嘗試執行輸出而不是顯示輸出。
子集和子字串
此擴充的一般形式為 ${PARAMETER:OFFSET:LENGTH},其中的 LENGTH 參數是選擇性參數。因此,如果您只希望選擇指令碼參數的特定子集,則可以使用完整版本來指定要選擇多少個參數。例如,${@:4:3} 引用從參數 4 開始的 3 個參數,即參數 4、5 和 6。您可以使用此擴充來選擇除那些使用 $1 到 $9 可立即訪問的參數之外的單個參數。${@:15:1} 是一種直接存取參數 15 的方法。
您可以將此擴充與單個參數結合使用,也可以與 $* 或 [email protected] 表示的整個參數集結合使用。在本例中,參數被作為一個字串及引用位移量和長度的數字來處理。例如,如果變數 x 的值為 “some value”,則
${x:3:5}
的值就是 “e val”,如清單 9 所示。
清單 9. shell 參數值的子字串
[[email protected] ~]$ x="some value"[[email protected] ~]$ echo "${x:3:5}"e val長度
您已經知道 $# 表示參數的數量,而 ${PARAMETER:OFFSET:LENGTH} 擴充適用於單個參數以及 $* 和 [email protected],因此,可以使用一個類似的結構體 ${#PARAMETER} 來確定單個參數的長度也就不足為奇了。清單 10 中所示的簡單的 testlength 函數闡明了這一點。自己去嘗試使用它吧。
清單 10. 參數長度
[[email protected] ~]$ testlength () { for p in "[email protected]"; do echo ${#p};done }[[email protected] ~]$ testlength 1 abc "def ghi"137模式比對
參數擴充還包括了一些模式比對功能,該功能帶有在檔案名稱擴充或 globbing 中使用的萬用字元功能。注意:這不是 grep 使用的Regex匹配。
表 2. Shell 擴充模式比對
| 擴充 |
目的 |
| ${PARAMETER#WORD} |
shell 像檔案名稱擴充中那樣擴充 WORD,並從 PARAMETER 擴充後的值的開頭刪除最短的匹配模式(若存在匹配模式的話)。使用 ‘@’ 或 ‘$’ 即可刪除列表中每個參數的模式。 |
| ${PARAMETER##WORD} |
導致從開頭刪除最長的匹配模式而不是最短的匹配模式。 |
| ${PARAMETER%WORD} |
shell 像檔案名稱擴充中那樣擴充 WORD,並從 PARAMETER 擴充後的值末尾刪除最短的匹配模式(若存在匹配模式的話)。使用 ‘@’ 或 ‘$’ 即可刪除列表中每個參數的模式。 |
| ${PARAMETER%%WORD} |
導致從末尾刪除最長的匹配模式而不是最短的匹配模式。 |
| ${PARAMETER/PATTERN/STRING} |
shell 像檔案名稱擴充中那樣擴充 PATTERN,並替換 PARAMETER 擴充後的值中最長的匹配模式(若存在匹配模式的話)。為了在 PARAMETER 擴充後的值開頭匹配模式,可以給 PATTERN 附上首碼 #,如果要在值末尾匹配模式,則附上首碼 %。如果 STRING 為空白,則末尾的 / 可能被忽略,匹配將被刪除。使用 ‘@’ 或 ‘$’ 即可對列表中的每個參數進行模式替換。 |
| ${PARAMETER//PATTERN/STRING} |
對所有的匹配(而不只是第一個匹配)執行替換。 |
清單 11 給出了模式比對擴充的一些基本用法。
清單 11. 模式比對樣本
[[email protected] ~]$ x="a1 b1 c2 d2"[[email protected] ~]$ echo ${x#*1}b1 c2 d2[[email protected] ~]$ echo ${x##*1}c2 d2[[email protected] ~]$ echo ${x%1*}a1 b[[email protected] ~]$ echo ${x%%1*}a[[email protected] ~]$ echo ${x/1/3}a3 b1 c2 d2[[email protected] ~]$ echo ${x//1/3}a3 b3 c2 d2[[email protected] ~]$ echo ${x//?1/z3}z3 z3 c2 d2
回頁首
整合
在介紹其餘要點之前,先來觀察一下參數處理的實際樣本。我構建了 developerWorks 作者程式包(有關 Linux 系統使用 bash 指令碼的資訊,請參閱 參考資料)。我們將所需的各種檔案儲存體在 developerworks/library 庫的子目錄中。該庫的最新發行版是 5.7 版,因此,模式檔案位於 developerworks/library/schema/5.7 中、XSL 檔案位於 developerworks/library/xsl/5.7 中,而樣本模板則位於 developerworks/library/schema/5.7/templates 中。很明顯,一個提供版本(本例中為 5.7)的參數即可滿足指令碼構建指向所有這些檔案的路徑的需要。因此指令碼擷取的 -v 參數必須有值。稍後對這個參數執行驗證,方法是構建路徑然後使用 [ -d "$pathname" ] 檢查它是否存在。
這種方法對產品構建而言非常有效,但是在開發期間,檔案被儲存在不同的目錄中:
- developerworks/library/schema/5.8/archive/test-5.8/merge-0430
- developerworks/library/xsl/5.8/archive/test-5.8/merge-0430 和
- developerworks/library/schema/5.8/archive/test-5.8/merge-0430/templates-0430
這些目錄中的目前的版本為 5.8,0430 則表示最新測試版本的日期。
為了處理這一情況,我添加了一個參數 -p,它包含了一段補充的路徑資訊 —archive/test-5.8/merge-0430。現在,我(或者別的什麼人)可能忘記了前置斜杠或末尾斜杠,而一些 Windows 使用者可能使用反斜線而不是正斜杠,因此我決定在指令碼中對此進行處理。另外,您還注意到指向模板目錄的路徑包含了兩次日期,因此需要在運行時設法摘除日期 0430。
清單 12 給出了用來處理兩個參數和根據這些需求清理部分路徑的代碼。-v 選項的值儲存在 ssversion 變數中,清理後的 -p 變數儲存在 pathsuffix 中,而日期(連同前置連字號)則儲存在 datesuffix 中。 注釋解釋了每一步執行的操作。即使在這樣一小段指令碼中,您也可以找到一些參數擴充,包括長度、子字串、模式比對和模式替換。
清單 12. 分析用於 developerWorks 作者程式包構建的參數
while getopts ":v:p:" optval "[email protected]" do case $optval in "v") ssversion="$OPTARG" ;; "p") # Convert any backslashes to forward slashes pathsuffix="${OPTARG//\\//}" # Ensure this is a leading / and no trailing one [ ${pathsuffix:0:1} != "/" ] && pathsuffix="/$pathsuffix" pathsuffix=${pathsuffix%/} # Strip off the last hyphen and what follows dateprefix=${pathsuffix%-*} # Use the length of what remains to get the hyphen and what follows [ "$dateprefix" != "$pathsuffix" ] && datesuffix="${pathsuffix:${#dateprefix}}" ;; *) errormsg="Unknown parameter or option error with option - $OPTARG" ;; esac done
像 Linux 中的大多數內容一樣,也許通常對編程而言,這並非解決此問題的惟一解決方案,但它確實展示了您瞭解的擴充的一種更實際的用法。
回頁首
預設值
在上一節中您已經瞭解了如何為 ssversion 或 pathsuffix 之類的變數指派選項值。在這種情況下,稍後將檢測到空值,產品構建時會出現空路徑尾碼,因此這是可以接受的。如果需要為尚未指定的參數指派預設值怎麼辦?表 3 所示的 殼層延伸將協助您完成這個任務。
表 3. 預設值相關的 Shell 擴充
| 擴充 |
目的 |
| ${PARAMETER:-WORD} |
如果 PARAMETER 沒有設定或者為空白,則 殼層延伸 WORD 並替換結果。PARAMETER 的值沒有更改。 |
| `${PARAMETER:=WORD} |
如果 PARAMETER 沒有設定或者為空白,則 殼層延伸 WORD 並將結果指派給 PARAMETER。這個值然後被替換。不能用這種方式指派位置參數或特殊參數的值。 |
| ${PARAMETER:?WORD} |
如果 PARAMETER 沒有設定或者為空白,殼層延伸 WORD 並將結果寫入標準錯誤中。如果沒有 WORD 則寫入一條訊息。如果 shell 不是互動,則表示存在這個擴充。 |
| ${PARAMETER:+WORD} |
如果 PARAMETER 沒有設定或者為空白,則不作替換。否則 殼層延伸 WORD 並替換結果。 |
清單 13 示範了這些擴充以及它們之間的區別。
清單 13. 替換空變數或未設定的變數。
[[email protected] ~]$ unset x;y="abc def"; echo "/${x:-‘XYZ‘}/${y:-‘XYZ‘}/$x/$y/"/‘XYZ‘/abc def//abc def/[[email protected] ~]$ unset x;y="abc def"; echo "/${x:=‘XYZ‘}/${y:=‘XYZ‘}/$x/$y/"/‘XYZ‘/abc def/‘XYZ‘/abc def/[[[email protected] ~]$ ( unset x;y="abc def"; echo "/${x:?‘XYZ‘}/${y:?‘XYZ‘}/$x/$y/" )> >so.txt 2>se.txt[[email protected] ~]$ cat so.txt[[email protected] ~]$ cat se.txt-bash: x: XYZ[[[email protected] ~]$ unset x;y="abc def"; echo "/${x:+‘XYZ‘}/${y:+‘XYZ‘}/$x/$y/"//‘XYZ‘//abc def/
回頁首
傳遞參數
關於參數傳遞有一些微妙之處,如果不小心,可能會犯錯誤。您已經瞭解了使用引號的重要性以及引號對使用 $* 和 [email protected] 的影響,但是考慮以下的例子。假設您想要一個指令碼或函數來操作當前工作目錄中的所有檔案或目錄。為了說明這個例子,考慮清單 14 所示的 ll-1.sh 和 ll-2.sh 指令碼。
清單 14. 兩個樣本指令碼
#!/bin/bash# ll-1.shfor f in "[email protected]" do ll-2.sh "$f" done#!/bin/bashls -l "[email protected]"
指令碼 ll-1.sh 只是將它的每個參數依次傳遞給指令碼 ll-2.sh 而 ll-2.sh 執行傳遞的參數的一個長目錄清單。我的測試目錄包含了兩個空檔案 “file1” 和 “file 2”。清單 15 顯示了指令碼的輸出。
清單 15. 運行指令碼 - 1
[[email protected] test]$ ll-1.sh *-rw-rw-r-- 1 ian ian 0 May 16 15:15 file1-rw-rw-r-- 1 ian ian 0 May 16 15:15 file 2
到目前為止,一切進展得還不錯。但是如果您忘記使用 * 參數,則指令碼不會執行任何操作。它不會像 ls 命令那樣自動執行當前工作目錄的內容。可以做一個簡單的修正,當沒有給 ll1-sh 提供資料時為 ll-1.sh 中的這個條件添加檢查並使用 ls 命令的輸出來產生 ll-2.sh 的輸入。清單 16 給出了一個可能的解決方案。
清單 16. 修正後的 ll-1.sh
#!/bin/bash# ll-1.sh - revision 1for f in "[email protected]" do ll-2.sh "$f" done[ $# -eq 0 ] && for f in "$(ls)" do ll-2.sh "$f" done
注意:我們小心地將 ls 命令的結果用引號引用起來,確保可以正確地處理 “file 2”。 清單 17 給出了運行帶 * 和不帶 * 的新 ll-1.sh 的結果。
清單 17. 運行指令碼 - 2
[[email protected] test]$ ll-1.sh *-rw-rw-r-- 1 ian ian 0 May 16 15:15 file1-rw-rw-r-- 1 ian ian 0 May 16 15:15 file 2[[email protected] test]$ ll-1.shls: file1file 2: No such file or directory
很奇怪吧?當您傳遞參數時,尤其當這些參數是命令的輸出時,處理起來可能需要些技巧。錯誤訊息提示檔案名被分行符號分隔,這就給我們提供了線索。有很多種方法可以處理這個問題,但是有一種簡單的方法就是,使用清單 18 所示的內建 read。自己可以試用一下。
清單 17. 運行指令碼 - 2
#!/bin/bash# ll-1.sh - revision 2for f in "[email protected]" do ll-2.sh "$f" done[ $# -eq 0 ] && ls | while read f do ll-2.sh "$f" done
這個例子的目的就是要說明:注意細節並使用一些不常見的輸入來進行測試可以使指令碼更加可靠。祝您編程順利!
【轉】Linux 技巧: Bash 參數和參數擴充