剛接觸 bash 的程式設計師會一再聽到的一句忠告:
記得為你的變數加上引號!
為什麼呢?
當你展開一個變數(例如 $name)時,幾乎總是希望把變數的_值_當成單一的字串(或數字)來使用。
你不會希望這個值被意外當成兩個或多個分開的字串來處理。
引號是確保一段文字被視為單一詞彙的主要機制。 引號主要有兩種。
雙引號內可以進行參數展開、算術展開和命令替換。
echo "This is bash version $BASH_VERSION"
你可以在雙引號字串中加入字面上的雙引號,只要在它前面加上反斜線即可。
echo "She said \"Let me teach you about bash.\""
單引號_不允許_在其中進行任何展開。
echo 'This is bash version $BASH_VERSION'
# ...^..................................^
# prints exactly: This is bash version $BASH_VERSION
在單引號字串裡嵌入單引號是_不可能_的。
echo $name 究竟哪裡有問題?
當 shell 解析一行程式碼時,會依序執行幾種展開。 最後幾個展開中有兩個是_詞分割_和_檔名展開_。
任何參數展開、命令替換或算術展開的結果,只要不是發生在雙引號內,都可能被進行「詞分割」。
shell 會取用展開後的結果,並依 $IFS 值中出現的任何字元序列將它分割。
預設是空格、Tab 字元及換行。
我們可以用一個 shell 迴圈來看看實際情況:
sentence="The quick brown fox jumps."
i=0
for word in "$sentence"; do echo "$((++i)) $word"; done
# ..........^.........^
輸出
1 The quick brown fox jumps.
但如果沒有加引號:
i=0
for word in $sentence; do echo "$((++i)) $word"; done
# .........^.........^
輸出
1 The
2 quick
3 brown
4 fox
5 jumps.
換一個 $IFS 值來看看:
csv="first,second,third"
IFS=","
for word in $csv; do echo "$word"; done
輸出
first
second
third
要停用詞分割,一個技巧是把 $IFS 設成空字串
sentence="The quick brown fox jumps."
IFS= # or explicitly, IFS=""
i=0
for word in $sentence; do echo "$((++i)) $word"; done
# .........^.........^
輸出
1 The quick brown fox jumps.
不過,除非你也停用檔名展開,否則不建議讓變數不加引號。 我們會在下面看到怎麼做。
「Glob」模式用來簡潔地表達一組要匹配的檔案。
例如,你可以用 *.md 匹配一個目錄中所有的 Markdown 檔案。
Glob 模式從 Unix 發明以來就存在了。
glob 的萬用字元有:
* 匹配零個或多個任意字元,
? 恰好匹配一個任意字元,
[...] 從方括號內列出的字元集合中恰好匹配一個字元。
[abc] 恰好匹配一個字元,該字元是 a、b 或 c 其中之一。
^)
[][] 是一個字元集,會匹配右方括號或左方括號。[[\]]
[x-y] 恰好匹配一個位於 x 到 y _範圍_內的字元
^),或是右方括號之前的最後一個字元。
[-xy] 或 [xy-] 都恰好匹配一個 -、x 或 y 字元[x\-y]
[0-9A-Fa-f] 會匹配一個十六進位數字[^abc] 恰好匹配一個不是 a、b 或 c 的字元,[^x-y] 恰好匹配一個不在 x 到 y 範圍內的字元[[:character_class:]] 匹配一個屬於指定「character_class」的字元
lower -- 小寫字母upper -- 大寫字母alpha -- 字母digit -- 數字alnum -- 字母與數字space -- 空白blank -- 水平空白(空格與 Tab)xdigit -- 十六進位數字word -- 識別字允許的字元(字母、數字、底線)punct -- 標點符號cntrl -- 控制字元graph -- 「可見」字元print -- 可見字元加上空格[[:alpha:]] 類別由 [[:lower:][:upper:]] 組成[[:alnum:]] 類別由 [[:alpha:][:digit:]] 組成[[:word:]] 類別由 [[:alnum:]_] 組成[[:word:]] 和 [[:punct:]]
[[:graph:]] 類別由 [[:alnum:][:punct:]] 組成[[:print:]] 類別由 [[:graph:] ] 組成 -- 只有空格(八進位 040),不含其他空白。你可以用 for 迴圈疊代一份檔案清單
for file in *.csv; do
do_something_with "$file"
done
注意變數 "$file" 加了引號。
glob 模式可能回傳名稱中含有空格的檔案。
我們需要為這個變數加上引號,這樣 do_something_with 指令才會剛好收到一個引數,也就是檔案名稱。
glob 模式不是正規表示式。
.
?
.*
*
.* 的意思是「匹配一個檔案名稱,其第一個字元是字面上的點,後面接著零個或多個任意字元」。.+
?*
Bash 提供了擴充模式,更接近正規表示式。
有時候你會想抑制檔名展開。 處理使用者輸入就是其中一種情況。 看看這段程式碼
read -p "Enter something: " user_input
echo $user_input
如果使用者輸入星號(*),會輸出什麼?
目前目錄中的檔案清單。
這大概不是你想要的結果。
另一個例子是把模式當成引數傳給預期收到模式的程式。
tr 程式常用於文字處理。要把變數值轉成小寫,可以這樣做
var="My Puppet"
echo "$var" | tr '[:upper:]' '[:lower:]'
輸出
my puppet
一個常見的錯誤是忘了為 tr 的引數加上引號:tr [:upper:] [:lower:]
為什麼這會是個問題?
沒有加引號的 glob 模式會被展開成符合的檔案清單。
假設使用你指令碼的人,目前在目錄中有名為 p、u 和 w 的檔案:
touch p u w
var="My Puppet"
echo "$var" | tr [:upper:] [:lower:]
這會導致錯誤!
沒有加引號的 [:upper:] 模式會匹配 :、u、p、e 或 r 其中任一字元。
沒有加引號的 [:lower:] 模式會匹配 :、l、o、w、e 或 r 其中任一字元。
Bash 會為第一個模式匹配檔案 p 和 u,為第二個模式匹配檔案 w,然後 tr 會被這樣呼叫
echo "$var" | tr p u w
而這對 tr 來說是錯誤的引數數量。
你可以用 set 指令來停用檔名展開。
echo * # prints a list of files
set -f # disable filename expansion
echo * # prints a literal asterisk
set +f # enable filename expansion
內建的 shopt 指令有幾個設定可以控制檔名展開的運作方式。
幾個有趣的選項是:
shopt -s nocaseglob -- 執行不區分大小寫的匹配shopt -s extglob -- 啟用擴充模式shopt -s nullglob -- 如果沒有檔案符合,就把模式取代為空。
預設行為是把模式保留為字面上的字串。