##shell十三問之4:""(雙引號)與''(單引號)差在哪?

還是回到我們的command line來吧...

經過前面兩章的學習,應該很清楚當你在shell prompt後面敲打鍵盤, 直到按下Enter鍵的時候,你輸入的文字就是command line了, 然後shell才會以進程的方式執行你所交給它的命令。 但是,你又可知道:你在command line中輸入的每一個文字, 對shell來說,是有類別之分的呢?

簡單而言,(我不敢說精確的定義,注1), command line的每一個charactor, 分為如下兩種:

  • literal:也就是普通的純文字,對shell來說沒特殊功能;
  • meta: 對shell來說,具有特定功能的特殊保留元字符。

Note:

對於bash shell在處理comamnd line的順序說明, 請參考O'Reilly出版社的Learning the Bash Shell,2nd Edition, 第177-180頁的說明,尤其是178頁的流程圖:Figure 7-1 ...

literal沒什麼好談的, 像abcd、123456這些"文字"都是literal...(so easy? _) 但meta卻常使我們困惑...(confused?) 事實上,前兩章,我們在command line中已碰到兩個 似乎每次都會碰到的meta:

  • IFS:有space或者tab或者Enter三者之一組成(我們常用space)
  • CR: 由Enter產生;

IFS是用來拆解command line中每一個詞(word)用的, 因為shell command line是按詞來處理的。 而CR則是用來結束command line用的,這也是為何我們敲Enter鍵, 命令就會跑的原因。

除了常用的IFSCR, 常用的meta還有:

meta字符 meta字符作用
= 設定變量
$ 作變量或運算替換(請不要與shell prompt混淆) 命令
> 輸出重定向(重定向stdout)
< 輸入重定向(重定向stdin)
| 命令管道
& 重定向file descriptor或將命令至於後臺(bg)運行
() 將其內部的命令置於nested subshell執行,或用於運算或變量替換
{} 將期內的命令置於non-named function中執行,或用在變量替換的界定範圍
; 在前一個命令執行結束時,而忽略其返回值,繼續執行下一個命令
&& 在前一個命令執行結束時,若返回值為true,繼續執行下一個命令
|| 在前一個命令執行結束時,若返回值為false,繼續執行下一個命令
! 執行histroy列表中的命令
... ...

假如我們需要在command line中將這些保留元字符的功能關閉的話, 就需要quoting處理了。

bash中,常用的quoting有以下三種方法:

  • hard quote:''(單引號),凡在hard quote中的所有meta均被關閉;
  • soft quote:""(雙引號),凡在soft quote中大部分meta都會被關閉,但某些會保留(如$);
  • escape: \ (反斜槓),只有在緊接在escape(跳脫字符)之後的單一meta才被關閉;

Note:

在soft quote中被豁免的具體meta清單,我不完全知道, 有待大家補充,或通過實踐來發現並理解。

下面的例子將有助於我們對quoting的瞭解:

$ A=B C #空白符未被關閉,作為IFS處理
$ C:command not found.
$ echo $A

$ A="B C" #空白符已被關掉,僅作為空白符
$ echo $A
B C

在第一個給A變量賦值時,由於空白符沒有被關閉, command line 將被解釋為: A=B 然後碰到<IFS>,接著執行C命令 在第二次給A變量賦值時,由於空白符被置於soft quote中, 因此被關閉,不在作為IFSA=B<space>C 事實上,空白符無論在soft quote還是在hard quote中, 均被關閉。Enter鍵字符亦然:

