##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`鍵,
命令就會跑的原因。
除了常用的`IFS`與`CR`, 常用的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的了解:
```shell
$ 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中,
因此被關閉,不在作為`IFS`;
`A=B<space>C`
事實上,空白符無論在soft quote還是在hard quote中,
均被關閉。Enter鍵字符亦然:
```shell
$ 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`字符也會同樣被關閉:
```shell
$ A="B
> C
> "
$ echo $A
B C
```
然而,由于 `echo $A`時的變量沒有置于soft quote中,
因此,當變量替換完成后,并作命令行重組時,`enter`被解釋為`IFS`,
而不是new-line字符。
同樣的,用escape亦可關閉CR字符:
```shell
$ 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的關閉與否,以$來做說明:
```shell
$ A=B\ C
$ echo "$A"
B C
$ echo '$A'
$A
```
在第一個`echo`命令行中,$被置于soft quote中,將不被關閉,
因此繼續處理變量替換,
因此,`echo`將A的變量值輸出到屏幕,也就是"B C"的結果。
在第二個`echo`命令行中,$被置于hard quote中,則被關閉,
因此,$只是一個$符號,并不會用來做變量替換處理,
因此結果是$符號后面接一個A字母:$A.
> **練習與思考:** 如下結果為何不同?
> tips: 單引號和雙引號,在quoting中均被關閉了。
```shell
$ 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中如此輸入:
```shell
$ awk {print $0} 1.txt
```
由于{}在shell中并沒有關閉,那shell就將{print $0}視為command block,
但同時沒有`;`符號作命令分隔,因此,就出現awk語法錯誤結果。
要解決之,可用hard quote:
```shell
awk '{print $0}'
```
上面的hard quote應好理解,就是將原來的
{、<space>、$、}這幾個shell meta關閉,
避免掉在shell中遭到處理,而完整的成為awk的參數中command meta。
> **Note:**
> awk中使用的$0 是awk中內建的field nubmer,而非awk的變量,
> awk自身的變量無需使用$.
要是理解了hard quote的功能,在來理解soft quote與escape就不難:
```shell
awk "{print \$0}" 1.txt
awk \{print \$0\} 1.txt
```
然而,若要你改變awk的$0的0值是從另一個shell變量中讀進呢?
比方說:已有變量$A的值是0, 那如何在`command line`中解決
awk的$$A呢?
你可以很直接否定掉hard quote的方案:
```shell
$ awk '{print $$A}' 1.txt
```
那是因為$A的$在hard quote中是不能替換變量的。
聰明的讀者(如你!),經過本章的學習,我想,你應該可以理解為
為何我們可以使用如下操作了吧:
```shell
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命令,然后讀取用戶輸入給變量賦值,
但如果輸入是以空格鍵開始的話,這空格會被忽略,比如:
```shell
read a #輸入: abc
echo "$a" #只輸出abc
```
原因:
變量a的值,從終端輸入的值是以IFS開頭,而這些IFS將被shell解釋器忽略(trim)。
應該與shell解釋器分詞的規則有關;
```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。那在重組時,可以得到這些詞。
若你還是不理解,那來驗證一下下面這個例子:
```shell
$ 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);
> 2. IFS=; 意義是將IFS設置為空字符,因為;是shell的元字符(meta);
問題二:為什么多做了幾個分號,我想知道為什么會出現空格呢?
```shell
$ 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)。
要不是試試下面這個代碼片段:
```shell
$ 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
思考問題二:文本處理:讀文件時,如何保證原汁原味。
```shell
cat file | while read i
do
echo $i
done
```
文件file的行中包含若干空,經過read只保留不重復的空格。
如何才能所見即所得。
```shell
cat file | while read i
do
echo "X${i}X"
done
```
從上面的輸出,可以看出read,讀入是按整行讀入的;
不能原汁原味的原因:
1. 如果行的起始部分有IFS之類的字符,將被忽略;
2. `echo $i`的解析過程中,首先將$i替換為字符串,
然后對echo 字符串中字符串分詞,然后命令重組,輸出結果;
在分詞,與命令重組時,可能導致多個相鄰的IFS轉化為一個;
```shell
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的`";"`或者`';'`。
因此上述的解決方案是:
```shell
old_IFS=$IFS
IFS=; #將IFS設置為null
cat file | while read i
do
echo "$i"
done
IFS=old_IFS #恢復IFS的原始值
```
現在,回過頭來看這個問題,為什么會有這個問題呢;
其本源的問題應該是沒有找到解決原始問題的最合適的方法,
而是采取了一個迂回的方式來解決了問題;
因此,我們應該回到問題的本源,重新審視一下,問題的本質。
如果要精準的獲取文件的內容,應該使用od或者hexdump會更好些。