shell十三問之11:>與< 差在哪?


這次的題目,之前我在CU的shell版說明過了: (原帖的連接在論壇改版後,已經失效) 這次我就不重寫了,將帖子的內容“抄”下來就是了...

1. 文件描述符(fd, File Descriptor)


談到I/O redirection,不妨先讓我們認識一下File Descriptor(fd,文件描述符)。

進程的運算,在大部分情況下,都是進行數據(data)的處理, 這些數據從哪裡,讀進來?又輸出到哪裡呢? 這就是file descriptor(fd)的功用了。

在shell的進程中,最常使用的fd大概有三個,分別為:

  • 0:standard Input (STDIN)
  • 1: standard output(STDOUT)
  • 2: standard Error output (STDERR

在標準情況下,這些fd分別跟如下設備(device)關聯:

  • stdin(0): keyboard
  • stdout(1): monitor
  • stderr(2): monitor

Tips: linux中的文件描述符(fd)用整數表示。 linux中任何一個進程都默認打開三個文件, 這三個文件對應的文件描述符分別是:0, 1, 2; 即stdin, stdout, stderr.

我們可以用如下命令測試一下:

$ mail -s test root
this is a test mail。
please skip.
^d (同時按下ctrl 跟d鍵)

很明顯,mail進程所讀進的數據,就是從 stdin 也就是keyboard讀進的。 不過,不見得每個進程的stdin都跟mail一樣 從keyboard讀進,因為進程的作者可以從文件參數讀進stdin, 如:

$ cat /etc/passwd

但,要是cat之後沒有文件參數則如何呢? 哦, 請你自己玩玩看...^_^

$ cat

Tips:

請留意數據輸出到哪裡去了, 最後別忘了按ctrl+d(^d), 退出stdin輸入。

至於stdoutstderr,嗯...等我有空再續吧...^_^ 還是,有哪位前輩來玩接龍呢?

相信,經過上一個練習後, 你對stdinstdout應該不難理解了吧? 然後,讓我們看看stderr好了。

事實上,stderr沒什麼難理解的: 說白了就是“錯誤信息”要往哪裡輸出而已... 比方說, 若讀進的文件參數不存在的, 那我們在monitor上就看到了:

$ ls no.such.file
ls: no.such.file: No such file or directory

若同一個命令,同時成生stdoutstderr呢? 那還不簡單,都送到monitor來就好了:

$ touch my.file
$ ls my.file on.such.file
ls: no.such.file: No such file or directory
my.file

okay, 至此,關於fd及其名稱、還有相關聯的設備, 相信你已經沒問題了吧?


2. I/O 重定向(I/O Redirection)


那好,接下來讓我們看看如何改變這些fd的預設數據通道。

  • < 來改變讀進的數據通道(stdin),使之從指定的文件讀進。
  • > 來改變輸出的數據通道(stdout,stderr),使之輸出到指定的文件。

2.1 輸入重定向n<(input redirection)

比方說:

$ cat < my.file

就是從my.file讀入數據

$ mail -s test root < /etc/passwd

則是從/etc/passwd讀入...

這樣一來,stdin將不再是從keyboard讀入, 而是從指定的文件讀入了...

嚴格來說,<符號之前需要指定一個fd的(之前不能有空白),但因為0是<的預設值,因此,<0<是一樣的*

okay,這樣好理解了吧?

那要是用兩個<,即<<又是啥呢? 這是所謂的here document, 它可以讓我們輸入一段文本, 直到讀到<< 後指定的字符串

比方說:

$ cat <

這樣的話, cat會讀入3個句子, 而無需從keyboard讀進數據且要等到(ctrl+d, ^d)結束輸入。


2.2 重定向輸出>n(output redirection)

當你搞懂了0< 原來就是改變stdin的數據輸入通道之後, 相信要理解如下兩個redirection就不難了:

  • 1> #改變stdout的輸出通道;
  • 2> #改變stderr的輸出通道;

兩者都是將原來輸出到monitor的數據, 重定向輸出到指定的文件了。

由於1是>的預設值, 因此,1>>是相同的,都是改變stdout.

用上次的ls的例子說明一下好了:

$ ls my.file no.such.file 1>file.out
ls: no.such.file: No such file or directory

這樣monitor的輸出就只剩下stderr的輸出了, 因為stdout重定向輸出到文件file.out去了。

$ ls my.file no.such.file 2>file.err
my.file

這樣monitor就只剩下了stdout, 因為stderr重定向輸出到文件file.err了。

$ ls my.file no.such.file 1>file.out 2>file.err

這樣monitor就啥也沒有了, 因為stdoutstderr都重定向輸出到文件了。

呵呵,看來要理解>一點也不難啦是不? 沒騙你吧? ^_^ 不過有些地方還是要注意一下的

$ ls my.file no.such.file 1>file.both 2>file.both

假如stdout(1)與stderr(2)都同時在寫入file.both的話, 則是採取"覆蓋"的方式:後來寫入覆蓋前面的。

讓我們假設一個stdoutstderr同時寫入到file.out的情形好了;

  • 首先stdout寫入10個字符
  • 然後stderr寫入6個字符

那麼,這時原本的stdout輸出的10個字符, 將被stderr輸出的6個字符覆蓋掉了。

那如何解決呢?所謂山不轉路轉,路不轉人轉嘛, 我們可以換一個思維: 將stderr導進stdout 或者將stdout導進到stderr, 而不是大家在搶同一份文件,不就行了。 bingo就是這樣啦:

  • 2>&1 #將stderr並進stdout輸出
  • 1>&2 或者 >&2 #將stdout並進stderr輸出。

於是,前面的錯誤操作可以改寫為:

$ ls my.file no.such.file 1>file.both 2>&1
$ ls my.file no.such.file 2>file.both >&2

這樣,不就皆大歡喜了嗎? ~~~ ^_^

不過,光解決了同時寫入的問題還不夠, 我們還有其他技巧需要了解的。 故事還沒有結束,別走開廣告後,我們在回來....


2.3 I/O重定向與linux中的/dev/null

okay,這次不講I/O Redirection, 請佛吧... (有沒有搞錯?網中人是否頭殼燒壞了?...)嘻~~~^_^

學佛的最高境界,就是"四大皆空"。 至於是空哪四大塊,我也不知,因為我還沒有到那個境界.. 這個“空”字,卻非常值得反覆把玩: ---色即是空,空即是色 好了,施主要是能夠領會"空"的禪意,那離修成正果不遠了。

在linux的文件系統中,有個設備文件: /dev/null. 許多人都問過我,那是什麼玩意兒? 我跟你說好了,那就是"空"啦。

沒錯空空如也的空就是null了... 請問施主是否忽然有所頓悟了呢? 然則恭喜了。

這個null在 I/O Redirection中可有用的很呢?

  • 將fd 1跟fd 2重定向到/dev/null去,就可忽略stdout, stderr的輸出。
  • 將fd 0重定向到/dev/null,那就是讀進空(nothing).

比方說,我們在執行一個進程時,會同時輸出到stdout與stderr, 假如你不想看到stderr(也不想存到文件), 那就可以:

$ ls my.file no.such.file 2>/dev/null
my.file

若要相反:只想看到stderr呢? 還不簡單將stdout,重定向的/dev/null就行:

$ ls my.file no.such.file >/dev/null
ls: no.such.file: No such file or directory

那接下來,假如單純的只跑進程,而不想看到任何輸出呢? 哦,這裡留了一手,上次沒講的法子,專門贈與有緣人... ^_^ 除了用 >/dev/null 2>&1之外,你還可以如此:

$ ls my.file no.such.file &>/dev/null

Tips:

將&>換成>&也行!


2.4 重定向輸出append (>>)

okay? 請完佛,接下來,再讓我們看看如下情況:

$ echo "1" > file.out
$ cat file.out
1
$ echo "2" > file.out
$ cat file.out
2

看來,我們在重定向stdout或stderr進一個文件時, 似乎永遠只能獲得最後一次的重定向的結果. 那之前的內容呢?

呵呵,要解決這個問題,很簡單啦,將>換成>> 就好了;

$ echo "3" >> file.out
$ cat file.out
2
3

如此一來,被重定向的文件的之前的內容並不會丟失, 而新的內容則一直追加在最後面去。so easy?...

但是,只要你再次使用>來重定向輸出的話, 那麼,原來文件的內容被truncated(清洗掉)。 這是,你要如何避免呢? ----備份, yes,我聽到了,不過,還有更好的嗎? 既然與施主這麼有緣分,老衲就送你一個錦囊妙法吧:

$ set -o noclobber
$ echo "4" > file.out
-bash:file: cannot overwrite existing file.

那,要如何取消這個限制呢? 哦,將set -o換成 set +o就行了:

$ set +o noclobber
$ echo "5" > file.out
$ cat file.out
5

再問:那有辦法不取消而又“臨時”改寫目標文件嗎? 哦,佛曰:不可告也。 啊,~開玩笑的,開玩笑啦~^_^, 哎,早就料到人心是不足的了

$ set -o noclobber
$ echo "6" >| file.out
$ cat file.out
6

留意到沒有: >後面加個|就好, 注意: >|之間不能有空白哦...


2.5 I/O Redirection的優先級

呼....(深呼吸吐納一下吧)~~~ ^_^ 再來還有一個難題要你去參透呢:

$ echo "some text here" >file
$ cat < file
some text here
$cat < file >file.bak
$cat < file.bak
some text here
$cat < file >file

嗯?注意到沒有? ---怎麼最後那個cat命令看到file是空的呢? why? why? why?

前面提到:$cat < file > file之後, 原本有內容的文件,結果卻被清空了。 要理解這個現象其實不難, 這只是priority的問題而已: 在IO Redirection中, stdout與stderr的管道先準備好, 才會從stdin讀入數據。 也就是說,在上例中,>file會將file清空, 然後才讀入 < file。 但這時候文件的內容已被清空了,因此就變成了讀不進任何數據。

哦,~原來如此~^_^ 那...如下兩例又如何呢?

$ cat <> file
$ cat < file >>file

嗯...同學們,這兩個答案就當練習題嘍, 下課前交作業。

Tips: 我們瞭解到>file能夠快速把文件file清空; 或者使用:>file同樣可以清空文件, :>file>file的功能: 若文件file存在,則將file清空; 否則,創建空文件file (等效於touch file); 二者的差別在於>file的方式不一定在所有的shell的都可用。

exec 5<>file; echo "abcd" >&5; cat <&5 將file文件的輸入、輸出定向到文件描述符5, 從而描述符5可以接管file的輸入輸出; 因此,cat <>file等價於cat < file

cat < file >>file則使file內容成幾何級數增長。

好了, I/O Redirection也快講完了, sorry,因為我也只知道這麼多而已啦~嘻~^_^ 不過,還有一樣東東是一定要講的,各位觀眾(請自行配樂~!#@$%): 就是pipe line也。


2.6 管道(pipe line)

談到pipe line,我相信不少人都不會陌生: 我們在很多command line上常看到|符號就是pipe line了。

不過,pipe line究竟是什麼東東呢? 別急別急...先查一下英文字典,看看pipe是什麼意思? 沒錯他就是“水管”的意思... 那麼,你能想象一下水管是怎樣一個根接一根的嗎? 又, 每根水管之間的input跟output又如何呢? 靈光一閃:原來pipe line的I/O跟水管的I/O是一模一樣的: 上一個命令的stdout接到下一個命令的stdin去了 的確如此。不管在command line上使用了多少個pipe line, 前後兩個command的I/O是彼此連接的 (恭喜:你終於開放了 ^_^ )

不過...然而...但是... ...stderr呢? 好問題不過也容易理解: 若水管漏水怎麼辦? 也就是說:在pipe line之間, 前一個命令的stderr是不會接進下一個命令的stdin的, 其輸出,若不用2>file的話,其輸出在monitor上來。 這點請你在pipe line運用上務必要注意的。

那,或許你有會問: 有辦法將stderr也喂進下一個命令的stdin嗎? (貪得無厭的傢伙),方法當然是有的,而且,你早已學習過了。 提示一下就好:**請問你如何將stderr合併進stdout一同輸出呢? 若你答不出來,下課後再來問我...(如果你臉皮足夠厚的話...)

或許,你仍意猶未盡,或許,你曾經碰到過下面的問題: 在cmd1 | cmd2 | cmd3 | ... 這段pipe line中如何將cmd2的輸出保存到一個文件呢?

若你寫成cmd1 | cmd2 >file | cmd3的話, 那你肯定會發現cmd3的stdin是空的,(當然了,你都將 水管接到別的水池了) 聰明的你或許會如此解決:

cmd1 | cmd2 >file; cmd3 < file

是的,你可以這樣做,但最大的壞處是: file I/O會變雙倍,在command執行的整個過程中, file I/O是最常見的最大效能殺手。 凡是有經驗的shell操作者,都會盡量避免或降低file I/O的頻度。

那上面問題還有更好的方法嗎? 有的,那就是tee命令了。 所謂的tee命令是在不影響原本I/O的情況下, 將stdout賦值到一個文件中去。 因此,上面的命令行,可以如此執行:

cmd1 | cmd2 | tee file | cmd3

在預設上,tee會改寫目標文件, 若你要改為追加內容的話,那可用-a參數選項。

基本上,pipe line的應用在shell操作上是非常廣泛的。 尤其是在text filtering方面, 如,cat, more, head, tail, wc, expand, tr, grep, sed, awk...等等文字處理工具。 搭配起pipe line 來使用,你會覺得 command line 原來活得如此精彩的。 常讓人有“眾裡尋他千百度,驀然回首,那人卻在燈火闌珊處”之感...

好了,關於I/O Redirection的介紹就到此告一段落。 若日後,有空的話,在為大家介紹其他在shell上好玩的東西。