軌道
/
Bash
Bash
/
課程大綱
/
引號的重要性
引號

引號的重要性 在 Bash

{other: "%{count} 個練習"}

關於 引號的重要性

剛接觸 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 的萬用字元有:

  • * 匹配零個或多個任意字元,

  • ? 恰好匹配一個任意字元,

  • [...] 從方括號內列出的字元集合中恰好匹配一個字元。

    Advanced
    關於字元集,有很多細節。 點這裡顯示。
    • [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:]_] 組成
        • 底線(八進位 137)同時屬於 [[: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 模式不是正規表示式

glob 模式不是正規表示式。

  • 恰好匹配一個字元
    • 正規表示式 .
    • glob ?
  • 匹配零個或多個字元
    • 正規表示式 .*
    • glob *
      • glob 模式 .* 的意思是「匹配一個檔案名稱,其第一個字元是字面上的點,後面接著零個或多個任意字元」。
  • 匹配一個或多個字元
    • 正規表示式 .+
    • glob ?*
  • 正規表示式的字元集和 glob 的字元集行為相同。
Advanced

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:]

Caution

為什麼這會是個問題? 沒有加引號的 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 -- 如果沒有檔案符合,就把模式取代為空。 預設行為是把模式保留為字面上的字串。
透過 GitHub 編輯 連結會在新視窗或分頁中開啟