在這一章我們將看一下,當你按下 enter 鍵后,發(fā)生在命令行中的一些“魔法”。雖然我們會 仔細查看幾個復雜有趣的 shell 特點,但我們只使用一個新命令來處理這些特性。
每一次你輸入一個命令,然后按下 enter 鍵,在 bash 執(zhí)行你的命令之前,bash 會對輸入 的字符完成幾個步驟處理。我們已經知道兩三個案例,怎樣一個簡單的字符序列,例如"*", 對 shell 來說,有很多的涵義。使這個發(fā)生的過程叫做(字符)展開。通過展開, 你輸入的字符,在 shell 對它起作用之前,會展開成為別的字符。為了說明我們所要 表達的意思,讓我們看一看 echo 命令。echo 是一個 shell 內部命令,來完成非常簡單的任務。 它在標準輸出中打印出它的文本參數。
[me@linuxbox ~]$ echo this is a test
this is a test
這個命令的作用相當簡單明了。傳遞到 echo 命令的任一個參數都會在(屏幕上)顯示出來。 讓我們試試另一個例子:
[me@linuxbox ~]$ echo *
Desktop Documents ls-output.txt Music Pictures Public Templates Videos
那么剛才發(fā)生了什么事情呢? 為什么 echo 不打印"*"呢?隨著你回想起我們所學過的 關于通配符的內容,這個"*"字符意味著匹配文件名中的任意字符,但是在原先的討論 中我們卻不知道 shell 是怎樣實現這個功能的。最簡單的答案就是 shell 把"*"展開成了 另外的東西(在這種情況下,就是在當前工作目錄下的文件名字),在 echo 命令被執(zhí)行 前。當回車鍵被按下時,shell 在命令被執(zhí)行前在命令行上自動展開任何符合條件的字符, 所以 echo 命令從不會發(fā)現"*",只把它展開成結果。知道了這個以后,我們能看到 echo 執(zhí)行 的結果和我們想象的一樣。
這種通配符工作機制叫做路徑名展開。如果我們試一下在之前的章節(jié)中使用的技巧, 我們會看到它們真是要展開的字符。給出一個家目錄,它看起來像這樣:
[me@linuxbox ~]$ ls
Desktop ls-output.txt Pictures Templates
....
我們能夠執(zhí)行以下參數展開模式:
[me@linuxbox ~]$ echo D*
Desktop Documents
和:
[me@linuxbox ~]$ echo *s
Documents Pictures Templates Videos
甚至是:
[me@linuxbox ~]$ echo [[:upper:]]*
Desktop Documents Music Pictures Public Templates Videos
查看家目錄之外的目錄:
[me@linuxbox ~]$ echo /usr/*/share
/usr/kerberos/share /usr/local/share
隱藏文件路徑名展開
正如我們知道的,以圓點字符開頭的文件名是隱藏文件。路徑名展開也尊重這種 行為。像這樣的展開:
echo *
不會顯示隱藏文件
要是展開模式以一個圓點開頭,我們就能夠在展開模式中包含隱藏文件, 而且隱藏文件可能會出現在第一位置,就像這樣:
echo .*
它幾乎是起作用了。然而,如果我們仔細檢查一下輸出結果,我們會看到名字"." 和".."也出現在結果中。因為這些名字是指當前工作目錄和它的父目錄,使用這種 模式可能會產生不正確的結果。我們能看到這樣的結果,如果我們試一下這個命令:
ls -d .* | less
為了在這種情況下正確地完成路徑名展開,我們應該雇傭一個更精確些的模式。 這個模式會正確地工作:
ls -d .[!.]?*
這種模式展開成為文件名,每個文件名以圓點開頭,第二個字符不包含圓點,再包含至少一個字符, 并且這個字符之后緊接著任意多個字符。這將列出大多數的隱藏文件 (但仍將不能包含以多個圓點開頭的文件名)這個帶有 -A 選項(“幾乎所有”)的 ls 命令能夠提供一份正確的隱藏文件清單:
ls -A
可能你從我們對 cd 命令的介紹中回想起來,波浪線字符("~")有特殊的意思。當它用在 一個單詞的開頭時,它會展開成指定用戶的家目錄名,如果沒有指定用戶名,則是當前用戶的家目錄:
[me@linuxbox ~]$ echo ~
/home/me
如果有用戶"foo"這個帳號,然后:
[me@linuxbox ~]$ echo ~foo
/home/foo
shell 允許算術表達式通過展開來執(zhí)行。這允許我們把 shell 提示當作計算器來使用:
[me@linuxbox ~]$ echo $((2 + 2))
4
算術表達式展開使用這種格式:
$((expression))
(以上括號中的)表達式是指算術表達式,它由數值和算術操作符組成。
算術表達式只支持整數(全部是數字,不帶小數點),但是能執(zhí)行很多不同的操作。這里是 一些它支持的操作符:
操作符 | 說明 |
---|---|
+ | 加 |
- | 減 |
* | 乘 |
/ | 除(但是記住,因為展開只是支持整數除法,所以結果是整數。) |
% | 取余,只是簡單的意味著,“余數” |
** | 取冪 |
在算術表達式中空格并不重要,并且表達式可以嵌套。例如,5的平方乘以3:
[me@linuxbox ~]$ echo $(($((5**2)) * 3))
75
一對括號可以用來把多個子表達式括起來。通過這個技術,我們可以重寫上面的例子, 同時用一個展開代替兩個,來得到一樣的結果:
[me@linuxbox ~]$ echo $(((5**2) * 3))
75
這是一個使用除法和取余操作符的例子。注意整數除法的結果:
[me@linuxbox ~]$ echo Five divided by two equals $((5/2))
Five divided by two equals 2
[me@linuxbox ~]$ echo with $((5%2)) left over.
with 1 left over.
在35章會更深入的討論算術表達式的內容。
可能最奇怪的展開是花括號展開。通過它,你可以從一個包含花括號的模式中 創(chuàng)建多個文本字符串。這是一個例子:
[me@linuxbox ~]$ echo Front-{A,B,C}-Back
Front-A-Back Front-B-Back Front-C-Back
花括號展開模式可能包含一個開頭部分叫做報頭,一個結尾部分叫做附言。花括號表達式本身可 能包含一個由逗號分開的字符串列表,或者一系列整數,或者單個的字符串。這種模式不能 嵌入空白字符。這個例題使用了一系列整數:
[me@linuxbox ~]$ echo Number_{1..5}
Number_1 Number_2 Number_3 Number_4 Number_5
一系列以倒序排列的字母:
[me@linuxbox ~]$ echo {Z..A}
Z Y X W V U T S R Q P O N M L K J I H G F E D C B A
花括號展開可以嵌套:
[me@linuxbox ~]$ echo a{A{1,2},B{3,4}}b
aA1b aA2b aB3b aB4b
那么這對什么有好處呢?最普遍的應用是,創(chuàng)建一系列的文件或目錄列表。例如, 如果我們是攝影師,有大量的相片。我們想把這些相片按年月先后組織起來。首先, 我們要創(chuàng)建一系列以數值"年-月"形式命名的目錄。通過這種方式,目錄名按照 年代順序排列。我們可以鍵入整個目錄列表,但是工作量太大了,并且易于出錯。 反而,我們可以這樣做:
[me@linuxbox ~]$ mkdir Pics
[me@linuxbox ~]$ cd Pics
[me@linuxbox Pics]$ mkdir {2007..2009}-0{1..9} {2007..2009}-{10..12}
[me@linuxbox Pics]$ ls
2007-01 2007-07 2008-01 2008-07 2009-01 2009-07
2007-02 2007-08 2008-02 2008-08 2009-02 2009-08
2007-03 2007-09 2008-03 2008-09 2009-03 2009-09
2007-04 2007-10 2008-04 2008-10 2009-04 2009-10
2007-05 2007-11 2008-05 2008-11 2009-05 2009-11
2007-06 2007-12 2008-06 2008-12 2009-06 2009-12
棒極了!
在這一章我們將會簡單地介紹參數展開,只是皮毛而已。后續(xù)章節(jié)我們會廣泛地 討論參數展開。這個特性在 shell 腳本中比直接在命令行中更有用。它的許多性能 和系統(tǒng)存儲小塊數據,并給每塊數據命名的能力有關系。許多像這樣的小塊數據, 更適當些應叫做變量,可以方便地檢查它們。例如,叫做"USER"的變量包含你的 用戶名。喚醒參數展開,揭示 USER 中的內容,可以這樣做:
[me@linuxbox ~]$ echo $USER
me
查看有效的變量列表,試試這個:
[me@linuxbox ~]$ printenv | less
你可能注意到其它展開類型,如果你誤輸入一個模式,展開就不會發(fā)生。這時 echo 命令只簡單地顯示誤鍵入的模式。通過參數展開,如果你拼寫錯了一個變量名, 展開仍然會進行,只是展成一個空字符串:
[me@linuxbox ~]$ echo $SUER
[me@linuxbox ~]$
命令替換允許我們把一個命令的輸出作為一個展開模式來使用:
[me@linuxbox ~]$ echo $(ls)
Desktop Documents ls-output.txt Music Pictures Public Templates
Videos
我最喜歡用的一行命令是像這樣的:
[me@linuxbox ~]$ ls -l $(which cp)
-rwxr-xr-x 1 root root 71516 2007-12-05 08:58 /bin/cp
這里我們把 which cp 的執(zhí)行結果作為一個參數傳遞給 ls 命令,因此要想得到 cp 程序的 輸出列表,不必知道它完整的路徑名。我們不只限制于簡單命令。也可以使用整個管道線 (只展示部分輸出):
[me@linuxbox ~]$ file $(ls /usr/bin/* | grep zip)
/usr/bin/bunzip2: symbolic link to `bzip2'
....
在這個例子中,管道線的輸出結果成為 file 命令的參數列表。
在舊版 shell 程序中,有另一種語法也支持命令替換,可與剛提到的語法輪換使用。 bash 也支持這種語法。它使用倒引號來代替美元符號和括號:
[me@linuxbox ~]$ ls -l `which cp`
-rwxr-xr-x 1 root root 71516 2007-12-05 08:58 /bin/cp
我們已經知道 shell 有許多方式可以完成展開,現在是時候學習怎樣來控制展開了。 以下面例子來說明:
[me@linuxbox ~]$ echo this is a test
this is a test
或者:
[me@linuxbox ~]$ echo The total is $100.00
The total is 00.00
在第一個例子中,shell 從 echo 命令的參數列表中,刪除多余的空格。在第二個例子中,
參數展開把 $1
的值替換為一個空字符串,因為 1
是沒有定義的變量。shell 提供了一種
叫做引用的機制,來有選擇地禁止不需要的展開。
我們將要看一下引用的第一種類型,雙引號。如果你把文本放在雙引號中, shell 使用的特殊字符,除了 $,\ (反斜杠),和 `(倒引號)之外, 則失去它們的特殊含義,被當作普通字符來看待。這意味著單詞分割,路徑名展開, 波浪線展開,和花括號展開都被禁止,然而參數展開,算術展開,和命令替換 仍然執(zhí)行。使用雙引號,我們可以處理包含空格的文件名。比方說我們是不幸的 名為 two words.txt 文件的受害者。如果我們試圖在命令行中使用這個 文件,單詞分割機制會導致這個文件名被看作兩個獨自的參數,而不是所期望 的單個參數:
[me@linuxbox ~]$ ls -l two words.txt
ls: cannot access two: No such file or directory
ls: cannot access words.txt: No such file or directory
使用雙引號,我們可以阻止單詞分割,得到期望的結果;進一步,我們甚至可以修復 破損的文件名。
[me@linuxbox ~]$ ls -l "two words.txt"
-rw-rw-r-- 1 me me 18 2008-02-20 13:03 two words.txt
[me@linuxbox ~]$ mv "two words.txt" two_words.txt
你瞧!現在我們不必一直輸入那些討厭的雙引號了。
記住,在雙引號中,參數展開,算術表達式展開,和命令替換仍然有效:
[me@linuxbox ~]$ echo "$USER $((2+2)) $(cal)"
me 4 February 2008
Su Mo Tu We Th Fr Sa
....
我們應該花費一點時間來看一下雙引號在命令替換中的效果。首先仔細研究一下單詞分割 是怎樣工作的。在之前的范例中,我們已經看到單詞分割機制是怎樣來刪除文本中額外空格的:
[me@linuxbox ~]$ echo this is a test
this is a test
在默認情況下,單詞分割機制會在單詞中尋找空格,制表符,和換行符,并把它們看作 單詞之間的界定符。它們只作為分隔符使用。因為它們把單詞分為不同的參數,在范例中, 命令行包含一個帶有四個不同參數的命令。如果我們加上雙引號:
[me@linuxbox ~]$ echo "this is a test"
this is a test
單詞分割被禁止,內嵌的空格也不會被當作界定符,它們成為參數的一部分。 一旦加上雙引號,我們的命令行就包含一個帶有一個參數的命令。
事實上,單詞分割機制把換行符看作界定符,對命令替換產生了一個,雖然微妙,但有趣的影響。 考慮下面的例子:
[me@linuxbox ~]$ echo $(cal)
February 2008 Su Mo Tu We Th Fr Sa 1 2 3 4 5 6 7 8 9 10 11 12 13 14
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
[me@linuxbox ~]$ echo "$(cal)"
February 2008
....
在第一個實例中,沒有引用的命令替換導致命令行包含38個參數。在第二個例子中, 命令行只有一個參數,參數中包括嵌入的空格和換行符。
如果需要禁止所有的展開,我們使用單引號。以下例子是無引用,雙引號,和單引號的比較結果:
[me@linuxbox ~]$ echo text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER
text /home/me/ls-output.txt a b foo 4 me
[me@linuxbox ~]$ echo "text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER"
text ~/*.txt {a,b} foo 4 me
[me@linuxbox ~]$ echo 'text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER'
text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER
正如我們所看到的,隨著引用程度加強,越來越多的展開被禁止。
有時候我們只想引用單個字符。我們可以在字符之前加上一個反斜杠,在這個上下文中叫做轉義字符。 經常在雙引號中使用轉義字符,來有選擇地阻止展開。
[me@linuxbox ~]$ echo "The balance for user $USER is: \$5.00"
The balance for user me is: $5.00
使用轉義字符來消除文件名中一個字符的特殊含義,是很普遍的。例如,在文件名中可能使用 一些對于 shell 來說,有特殊含義的字符。這些字符包括"$", "!", " "等字符。在文件名 中包含特殊字符,你可以這樣做:
[me@linuxbox ~]$ mv bad\&filename good_filename
為了允許反斜杠字符出現,輸入"\"來轉義。注意在單引號中,反斜杠失去它的特殊含義,它 被看作普通字符。
反斜杠轉義字符序列
反斜杠除了作為轉義字符外,反斜杠也是一種表示法的一部分,這種表示法代表某種 特殊字符,叫做控制碼。ASCII 編碼表中前32個字符被用來把命令轉輸到像電報機 一樣的設備。一些編碼是眾所周知的(制表符,退格符,換行符,和回車符),其它 一些編碼就不熟悉了(空值,傳輸結束碼,和確認)。
|轉義序列|含義 |\a|響鈴("警告"-導致計算機嘟嘟響) |\b|退格符 |\n|新的一行。在類 Unix 系統(tǒng)中,產生換行。 |\r|回車符 |\t|制表符
上表列出了一些常見的反斜杠轉義字符。反斜杠表示法背后的思想來源于 C 編程語言, 許多其它語言也采用了這種表示方法,包括 shell。
echo 命令帶上 '-e' 選項,能夠解釋轉義序列。你可以把轉義序列放在 $\' \' 里面。 以下例子,使用 sleep 命令,一個簡單的程序,它會等待指定的秒數,然后退出。 我們可以創(chuàng)建一個簡單的倒數計數器:
sleep 10; echo -e \"Time\'s up\a\"
我們也可以這樣做:
sleep 10; echo \"Time\'s up\" $\'\a\'
隨著我們繼續(xù)學習 shell,你會發(fā)現使用展開和引用的頻率逐漸多起來,所以能夠很好的 理解他們的工作方式很有意義。事實上,可以這樣說,他們是學習 shell 的最重要的主題。 如果沒有準確地理解展開模式,shell 總是神秘和混亂的源泉,并且 shell 潛在的能力也 浪費掉了。
Bash 手冊頁有主要段落是關于展開和引用的,它們以更正式的方式介紹了這些題目。
Bash 參考手冊也包含章節(jié),介紹展開和引用: