##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.
我們可以用如下命令測試一下:
```shell
$ mail -s test root
this is a test mail。
please skip.
^d (同時按下ctrl 跟d鍵)
```
很明顯,`mail`進程所讀進的數據,就是從
`stdin` 也就是keyboard讀進的。
不過,不見得每個進程的`stdin`都跟`mail`一樣
從`keyboard`讀進,因為進程的作者可以從文件參數讀進`stdin`,
如:
```shell
$ cat /etc/passwd
```
但,要是`cat`之后沒有文件參數則如何呢?
哦, 請你自己玩玩看...^_^
```shell
$ cat
```
> **Tips:**
> 請留意數據輸出到哪里去了,
> 最后別忘了按`ctrl+d`(`^d`), 退出stdin輸入。
至于`stdout`與`stderr`,嗯...等我有空再續吧...^_^
還是,有哪位前輩來玩接龍呢?
相信,經過上一個練習后,
你對`stdin`與`stdout`應該不難理解了吧?
然后,讓我們看看`stderr`好了。
事實上,`stderr`沒什么難理解的:
說白了就是“錯誤信息”要往哪里輸出而已...
比方說, 若讀進的文件參數不存在的,
那我們在monitor上就看到了:
```shell
$ ls no.such.file
ls: no.such.file: No such file or directory
```
若同一個命令,同時成生`stdout`與`stderr`呢?
那還不簡單,都送到monitor來就好了:
```shell
$ 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)
--------
比方說:
```shell
$ cat < my.file
```
就是從my.file讀入數據
```shell
$ mail -s test root < /etc/passwd
```
則是從/etc/passwd讀入...
這樣一來,stdin將不再是從keyboard讀入,
而是從指定的文件讀入了...
嚴格來說,**`<`符號之前需要指定一個fd的(之前不能有空白),但因為0是`<`的預設值,因此,`<`與`0<`是一樣的***。
okay,這樣好理解了吧?
那要是用兩個`<`,即`<<`又是啥呢?
**這是所謂的`here document`,
它可以讓我們輸入一段文本,
直到讀到`<<` 后指定的字符串**。
比方說:
```shell
$ cat <<EOF
first line here
second line here
third line here
EOF
```
這樣的話, `cat`會讀入3個句子,
而無需從keyboard讀進數據且要等到(ctrl+d, ^d)結束輸入。
-------------
#####2.2 重定向輸出`>n`(output redirection)
-------------
當你搞懂了`0<` 原來就是改變`stdin`的數據輸入通道之后,
相信要理解如下兩個redirection就不難了:
- `1>` #改變stdout的輸出通道;
- `2>` #改變stderr的輸出通道;
兩者都是將原來輸出到monitor的數據,
重定向輸出到指定的文件了。
**由于1是`>`的預設值,
因此,`1>`與`>`是相同的,都是改變`stdout`**.
用上次的ls的例子說明一下好了:
```shell
$ ls my.file no.such.file 1>file.out
ls: no.such.file: No such file or directory
```
這樣monitor的輸出就只剩下`stderr`的輸出了,
因為`stdout`重定向輸出到文件file.out去了。
```shell
$ ls my.file no.such.file 2>file.err
my.file
```
這樣monitor就只剩下了`stdout`,
因為`stderr`重定向輸出到文件file.err了。
```shell
$ ls my.file no.such.file 1>file.out 2>file.err
```
這樣monitor就啥也沒有了,
因為`stdout`與`stderr`都重定向輸出到文件了。
呵呵,看來要理解`>`一點也不難啦是不? 沒騙你吧? ^_^
**不過有些地方還是要注意一下的**。
```shell
$ ls my.file no.such.file 1>file.both 2>file.both
```
假如`stdout`(1)與`stderr`(2)都同時在寫入file.both的話,
則是采取"覆蓋"的方式:后來寫入覆蓋前面的。
讓我們假設一個`stdout`與`stderr`同時寫入到file.out的情形好了;
- 首先`stdout`寫入10個字符
- 然后`stderr`寫入6個字符
那么,這時原本的`stdout`輸出的10個字符,
將被`stderr`輸出的6個字符覆蓋掉了。
那如何解決呢?所謂山不轉路轉,路不轉人轉嘛,
我們可以換一個思維:
將`stderr`導進`stdout`
或者將`stdout`導進到`stderr`,
而不是大家在搶同一份文件,不就行了。
bingo就是這樣啦:
- 2>&1 #將`stderr`并進`stdout`輸出
- 1>&2 或者 >&2 #將`stdout`并進`stderr`輸出。
于是,前面的錯誤操作可以改寫為:
```shell
$ 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(也不想存到文件), 那就可以:
```shell
$ ls my.file no.such.file 2>/dev/null
my.file
```
若要相反:只想看到stderr呢?
還不簡單將stdout,重定向的/dev/null就行:
```shell
$ ls my.file no.such.file >/dev/null
ls: no.such.file: No such file or directory
```
那接下來,假如單純的只跑進程,而不想看到任何輸出呢?
哦,這里留了一手,上次沒講的法子,專門贈與有緣人... ^_^
除了用 `>/dev/null 2>&1`之外,你還可以如此:
```shell
$ ls my.file no.such.file &>/dev/null
```
> **Tips:**
>將&>換成>&也行!
------
#####2.4 重定向輸出append (`>>`)
-------
okay? 請完佛,接下來,再讓我們看看如下情況:
```shell
$ echo "1" > file.out
$ cat file.out
1
$ echo "2" > file.out
$ cat file.out
2
```
看來,我們在重定向stdout或stderr進一個文件時,
似乎永遠只能獲得最后一次的重定向的結果.
那之前的內容呢?
呵呵,要解決這個問題,很簡單啦,將`>`換成`>>` 就好了;
```shell
$ echo "3" >> file.out
$ cat file.out
2
3
```
如此一來,被重定向的文件的之前的內容并不會丟失,
而新的內容則一直追加在最后面去。so easy?...
但是,只要你再次使用`>`來重定向輸出的話,
那么,原來文件的內容被truncated(清洗掉)。
這是,你要如何避免呢?
----備份, yes,我聽到了,不過,還有更好的嗎?
既然與施主這么有緣分,老衲就送你一個錦囊妙法吧:
```shell
$ set -o noclobber
$ echo "4" > file.out
-bash:file: cannot overwrite existing file.
```
那,要如何取消這個限制呢?
哦,將`set -o `換成 `set +o`就行了:
```shell
$ set +o noclobber
$ echo "5" > file.out
$ cat file.out
5
```
再問:那有辦法不取消而又“臨時”改寫目標文件嗎?
哦,佛曰:不可告也。
啊,~~~開玩笑的,開玩笑啦~~~^_^,
哎,早就料到人心是不足的了
```shell
$ set -o noclobber
$ echo "6" >| file.out
$ cat file.out
6
```
留意到沒有:
**在`>`后面加個`|`就好,
注意: `>`與`|`之間不能有空白哦**...
-----
#####2.5 I/O Redirection的優先級
-----
呼....(深呼吸吐納一下吧)~~~ ^_^
再來還有一個難題要你去參透呢:
```shell
$ 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`。
但這時候文件的內容已被清空了,因此就變成了讀不進任何數據。
哦,~~~原來如此~~~^_^
那...如下兩例又如何呢?
```shell
$ 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是空的,(當然了,你都將
水管接到別的水池了)
聰明的你或許會如此解決:
```shell
cmd1 | cmd2 >file; cmd3 < file
```
是的,你可以這樣做,但最大的壞處是:
file I/O會變雙倍,在command執行的整個過程中,
file I/O是最常見的最大效能殺手。
凡是有經驗的shell操作者,都會盡量避免或降低file I/O的頻度。
那上面問題還有更好的方法嗎?
有的,那就是`tee`命令了。
**所謂的`tee`命令是在不影響原本I/O的情況下,
將stdout賦值到一個文件中去。**
因此,上面的命令行,可以如此執行:
```shell
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上好玩的東西。