$ A=`B
> C
> '
$ echo "$A"
B
C

在上例中,由於enter被置於hard quote當中,因此不再作為CR字符來處理。 這裡的enter單純只是一個斷行符號(new-line)而已, 由於command line並沒得到CR字符, 因此進入第二個shell prompt(PS2, 以>符號表示), command line並不會結束,直到第三行, 我們輸入的enter並不在hard quote裡面, 因此沒有被關閉, 此時,command line碰到CR字符,於是結束,交給shell來處理。

上例的Enter要是被置於soft quote中的話,CR字符也會同樣被關閉:

$ A="B
> C
> "
$ echo $A
B C

然而,由於 echo $A時的變量沒有置於soft quote中, 因此,當變量替換完成後,並作命令行重組時,enter被解釋為IFS, 而不是new-line字符。

同樣的,用escape亦可關閉CR字符:

$ A=B\
> C\
>
$ echo $A
BC

上例中的,第一個enter跟第二個enter均被escape字符關閉了, 因此也不作為CR來處理,但第三個enter由於沒有被escape, 因此,作為CR結束command line。 但由於enter鍵本身在shell meta中特殊性,在 \ escape字符後面 僅僅取消其CR功能, 而不保留其IFS功能。

你或許發現光是一個enter鍵所產生的字符,就有可能是如下這些可能:

  • CR
  • IFS
  • NL(New Line)
  • FF(Form Feed)
  • NULL
  • ...

至於,什麼時候解釋為什麼字符,這個我就沒法去挖掘了, 或者留給讀者君自行慢慢摸索了...-

至於soft quote跟hard quote的不同,主要是對於某些meta的關閉與否,以$來做說明:

$ A=B\ C
$ echo "$A"
B C
$ echo '$A'
$A

在第一個echo命令行中,$被置於soft quote中,將不被關閉, 因此繼續處理變量替換, 因此,echo將A的變量值輸出到屏幕,也就是"B C"的結果。

在第二個echo命令行中,$被置於hard quote中,則被關閉, 因此,$只是一個$符號,並不會用來做變量替換處理, 因此結果是$符號後面接一個A字母:$A.

練習與思考: 如下結果為何不同?

tips: 單引號和雙引號,在quoting中均被關閉了。

$ A=B\ C
$ echo '"$A"'  #最外面的是單引號
"$A"
$ echo "'$A'"  #最外面的是雙引號
'B C'

在CU的shell版裡,我發現很多初學者的問題, 都與quoting的理解有關。 比方說,若我們在awk或sed的命令參數中, 調用之前設定的一些變量時,常會問及為何不能的問題。

要解決這些問題,關鍵點就是:區分出 shell meta 與 command meta

前面我們提到的那些meta,都是在command line中有特殊用途的, 比方說{}就是將一系列的command line置於不具名的函數中執行(可簡單視為command block), 但是,awk卻需要用{}來區分出awk的命令區段(BEGIN,MAIN,END). 若你在command line中如此輸入:

$ awk {print $0} 1.txt

由於{}在shell中並沒有關閉,那shell就將{print $0}視為command block, 但同時沒有;符號作命令分隔,因此,就出現awk語法錯誤結果。

要解決之,可用hard quote:

awk '{print $0}'

上面的hard quote應好理解,就是將原來的 {、 、$、}這幾個shell meta關閉, 避免掉在shell中遭到處理,而完整的成為awk的參數中command meta。

Note:

awk中使用的$0 是awk中內建的field nubmer,而非awk的變量, awk自身的變量無需使用$.

要是理解了hard quote的功能,在來理解soft quote與escape就不難:

awk "{print \$0}" 1.txt
awk \{print \$0\} 1.txt

然而,若要你改變awk的$0的0值是從另一個shell變量中讀進呢? 比方說:已有變量$A的值是0, 那如何在command line中解決 awk的$$A呢? 你可以很直接否定掉hard quote的方案:

$ awk '{print $$A}' 1.txt

那是因為$A的$在hard quote中是不能替換變量的。

聰明的讀者(如你!),經過本章的學習,我想,你應該可以理解為 為何我們可以使用如下操作了吧:

A=0
awk "{print \$$A}" 1.txt
awk  \{print\ \$$A\} 1.txt
awk '{print $'$A'}' 1.txt
awk '{print $'"$A"'}' 1.txt

或許,你能給出更多方案... _

更多練習:

  • http://bbs.chinaunix.net/forum/viewtopic.php?t=207178 一個關於read命令的小問題: 很早以前覺得很奇怪:執行read命令,然後讀取用戶輸入給變量賦值, 但如果輸入是以空格鍵開始的話,這空格會被忽略,比如:
read a  #輸入:    abc
echo "$a" #只輸出abc

原因: 變量a的值,從終端輸入的值是以IFS開頭,而這些IFS將被shell解釋器忽略(trim)。 應該與shell解釋器分詞的規則有關;

read a  #輸入:\ \ \ abc
echo "$a" #只輸出abc

需要將空格字符轉義

Note:

IFS Internal field separators, normally space, tab, and newline (see Blank Interpretation section). ...... Blank Interpretation After parameter and command substitution, the results of substitution are scanned for internal field separator characters (those found in IFS) and split into distinct arguments where such characters are found. Explicit null arguments ("" or '') are retained. Implicit null arguments(those resulting from parameters that have no values) are removed. (refre to: man sh)

解決思路:

  1. shell command line 主要是將整行line給分解(break down)為每一個單詞(word);
  2. 而詞與詞之間的分隔符就是IFS (Internal Field Seperator)。
  3. shell會對command line作處理(如替換,quoting等), 然後再按詞重組。(注:別忘了這個重組特性)
  4. 當你用IFS來事開頭一個變量值,那shell會先整理出這個詞,然後在重組command line。 5.然而,你將IFS換成其他,那shell將視你哪些space/tab為“詞”,而不是IFS。那在重組時,可以得到這些詞。

若你還是不理解,那來驗證一下下面這個例子:

$ A="  abc"
$ echo $A
abc
$ echo "$A" #note1
   abc
$ old_IFS=$IFS
$ IFS=;
$ echo $A
   abc
$ IFS=$old_IFS
$ echo $A
abc

Note:

  1. 這裡是用 soft quoting 將裡面的 space 關閉,使之不是 meta(IFS), 而是一個literal(white space);
  1. IFS=; 意義是將IFS設置為空字符,因為;是shell的元字符(meta);

問題二:為什麼多做了幾個分號,我想知道為什麼會出現空格呢?

$ a=";;;test"
$ IFS=";"
$ echo $a
   test
$ a="   test"
$ echo $a
   test
$ IFS=" "
$ echo $a
test

解答:

這個問題,出在IFS=;上。 因為這個;在問題一中的command line上是一個meta, 並非";"符號本身。 因此,IFS=;是將IFS設置為 null charactor (不是space、tab、newline)。

要不是試試下面這個代碼片段:

$ old_IFS=$IFS
$ read A
;a;b;c
$ echo $A
;a;b;c
$ IFS=";"  #Note2
$ echo $A
a b c

Note:

要關閉;可用";"或者';'或者\;

  • http://bbs.chinaunix.net/forum/viewtopic.php?t=216729

思考問題二:文本處理:讀文件時,如何保證原汁原味。

cat file | while read i
do
   echo $i
done

文件file的行中包含若干空,經過read只保留不重複的空格。 如何才能所見即所得。

cat file | while read i
do
   echo "X${i}X"
done

從上面的輸出,可以看出read,讀入是按整行讀入的; 不能原汁原味的原因:

  1. 如果行的起始部分有IFS之類的字符,將被忽略;
  2. echo $i的解析過程中,首先將$i替換為字符串, 然後對echo 字符串中字符串分詞,然後命令重組,輸出結果; 在分詞,與命令重組時,可能導致多個相鄰的IFS轉化為一個;
cat file | while read i
do
  echo "$i"
done

以上代碼可以解決原因2中的,command line的分詞和重組導致meta字符丟失; 但仍然解決不了原因1中,read讀取行時,忽略行起始的IFS meta字符。

回過頭來看上面這個問題:為何要原汁原味呢? cat命令就是原汁原味的,只是shell的read、echo導致了某些shell的meta字符丟失;

如果只是IFS meta的丟失,可以採用如下方式: 將IFS設置為null,即IFS=;, 在此再次重申此處;是shell的meta字符,而不是literal字符; 因此要使用literal的 ;應該是\; 或者關閉meta 的(soft/hard) quoting的";"或者';'

因此上述的解決方案是:

old_IFS=$IFS
IFS=; #將IFS設置為null
cat file | while read i
do
  echo "$i"
done
IFS=old_IFS #恢復IFS的原始值

現在,回過頭來看這個問題,為什麼會有這個問題呢; 其本源的問題應該是沒有找到解決原始問題的最合適的方法, 而是採取了一個迂迴的方式來解決了問題;

因此,我們應該回到問題的本源,重新審視一下,問題的本質。 如果要精準的獲取文件的內容,應該使用od或者hexdump會更好些。

d


书籍推荐