Chapter 16. I/O 重新導向
預設情況下始終有3個"檔案"處於開啟狀態, (鍵盤), (螢幕), and (錯誤訊息輸出到螢幕上). 這3個檔案和其他開啟的檔案都可以被重新導向. 對於重新導向簡單的解釋就是捕捉一個檔案, 命令, 程式, 指令碼, 或者甚至是指令碼中的代碼塊(參見Example 3-1
和
Example 3-2)的輸出, 然後將這些輸出作為輸入發送到另一個檔案, 命令, 程式, 或指令碼中.
每個開啟的檔案都會被分配一個檔案描述符.[1],, 和的檔案描述符分別是0, 1, 和 2. 對於正在開啟的額外檔案, 保留了描述符3到9. 在某些時候將這些格外的檔案描述符分配給stdin,
stdout, 或者是stderr作為臨時的副本連結是非常有用的.[2] 在經過複雜的重新導向和重新整理之後需要把它們恢複成正常的樣子 (參見
Example 16-1).
1 COMMAND_OUTPUT > 2 # 重新導向stdout到一個檔案. 3 # 如果沒有這個檔案就建立, 否則就覆蓋. 4 5 ls -lR > dir-tree.list 6 # 建立一個包含分類樹列表的檔案. 7 8 : > filename 9 # > 會把檔案"filename"截斷為0長度. 10 # 如果檔案不存在, 那麼就建立一個0長度的檔案(與'touch'的效果相同). 11 # : 是一個預留位置, 不產生任何輸出. 12 13 > filename 14 # > 會把檔案"filename"截斷為0長度. 15 # 如果檔案不存在, 那麼就建立一個0長度的檔案(與'touch'的效果相同). 16 # (與上邊的": >"效果相同, 但是在某些shell下可能不能工作.) 17 18 COMMAND_OUTPUT >> 19 # 重新導向stdout到一個檔案. 20 # 如果檔案不存在, 那麼就建立它, 如果存在, 那麼就追加到檔案後邊. 21 22 23 # 單行重新導向命令(只會影響它們所在的行): 24 # -------------------------------------------------------------------- 25 26 1>filename 27 # 重新導向stdout到檔案"filename". 28 1>>filename 29 # 重新導向並追加stdout到檔案"filename". 30 2>filename 31 # 重新導向stderr到檔案"filename". 32 2>>filename 33 # 重新導向並追加stderr到檔案"filename". 34 &>filename 35 # 將stdout和stderr都重新導向到檔案"filename". 36 37 #============================================================================== 38 # 重新導向stdout, 一次一行. 39 LOGFILE=script.log 40 41 echo "This statement is sent to the log file, \"$LOGFILE\"." 1>$LOGFILE 42 echo "This statement is appended to \"$LOGFILE\"." 1>>$LOGFILE 43 echo "This statement is also appended to \"$LOGFILE\"." 1>>$LOGFILE 44 echo "This statement is echoed to stdout, and will not appear in \"$LOGFILE\"." 45 # 每行過後, 這些重新導向命令會自動"reset". 46 47 48 49 # 重新導向stderr, 一次一行. 50 ERRORFILE=script.errors 51 52 bad_command1 2>$ERRORFILE # 錯誤訊息發到$ERRORFILE中. 53 bad_command2 2>>$ERRORFILE # 錯誤訊息添加到$ERRORFILE中. 54 bad_command3 # 錯誤訊息echo到stderr, 55 #+ 並且不出現在$ERRORFILE中. 56 # 每行過後, 這些重新導向命令也會自動"reset". 57 #============================================================================== 58 59 60 61 2>&1 62 # 重新導向stderr到stdout. 63 # 得到的錯誤訊息與stdout一樣, 發送到一個地方. 64 65 i>&j 66 # 重新導向檔案描述符i 到 j. 67 # 指向i檔案的所有輸出都發送到j中去. 68 69 >&j 70 # 預設的, 重新導向檔案描述符1(stdout)到 j. 71 # 所有傳遞到stdout的輸出都送到j中去. 72 73 0< FILENAME 74 < FILENAME 75 # 從檔案中接受輸入. 76 # 與">"是成對命令, 並且通常都是結合使用. 77 # 78 # grep search-word <filename 79 80 81 [j]<>filename 82 # 為了讀寫"filename", 把檔案"filename"開啟, 並且分配檔案描述符"j"給它. 83 # 如果檔案"filename"不存在, 那麼就建立它. 84 # 如果檔案描述符"j"沒指定, 那預設是fd 0, stdin. 85 # 86 # 這種應用通常是為了寫到一個檔案中指定的地方. 87 echo 1234567890 > File # 寫字串到"File". 88 exec 3<> File # 開啟"File"並且給它分配fd 3. 89 read -n 4 <&3 # 唯讀4個字元. 90 echo -n . >&3 # 寫一個小數點. 91 exec 3>&- # 關閉fd 3. 92 cat File # ==> 1234.67890 93 # 隨機儲存. 94 95 96 97 | 98 # 管道. 99 # 通用目的的處理和命令鏈工具. 100 # 與">"很相似, 但是實際上更通用. 101 # 對於想將命令, 指令碼, 檔案和程式串聯起來的時候很有用. 102 cat *.txt | sort | uniq > result-file 103 # 對所有的.txt檔案的輸出進行排序, 並且重複資料刪除行, 104 # 最後將結果儲存到"result-file"中. |
可以將輸入輸出重新導向和(或)管道的多個執行個體結合到一起寫在一行上.
1 command < input-file > output-file 2 3 command1 | command2 | command3 > output-file |
參見
Example 12-28 和
Example A-15.
可以將多個輸出資料流重新導向到一個檔案上.
1 ls -yz >> command.log 2>&1 2 # 將錯誤選項"yz"的結果放到檔案"command.log"中. 3 # 因為stderr被重新導向到這個檔案中, 4 #+ 所有的錯誤訊息也就都指向那裡了. 5 6 # 注意, 下邊這個例子就不會給出相同的結果. 7 ls -yz 2>&1 >> command.log 8 # 輸出一個錯誤訊息, 但是並不寫到檔案中. 9 10 # 如果將stdout和stderr都重新導向, 11 #+ 命令的順序會有些不同. |
關閉檔案描述符
-
n<&-
-
關閉輸入檔案描述符n.
-
0<&-, <&-
-
關閉.
-
n>&-
-
關閉輸出檔案描述符.
-
1>&-, >&-
-
關閉.
子進程繼承了開啟的檔案描述符. 這就是為什麼管道可以工作. 如果想阻止fd被繼承, 那麼可以關掉它.
1 # 只重新導向stderr到一個管道. 2 3 exec 3>&1 # 儲存當前stdout的"值". 4 ls -l 2>&1 >&3 3>&- | grep bad 3>&- # 對'grep'關閉fd 3(但不關閉'ls'). 5 # ^^^^ ^^^^ 6 exec 3>&- # 現在對於剩餘的指令碼關閉它. 7 8 # Thanks, S.C. |
如果想瞭解關於I/O重新導向更多的細節參見
附錄 E.
16.1. 使用exec
exec <filename 命令會將stdin重新導向到檔案中. 從這句開始, 後邊的輸入就都來自於這個檔案了, 而不是標準輸入了(通常都是鍵盤輸入). 這樣就提供了一種按行讀取檔案的方法, 並且可以使用sed 和/或awk來對每一行進行分析.
Example 16-1. 使用exec重新導向標準輸入
1 #!/bin/bash 2 # 使用'exec'重新導向標準輸入. 3 4 5 exec 6<&0 # 將檔案描述符#6與stdin連結起來. 6 # 儲存了stdin. 7 8 exec < data-file # stdin被檔案"data-file"所代替. 9 10 read a1 # 讀取檔案"data-file"的第一行. 11 read a2 # 讀取檔案"data-file"的第二行. 12 13 echo 14 echo "Following lines read from file." 15 echo "-------------------------------" 16 echo $a1 17 echo $a2 18 19 echo; echo; echo 20 21 exec 0<&6 6<&- 22 # 現在將stdin從fd #6中恢複, 因為剛才我們把stdin重新導向到#6了, 23 #+ 然後關閉fd #6 ( 6<&- ), 好讓這個描述符繼續被其他進程所使用. 24 # 25 # <&6 6<&- 這麼做也可以. 26 27 echo -n "Enter data " 28 read b1 # 現在"read"已經恢複正常了, 就是從stdin中讀取. 29 echo "Input read from stdin." 30 echo "----------------------" 31 echo "b1 = $b1" 32 33 echo 34 35 exit 0 |
同樣的, exec >filename 命令將會把stdout重新導向到一個指定的檔案中. 這樣所有的命令輸出就都會發向那個指定的檔案, 而不是stdout.
Example 16-2. 使用exec來重新導向stdout
1 #!/bin/bash 2 # reassign-stdout.sh 3 4 LOGFILE=logfile.txt 5 6 exec 6>&1 # 將fd #6與stdout相串連. 7 # 儲存stdout. 8 9 exec > $LOGFILE # stdout就被檔案"logfile.txt"所代替了. 10 11 # ----------------------------------------------------------- # 12 # 在這塊中所有命令的輸出就都發向檔案 $LOGFILE. 13 14 echo -n "Logfile: " 15 date 16 echo "-------------------------------------" 17 echo 18 19 echo "Output of \"ls -al\" command" 20 echo 21 ls -al 22 echo; echo 23 echo "Output of \"df\" command" 24 echo 25 df 26 27 # ----------------------------------------------------------- # 28 29 exec 1>&6 6>&- # 恢複stdout, 然後關閉檔案描述符#6. 30 31 echo 32 echo "== stdout now restored to default == " 33 echo 34 ls -al 35 echo 36 37 exit 0 |
Example 16-3. 使用exec在同一指令碼中重新導向stdin和stdout
1 #!/bin/bash 2 # upperconv.sh 3 # 將一個指定的輸入檔案轉換為大寫. 4 5 E_FILE_ACCESS=70 6 E_WRONG_ARGS=71 7 8 if [ ! -r "$1" ] # 判斷指定的輸入檔案是否可讀? 9 then 10 echo "Can't read from input file!" 11 echo "Usage: $0 input-file output-file" 12 exit $E_FILE_ACCESS 13 fi # 即使輸入檔案($1)沒被指定 14 #+ 也還是會以相同的錯誤退出(為什麼?). 15 16 if [ -z "$2" ] 17 then 18 echo "Need to specify output file." 19 echo "Usage: $0 input-file output-file" 20 exit $E_WRONG_ARGS 21 fi 22 23 24 exec 4<&0 25 exec < $1 # 將會從輸入檔案中讀取. 26 27 exec 7>&1 28 exec > $2 # 將寫到輸出檔案中. 29 # 假設輸出檔案是可寫的(添加檢查?). 30 31 # ----------------------------------------------- 32 cat - | tr a-z A-Z # 轉換為大寫. 33 # ^^^^^ # 從stdin中讀取.Reads from stdin. 34 # ^^^^^^^^^^ # 寫到stdout上. 35 # 然而, stdin和stdout都被重新導向了. 36 # ----------------------------------------------- 37 38 exec 1>&7 7>&- # 恢複 stout. 39 exec 0<&4 4<&- # 恢複 stdin. 40 41 # 恢複之後, 下邊這行代碼將會如期望的一樣列印到stdout上. 42 echo "File \"$1\" written to \"$2\" as uppercase conversion." 43 44 exit 0 |
I/O重新導向是一種避免可怕的子shell中不可存取變數問題的方法.
Example 16-4. 避免子shell
1 #!/bin/bash 2 # avoid-subshell.sh 3 # Matthew Walker提出的建議. 4 5 Lines=0 6 7 echo 8 9 cat myfile.txt | while read line; # (譯者注: 管道會產生子shell) 10 do { 11 echo $line 12 (( Lines++ )); # 增加這個變數的值 13 #+ 但是外部迴圈卻不能存取. 14 # 子shell問題. 15 } 16 done 17 18 echo "Number of lines read = $Lines" # 0 19 # 錯誤! 20 21 echo "------------------------" 22 23 24 exec 3<> myfile.txt 25 while read line <&3 26 do { 27 echo "$line" 28 (( Lines++ )); # 增加這個變數的值 29 #+ 現在外部迴圈就可以存取了. 30 # 沒有子shell, 現在就沒問題了. 31 } 32 done 33 exec 3>&- 34 35 echo "Number of lines read = $Lines" # 8 36 37 echo 38 39 exit 0 40 41 # 下邊這些行是指令碼的結果, 指令碼是不會走到這裡的. 42 43 $ cat myfile.txt 44 45 Line 1. 46 Line 2. 47 Line 3. 48 Line 4. 49 Line 5. 50 Line 6. 51 Line 7. 52 Line 8. |
|
注意事項:
[1] |
一個檔案描述符說白了就是檔案系統為了跟蹤這個開啟的檔案而分配給它的一個數字. 也可以的將其理解為檔案指標的一個簡單版本. 與C中的檔案控制代碼的概念相似. |
[2] |
使用檔案描述符5可能會引起問題. 當Bash使用exec建立一個子進程的時候, 子進程會繼承fd5(參見Chet Ramey的歸檔e-mail,SUBJECT: RE: File descriptor 5 is held open). 最好還是不要去招惹這個特定的fd. |