文章目錄
- 把stdout導向stdin
- 從檔案而不是stout開始一個管線
- 使用xargs命令
- 使用find -exec或者 find和xargs一起使用
摘要:
也許你覺得流和管道讓Linux專家看起來像管道工人一樣,那麼讓我們來看看究竟,並且學習一下重新導向和多路輸出。還會學到把一個流作為命令的參數。
概述:本文教會你關於重新導向標準輸入輸出資料流的基本知識,具體是:
- 重新導向標準IO流:標準輸入資料流,標準輸出資料流,標準錯誤流
- 使用管道把一個命令的輸出當作另一個命令的輸入
- 把輸出同時寫入標準輸出資料流和檔案
- 把一個命令的輸出當作另一個命令的參數
建立實驗用目錄結構:
在本文裡,我們將繼續使用在上一篇文章中建立的檔案做實驗,即便你沒學過上篇文章或者沒有當時沒有儲存那些檔案也不要緊,我們重建立立它們。首先在家目錄下建立一個名為lpi103-4的目錄,然後在這個目錄下建立所需的檔案。這些可以通過如下命令來完成。
[root@localhost ~]# mkdir -p lpi103-4 && cd lpi103-4 && {
> echo -e "1 apple\n2 pear\n3 banana" > text1
> echo -e "9\tplum\n3\tbanana\n10\tapple" > text2
> echo "This is a sentence. " !#:* !#:1->text3
echo "This is a sentence. " "This is a sentence. " "This is a sentence. ">text3
> split -l 2 text1
> split -b 17 text2 y; }
執行完成後的目錄結構如下:
[root@localhost lpi103-4]# ll
total 28
-rw-r--r-- 1 root root 24 Jun 26 14:21 text1
-rw-r--r-- 1 root root 25 Jun 26 14:21 text2
-rw-r--r-- 1 root root 63 Jun 26 14:21 text3
-rw-r--r-- 1 root root 15 Jun 26 14:21 xaa
-rw-r--r-- 1 root root 9 Jun 26 14:21 xab
-rw-r--r-- 1 root root 17 Jun 26 14:21 yaa
-rw-r--r-- 1 root root 8 Jun 26 14:21 yab
重新導向標準IO流
像bash這樣的Linux shell程式,無論接受輸入還是發送輸出都是按照字元序列(字元流)來進行的。每一個字元都與前後相鄰的其他字元彼此獨立。這些字元不會組成結構化的記錄,也不會是固定大小的塊。無論真正的字元流來自或者流向一個檔案、鍵盤、顯示的視窗還是其他的裝置,都可以統一通過檔案IO技術來訪問。Linux shell程式使用三個標準IO流,每一個都用一個眾所周知的檔案描述符來指定:
- stdout代表標準輸出資料流,用來顯示命令的輸出結果,對應的檔案描述符是1;
- stderr代表標準錯誤流,用來顯示命令的錯誤輸出,對應的檔案描述符是2;
- stdin代表標準輸入資料流,用來為命令提供輸入,對應的檔案描述符是0。
輸入資料流為程式提供輸入,通常來自於終端鍵盤。輸出資料流列印文本字元,通常列印到終端裝置。過去的終端裝置就是一個ASCII打字機或者顯示終端,但是現在的終端往往是圖形案頭的一個文字視窗。重新導向輸出有兩種重新導向輸出的的方式:n> 把檔案描述符n代表的輸出資料流重新導向到檔案。此時必須對檔案有寫入權限。如果指定的檔案不存在,就會建立它,如果存在,就會替換掉原來的內容,而且往往沒有警告。n>> 與上面類似,只是不會替換檔案原來的內容,而是追加到原內容之後。
上面的n代表檔案描述符,如果沒有給出,那麼其預設值就是1,也就是標準輸出。下面是一些例子:
[root@localhost lpi103-4]# ls x* z*
ls: cannot access z*: No such file or directory
xaa xab
[root@localhost lpi103-4]# ls x* z* >stdout.txt 2>stderr.txt
[root@localhost lpi103-4]# ls w* y*
ls: cannot access w*: No such file or directory
yaa yab
[root@localhost lpi103-4]# ls w* y* >>stdout.txt 2>>stderr.txt
[root@localhost lpi103-4]# cat stdout.txt
xaa
xab
yaa
yab
[root@localhost lpi103-4]# cat stderr.txt
ls: cannot access z*: No such file or directory
ls: cannot access w*: No such file or directory
我們說過使用n>來進行重新導向會覆蓋掉原來的內容。然而你可以通過set -o noclobber來改變這種行為,如果這麼做了,可以使用n>|來覆蓋。
[root@localhost lpi103-4]# set -o noclobber
[root@localhost lpi103-4]# ls x* z* >stdout.txt 2>stderr.txt
bash: stdout.txt: cannot overwrite existing file
[root@localhost lpi103-4]# ls x* z* >|stdout.txt 2>|stderr.txt
[root@localhost lpi103-4]# cat stdout.txt
xaa
xab
[root@localhost lpi103-4]# cat stderr.txt
ls: cannot access z*: No such file or directory
[root@localhost lpi103-4]# set +o noclobber #恢複原來的設定
有時候你可能想要把stdout和stderr重新導向到同一個檔案,這種情況包括自動處理或者後台工作,目的是可以在以後再查看輸出。這可以通過&>或&>>來完成。另一種可行的方法是先重新導向n到m,然後把m重新導向到檔案,使用的命令是 m>&n或者m&>>n。重新導向的順序非常重要,如:
command 2>&1 >output.txt
與
commnd >output.txt 2>&1
是不一樣的。
第一個例子中,stderr被重新導向到了stdout,然後stdout被重新導向到了檔案output.txt,但是第二次重新導向知識影響了stdout,並沒有影響stderr。
第二個例子中,stderr被重新導向到了stdout,而此時的stdout已經重新導向到了output.txt,所以stderr也就重新導向到了output.txt。
[root@localhost lpi103-4]# ls x* z* &>output.txt
[root@localhost lpi103-4]# cat output.txt
ls: cannot access z*: No such file or directory
xaa
xab
[root@localhost lpi103-4]# ls x* z* >output.txt 2>&1
[root@localhost lpi103-4]# cat output.txt
ls: cannot access z*: No such file or directory
xaa
xab
[root@localhost lpi103-4]# ls x* z* 2>&1 >output.txt # stderr 沒有定向到output.txt
ls: cannot access z*: No such file or directory
[root@localhost lpi103-4]# cat output.txt
xaa
xab
有些時候,你可能想把stdout或stderr忽略掉。這可以通過重新導向到特殊的/dev/null檔案來完成。如下:
[root@localhost lpi103-4]# ls x* z* 2>/dev/null
xaa xab
[root@localhost lpi103-4]# cat /dev/null
重新導向標準輸入
與我們可以重新導向stdout和stderr一樣,我們可以通過<操作符來重新導向stdin。前面文章中我們使用cat file | command 的形式來把file作為command的輸入來源,這不是必須的,我們可以通過重新導向stdin來實現同樣的效果。如下:
[root@localhost lpi103-4]# tr ' ' '\t'<text1
1 apple
2 pear
3 banana
包括bash在內的shell程式們還有一個here-document的概念,這是另外一種形式的重新導向。它的格式是<<緊接一個單詞(如"END"),這個END的作用是作為輸入的終結標誌,如下:
[root@localhost lpi103-4]# sort -k2 <<END
> 1 apple
> 2 pear
> 3 banana
> END
1 apple
3 banana
2 pear
你可能想知道是否可以不輸入END,而是通過Ctrl+d來結束輸入。答案是肯定的,但是這在指令檔裡行不通,因為指令檔裡不能輸入Ctrl+d。因為shell指令碼為大量使用了tab來縮排代碼,出現了針對這種情況的一種here-document變體---- <<-,此時每行開頭的tab將被忽略。
下面例子中,我們建立了一個指令檔,檔案中使用了<<-。如下:
[root@localhost lpi103-4]# ht=$(echo -en "\t")
[root@localhost lpi103-4]# cat<<END>ex-here.sh
> cat <<-EOF
> apple
> EOF
> ${ht}cat <<-EOF
> ${ht}pear
> ${ht}EOF
> END
[root@localhost lpi103-4]# cat ex-here.sh
cat <<-EOF
apple
EOF
cat <<-EOF
pear
EOF
[root@localhost lpi103-4]# bas
base64 basename bash bashbug-32
[root@localhost lpi103-4]# bash ex-here.sh
apple
pear
建立管道
前面我們在“文字資料流和過濾器”中說過,可以使用管道來串連多個文本處理命令。但是管道並不局限於文字資料流,儘管在文字資料流中經常使用。
把stdout導向stdin
使用管道操作符|可以把第一個命令的stdout定向到第二個命令的stdin。更多地命令和管道可以組成更長的管線。管線中的每一個命令都可以有參數,很多命令使用一個單獨的-代表輸入檔案是stdin。虛擬碼如下:
command1 | command2 parameter1 | command3 parameter1 - parameter2 | command4
需要注意的是,管道只是把stdout定向到了stdin。你不能使用2| 把stderr也加入管道中。如果stderr已經被重新導向到了stdout,那麼所有的輸出資料流就被管道處理了。例子如下:
[root@localhost lpi103-4]# ls y* x* z* u* q*
ls: cannot access z*: No such file or directory
ls: cannot access u*: No such file or directory
ls: cannot access q*: No such file or directory
xaa xab yaa yab
[root@localhost lpi103-4]# ls y* x* z* u* q* 2>&1 | sort
ls: cannot access q*: No such file or directory
ls: cannot access u*: No such file or directory
ls: cannot access z*: No such file or directory
xaa
xab
yaa
yab
Linux或者Unix系統上的管道的一個優點是,管道處理過程中並沒有臨時檔案產生。第一個命令的stout不會寫入一個檔案,然後第二個命令讀取這個檔案。前面的文章中提到過使用tar 來一步完成打包和壓縮,如果你工作的Unix系統中的tar恰好不支援-z或者-j等壓縮選項,這也沒有問題,使用管道可以輕鬆搞定,如下:
bunzip -c somefile.tar.bz2 | tar -xvf -
從檔案而不是stout開始一個管線
上面的例子中,管線開始於一個產生輸出的命令。當然也可以開始於一個已經存在的檔案,使用<把第一個命令的輸入重新導向到這個檔案即可。(譯者註:從檔案開始也符合過濾器的哲學,就是過濾器只處理資料而不產生資料)。
把輸出作為參數
在前面的管道的討論中,你學會了如何利用一個命令的輸出作為另一個命令的輸入。假設你想要一個命令的輸出或者是一個檔案的內容作為一個命令的參數而不是輸入呢?管道無法做到這個,解決方案是:
- 使用xargs命令
- 使用 find -exec
- 命令替換
現在我們來學習前兩種。命令替換可以在命令列上使用,但是更經常的是出現在指令碼中。使用xargs命令xargs命令從標準輸入讀取資料,然後構建和執行命令,並把讀取的資料作為命令的參數。如果沒有命令,那麼預設使用echo。如下例子:
[root@localhost lpi103-4]# cat text1
1 apple
2 pear
3 banana
[root@localhost lpi103-4]# xargs<text1
1 apple 2 pear 3 banana為什麼只有一行輸出呢?預設情況下,xargs根據空白分隔輸入,每一個分隔的部分作為一個參數。然而,當xargs構建命令的時候,它會一次性傳遞儘可能多的參數。可以用過-n 或者--max-args來修正這種預設的行為。如下所示:
[root@localhost lpi103-4]# xargs<text1 echo "args >"
args > 1 apple 2 pear 3 banana
[root@localhost lpi103-4]# xargs --max-args 3 <text1 echo "args >"
args > 1 apple 2
args > pear 3 banana
[root@localhost lpi103-4]# xargs -n 1 <text1 echo "args >"
args > 1
args > apple
args > 2
args > pear
args > 3
args > banana
如果輸入中的空白被單引號或雙引號包圍,或者被反斜線轉義,那麼xargs就不會分隔輸入。如下:
[root@localhost lpi103-4]# echo '"4 plum"' | cat text1 -
1 apple
2 pear
3 banana
"4 plum"
[root@localhost lpi103-4]# echo '"4 plum"' | cat text1 - | xargs -n 1
1
apple
2
pear
3
banana
4 plum
目前為止,所有的參數都被加到了命令的後面。如果你需要在其他的參數中使用它們,那麼可以使用-I選項。如下:
[root@localhost lpi103-4]# xargs -I XYZ echo "START XYZ REPEAT XYZ END" <text1
START 1 apple REPEAT 1 apple END
START 2 pear REPEAT 2 pear END
START 3 banana REPEAT 3 banana END
[root@localhost lpi103-4]# xargs -IX echo "<X><X>" <text1
<1 apple><1 apple>
<2 pear><2 pear>
<3 banana><3 banana>
[root@localhost lpi103-4]#
[root@localhost lpi103-4]# cat text1 text2 | xargs -L2
1 apple 2 pear
3 banana 9 plum
3 banana 10 apple
其中 -L選項告訴xargs把每一行當作一個參數。
儘管例子中我們使用了一個簡單的文字檔來示範,實際中,你很少會這麼使用,通常使用的輸入是來自ls,grep等命令的輸出。如下:
[root@localhost lpi103-4]# ls | xargs grep "1"
text1:1 apple
text2:10 apple
xaa:1 apple
yaa:1
如果上面例子中一個或者多個檔案名稱包含空格會如何呢?答案是會產生錯誤。
對於ls命令,你可以使用--quoting-style選項來強製為檔案名稱增加引號或轉義。一個更好的解決方案是使用xargs的-0選項,這樣xargs就使用\0來分隔輸入的參數。儘管ls不支援選項來產生\0結尾的檔案名稱輸出,很多其他的命令都支援。
下面的例子中展示了這些技巧。
[root@localhost lpi103-4]# cp text1 "text 1"
[root@localhost lpi103-4]# ls *1 | xargs grep "1" # error
text1:1 apple
grep: text: No such file or directory
grep: 1: No such file or directory
[root@localhost lpi103-4]# ls --quoting-style escape *1
text1 text\ 1
[root@localhost lpi103-4]# ls --quoting-style shell *1
text1 'text 1'
[root@localhost lpi103-4]# ls --quoting-style shell *1 | xargs grep "1"
text1:1 apple
text 1:1 apple
[root@localhost lpi103-4]# ls *1 | tr '\n' '\0' | xargs -0 grep "1"
text1:1 apple
text 1:1 apple
xargs不能構建任意長度的命令。直到Linux 核心的 2.26.3版本中,命令的最大數量也是有限的。像rm somepath/*,如果是一個有很多長檔名的檔案的目錄,那麼這可能會失敗,並告知參數太長。在一些老的Linux版本或者Unix系統中,這個限制可能仍然會存在。
可以使用xargs的--show-limits選項來顯示預設的限制,使用-s選項來設定這個限制的大小。
使用find -exec或者 find和xargs一起使用
在“檔案與目錄管理”中,我們學會了如何根據檔案名稱、修改時間、檔案大小、或者其他的檔案屬性使用find來尋找檔案。一旦你得到了這樣一組檔案,你通常會對他們做些什麼:刪除它們、複製、重新命名、或者其他動作。現在讓我們來看看find的-exec選項,它具有和xargs類似的功能。
[root@localhost lpi103-4]# find text[12] -exec cat text3 {} \;
This is a sentence. This is a sentence. This is a sentence.
1 apple
2 pear
3 banana
This is a sentence. This is a sentence. This is a sentence.
9 plum
3 banana
10 apple
把它與xargs相比,你會發現很多不同點:
- 你必須使用{}來標記需要增加檔案名稱的地方,檔案名稱不會自動被添加到命令後;
- 你必須使用;來結束命令,因為shell也使用這個元字元,所以需要轉義或者引號包圍;
- 對於每一個輸入的檔案名稱執行一次命令。
對應的xargs版本的程式如下:
[root@localhost lpi103-4]# find text[12] |xargs cat text3
This is a sentence. This is a sentence. This is a sentence.
1 apple
2 pear
3 banana
9 plum
3 banana
10 apple現在讓我們回到“檔案名稱中有空格”的問題。如下:
[root@localhost lpi103-4]# find . -name "*1" -exec grep "1" {} \;
1 apple
1 apple
目前為止,一切OK。但是是不是丟失了什麼東西呢?是哪一個檔案包含grep找的行呢?檔案名稱不見了!這是因為對每一個檔案都執行一次grep,而grep只尋找一個檔案時不會給出檔案名稱在輸出中。如果使用xargs會如何呢?我們前面已經看到過含空格檔案名稱的問題了。解決方案有兩種:一是使用find的-print0 選項來產生以\0分隔的結果輸出;二是現代版本的find可以使用+而不是;來結束命令,這將導致find會傳遞儘可能多的檔案名稱,從而只執行一次命令。
[root@localhost lpi103-4]# find . -name "*1" -print0 | xargs -0 grep "1"
./text 1:1 apple
./text1:1 apple
[root@localhost lpi103-4]# find . -name "*1" -exec grep "1" {} +
./text 1:1 apple
./text1:1 apple通常情況下,上述兩種方法都可以使用,只是個人好惡問題。記住,在管道中傳送沒有處理過的空格會產生問題,所以使用find -print0和xargs -0 來處理它們。其他的命令,如tar也支援-0選項,所以如果你一個命令支援-0,那麼就儘可能的使用這個選項吧!最後需要說的是如果操作很多檔案,那麼一定要做好備份,並且在執行實際操作之前要驗證好。多路輸出tee這部分通過簡要討論另外一個命令來結束本文。有時候你可能需要在螢幕上看到輸出,同時還想把輸出寫入檔案供以後查閱。當然你可以通過把命令的輸出重新導向到一個檔案中,然後使用tail -f 命令來即時查閱這個檔案來實現。然而還有另外一個更簡單的方法,那就是使用tee命令。你可以在管線中使用tee。它的參數是一個或者多個檔案名稱來存放stdout的內容。使用-a選項會附加而不是覆蓋掉檔案原來的內容。前面我們也說過,你需要在管道之前就把stderr重新導向到stdout,這樣才能在結果中儲存兩者。如下列:
[root@localhost lpi103-4]# ls text[1-3] | tee f1 f2
text1
text2
text3
[root@localhost lpi103-4]# cat f1
text1
text2
text3
[root@localhost lpi103-4]# cat f2
text1
text2
text3