# 第十二章 命令替換
命令替換重新指定一個[^1]或多個命令的輸出。其實就是將命令的輸出導到另外一個地方[^2]。
命令替換的通常形式是(`` `...` ``),即用反引號引用命令。
```bash
script_name=`basename $0`
echo "The name of this script is $scirpt_name."
```
命令的輸出可以作為另一個命令的參數,也可以賦值給一個變量。甚至在 [`for`](http://tldp.org/LDP/abs/html/loops1.html#FORLOOPREF1) 循環中可以用輸出產生參數表。
```bash
rm `cat filename` # "filename" 中包含了一系列需要被刪除的文件名。
#
# S.C. 指出這樣寫可能會導致出現 "arg list too long" 的錯誤。
# 更好的寫法應該是 xargs rm -- < filename
# ( -- 可以在 "filename" 文件名以 "-" 為開頭時仍舊正常執行 )
textfile_listing=`ls *.txt`
# 變量中包含了當前工作目錄下所有的名為 *.txt 的文件。
echo $textfile_listing
textfile_listing2=$(ls *.txt) # 命令替換的另一種形式。
echo $textfile_listing2
# 結果相同。
# 這樣將一系列文件名賦值給一個單一字符串可能會出現換行。
#
# 而更加安全的方式是將這一系列文件存入數組。
# shopt -s nullglob # 設置后,如果沒有匹配到文件,那么變量會被賦值為空。
# textfile_listing=( *.txt )
#
# 感謝 S.C.
```
>  命令替換本質上是調用了一個 [子進程](http://tldp.org/LDP/abs/html/subshells.html#SUBSHELLSREF) 來執行。
>  命令替換有可能會出現 [字符分割](http://tldp.org/LDP/abs/html/quotingvar.html#WSPLITREF) 的情況。
> ```bash
> COMMAND `echo a b` # 2個參數:a和b
>
> COMMAND "`echo a b`" # 1個參數:"a b"
>
> COMMAND `echo` # 沒有參數
>
> COMMAND "`echo`" # 一個空參數
>
>
> # 感謝 S.C.
> ```
> 但即使不存在字符分割的情況,使用命令替換也會出現丟失尾部換行符的情況。
> ```bash
> # cd "`pwd`" # 你是不是認為這條語句在任何情況下都不會出現錯誤?
> # 但事實卻不是這樣的。
>
> mkdir 'dir with trailing newline
> '
>
> cd 'dir with trailing newline
> '
>
> cd "`pwd`" # Bash 會出現如下錯誤提示:
> # bash: cd: /tmp/file with trailing newline: No such file or directory
>
> cd "$PWD" # 這樣寫是對的。
>
>
>
>
>
> old_tty_setting=$(stty -g) # 保存舊的設置。
> echo "Hit a key "
> stty -icanon -echo # 禁用終端的 canonical 模式。
> # 同時禁用 echo。
> key=$(dd bs=1 count=1 2> /dev/null) # 使用 'dd' 獲得鍵值。
> stty "$old_tty_setting" # 恢復舊的設置。
> echo "You hit ${#key} key." # ${#variable} 表示 $variable 中的字符個數。
> #
> # 除了按下回車鍵外,其余情況都會輸出 "You hit 1 key."
> # 按下回車鍵會輸出 "You hit 0 key."
> # 因為唯一的換行符在命令替換中被丟失了。
>
> # 這段代碼摘自 Stéphane Chazelas。
> ```
>  使用 `echo` 輸出未被引用的命令代換的變量時會刪掉尾部的換行。這可能會導致非常不好的情況出現。
> ```bash
> dir_listing=`ls -l`
> echo $dir_listing # 未被引用
>
> # 你希望會出現按行顯示出文件列表。
>
> # 但是,你卻看到了:
> # total 3 -rw-rw-r-- 1 bozo bozo 30 May 13 17:15 1.txt -rw-rw-r-- 1 bozo
> # bozo 51 May 15 20:57 t2.sh -rwxr-xr-x 1 bozo bozo 217 Mar 5 21:13 wi.sh
>
> # 所有換行都消失了。
>
>
> echo "$dir_listing" # 被引用
> # -rw-rw-r-- 1 bozo 30 May 13 17:15 1.txt
> # -rw-rw-r-- 1 bozo 51 May 15 20:57 t2.sh
> # -rwxr-xr-x 1 bozo 217 Mar 5 21:13 wi.sh
> ```
你甚至可以使用 [重定向](http://tldp.org/LDP/abs/html/io-redirection.html#IOREDIRREF) 或者 [`cat`](http://tldp.org/LDP/abs/html/basic.html#CATREF) 命令把一個文件的內容通過命令代換賦值給一個變量。
```bash
variable1=`<file1` # 將 "file1" 的內容賦值給 variable1。
variable2=`cat file2` # 將 "file2" 的內容賦值給 variable2。
# 使用 cat 命令會開一個新進程,因此執行速度會比重定向慢。
# 需要注意的是,這些變量中可能包含一些空格或者控制字符。
# 無需顯示的賦值給一個變量。
echo "` <$0`" # 輸出腳本自身。
```
```bash
# 摘錄自系統文件 /etc/rc.d/rc.sysinit
#+ (Red Hat Linux 發行版)
if [ -f /fsckoptions ]; then
fsckoptions=`cat /fsckoptions`
...
fi
#
#
if [ -e "/proc/ide/${disk[$device]}/media" ] ; then
hdmedia=`cat /proc/ide/${disk[$device]}/media`
...
fi
#
#
if [ ! -n "`uname -r | grep -- "-"`" ]; then
ktag="`cat /proc/version`"
...
fi
#
#
if [ $usb = "1" ]; then
sleep 5
mouseoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=02"`
kbdoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=01"`
...
fi
```
>  盡量不要將一大段文字賦值給一個變量,除非你有足夠的理由。也絕不要將一個二進制文件的內容賦值給一個變量。
> 樣例 12-1. 蠢蠢的腳本
>
> ```bash
> #!/bin/bash
> # stupid-script-tricks.sh: 不要在自己的電腦上嘗試。
> # 摘自 "Stupid Script Tricks" 卷一。
>
> exit 99 ### 如果你有膽,就注釋掉這行。:)
>
> dangerous_variable=`cat /boot/vmlinuz` # 壓縮的 Linux 內核。
>
> echo "string-length of \$dangerous_variable = ${#dangerous_variable}"
> # $dangerous_variable 的長度為 794151
> # (更新版本的內核可能更大。)
> # 與 'wc -c /boot/vmlinuz' 的結果不同。
>
> # echo "$dangerous_variable"
> # 不要作死。否則腳本會掛起。
>
>
>
> # 將二進制文件的內容賦值給一個變量沒有任何意義。
>
> exit 0
> ```
> 盡管腳本會掛起,但并不會出現緩存溢出的情況。而這正是像 Bash 這樣的解釋型語言相比起編譯型語言能夠提供更多保護的一個例子。
命令替換允許將 [循環](http://tldp.org/LDP/abs/html/loops1.html#FORLOOPREF1) 的輸出結果賦值給一個變量。這其中的關鍵在于循環內部的 [`echo`](http://tldp.org/LDP/abs/html/internal.html#ECHOREF) 命令。
樣例 12-2. 將循環的輸出結果賦值給變量
```bash
#!/bin/bash
# csubloop.sh: 將循環的輸出結果賦值給變量。
variable1=`for i in 1 2 3 4 5
do
echo -n "$i" # 在這里,'echo' 命令非常關鍵。
done`
echo "variable1 = $variable1" # variable1 = 12345
i=0
variable2=`while [ "$i" -lt 10 ]
do
echo -n "$i" # 很關鍵的 'echo'。
let "i += 1" # i 自增。
done`
echo "variable2 = $variable2" # variable2 = 0123456789
# 這個例子表明可以在變量聲明時嵌入循環。
exit 0
```
> 命令替換能夠讓 Bash 做更多的事情。而這僅僅需要在書寫程序或者腳本時將結果輸出到標準輸出 `stdout` 中,然后將這些輸出結果賦值給變量即可。
>
> ```c
> #include <stdio.h>
>
> /* "Hello, world." C program */
>
> int main()
> {
> printf( "Hello, world.\n" );
> return (0);
> }
> ```
>
> ```
> bash$ gcc -0 hello hello.c
> ```
>
> ```bash
> #!/bin/bash
> # hello.sh
>
> greeting=`./hello`
> echo $greeting
> ```
>
> ```
> bash$ sh hello.sh
> Hello, world.
> ```
>  在命令替換中,你可以使用 `$(...)` 來替代反引號。
>
> ```bash
> output=$(sed -n /"$1"/p $file) # 摘自 "grp.sh"。
>
> # 將文本文件的內容賦值給一個變量。
> File_contents1=$(cat $file1)
> File_contents2=$(<$file2) # 這么做也是可以的。
> ```
>
> `$(...)` 和反引號在處理雙反斜杠上有所不同。
>
> ```
> bash$ echo `echo \\`
>
>
> bash$ echo $(echo \\)
> \
> ```
>
> `$(...)` 允許嵌套。[^3]
>
> ```bash
> word_count=$( wc -w $(echo * | awk '{print $8}') )
> ```
>
> 樣例 12-3. 尋找變位詞(anagram)
>
> ```bash
> #!/bin/bash
> # agram2.sh
> # 嵌套命令替換的例子。
>
> # 其中使用了作者寫的工具包 "yawl" 中的 "anagram" 工具。
> # http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz
> # http://bash.deta.in/yawl-0.3.2.tar.gz
>
> E_NOARGS=86
> E_BADARG=87
> MINLEN=7
>
> if [ -z "$1" ]
> then
> echo "Usage $0 LETTERSET"
> exit $E_NOARGS # 腳本需要命令行參數。
> elif [ ${#1} -lt $MINLEN ]
> then
> echo "Argument must have at least $MINLEN letters."
> exit $E_BADARG
> fi
>
>
>
> FILTER='.......' # 至少需要7個字符。
> # 1234567
> Anagrams=( $(echo $(anagram $1 | grep $FILTER) ) )
> # $( $( 嵌套命令集 ) )
> # ( 賦值給數組 )
>
> echo
> echo "${#Anagrams[*]} 7+ letter anagrams found"
> echo
> echo ${Anagrams[0]} # 第一個變位詞。
> echo ${Anagrams[1]} # 第二個變位詞。
> # 以此類推。
>
> # echo "${Anagrams[*]}" # 將所有變位詞在一行里面輸出。
>
> # 可以配合后面的數組章節來理解上面的代碼。
>
> # 建議同時查看另一個尋找變位詞的腳本 agram.sh。
>
> exit $?
> ```
以下是包含命令替換的樣例:
1. [樣例 11-8](http://tldp.org/LDP/abs/html/loops1.html#BINGREP)
2. [樣例 11-27](http://tldp.org/LDP/abs/html/testbranch.html#CASECMD)
3. [樣例 9-16](http://tldp.org/LDP/abs/html/randomvar.html#SEEDINGRANDOM)
4. [樣例 16-3](http://tldp.org/LDP/abs/html/moreadv.html#EX57)
5. [樣例 16-22](http://tldp.org/LDP/abs/html/textproc.html#LOWERCASE)
6. [樣例 16-17](http://tldp.org/LDP/abs/html/textproc.html#GRP)
7. [樣例 16-54](http://tldp.org/LDP/abs/html/extmisc.html#EX53)
8. [樣例 11-14](http://tldp.org/LDP/abs/html/loops1.html#EX24)
9. [樣例 11-11](http://tldp.org/LDP/abs/html/loops1.html#SYMLINKS)
10. [樣例 16-32](http://tldp.org/LDP/abs/html/filearchiv.html#STRIPC)
11. [樣例 20-8](http://tldp.org/LDP/abs/html/redircb.html#REDIR4)
12. [樣例 A-16](http://tldp.org/LDP/abs/html/contributed-scripts.html#TREE)
13. [樣例 29-3](http://tldp.org/LDP/abs/html/procref1.html#PIDID)
14. [樣例 16-47](http://tldp.org/LDP/abs/html/mathc.html#MONTHLYPMT)
15. [樣例 16-48](http://tldp.org/LDP/abs/html/mathc.html#BASE)
16. [樣例 16-49](http://tldp.org/LDP/abs/html/mathc.html#ALTBC)
[^1]: 在命令替換中可以使用外部系統命令,[內建命令](http://tldp.org/LDP/abs/html/internal.html#BUILTINREF) 甚至是 [腳本函數](http://tldp.org/LDP/abs/html/assortedtips.html#RVT)。
[^2]: 從技術的角度來講,命令替換實際上是獲得了命令輸出到標準輸出的結果,然后通過賦值號將結果賦值給一個變量。
[^3]: 事實上,使用反引號進行嵌套也是可行的。但是 John Default 提醒到需要將內部的反引號進行轉義。<pre>word_count=\` wc -w \\\`echo * | awk '{print $8}'\\\` \`</pre>
- 第一部分 初見shell
- 1. 為什么使用shell編程
- 2. 和Sha-Bang(#!)一起出發
- 2.1 調用一個腳本
- 2.2 牛刀小試
- 第二部分 shell基礎
- 3. 特殊字符
- 4. 變量與參數
- 4.1 變量替換
- 4.2 變量賦值
- 4.3 Bash弱類型變量
- 4.4 特殊變量類型
- 5. 引用
- 5.1 引用變量
- 5.2 轉義
- 6. 退出與退出狀態
- 7. 測試
- 7.1 測試結構
- 7.2 文件測試操作
- 7.3 其他比較操作
- 7.4 嵌套 if/then 條件測試
- 7.5 牛刀小試
- 8. 運算符相關話題
- 8.1 運算符
- 8.2 數字常量
- 8.3 雙圓括號結構
- 8.4 運算符優先級
- 第三部分 shell進階
- 10. 變量處理
- 10.1 字符串處理
- 10.1.1 使用 awk 處理字符串
- 10.1.2 參考資料
- 10.2 參數替換
- 11. 循環與分支
- 11.1 循環
- 11.2 嵌套循環
- 11.3 循環控制
- 11.4 測試與分支
- 12. 命令替換
- 13. 算術擴展
- 14. 休息時間
- 第五部分 進階話題
- 19. 嵌入文檔
- 20. I/O 重定向
- 20.1 使用 exec
- 20.2 重定向代碼塊
- 20.3 應用程序
- 22. 限制模式的Shell
- 23. 進程替換
- 26. 列表結構
- 25. 別名