在 shell 中管理檔案描述符
linux shell 中可以使用 0 到 9 的 file descriptor 檔案描述符來控制程式的輸入與輸出,但一些使用並不好理解。 例如重導向的順序不好理解,要將 fd 存在變數時得用到 eval, 或要測試一個 fd 有無被佔用時,只能直接嘗試重導向檢查有無錯誤。
shell 中執行程式時,會讓程式繼承 shell 本身的標準輸入、輸出、錯誤輸出三個 fd。 如執行腳本時,輸出可能被導向檔案,輸入被導向另一支程式等。 另外,也可以在執行時手動指定導向的目標, fd 中 0 1 2 分別為標準輸入、輸出、錯誤輸出。
fd 重導向的順序
fd 的結構為一個陣列,不同的索引中的內容為指向不同輸出的指標。
shell 中使用 2>&1
來表示將原本輸出到 stdout 的導向 stderr,
在對陣列的操作上,代表將原本在陣列位置 1 的指標拷貝到 2,
與語法上的 2 1 順序看來是相反的。
故程式向 2 寫入時,寫入的目標和向 1 寫入是相同的。
所以如果要同時導向 stdout stderr 到一檔案時,
寫法是 find 1>find.log 2>&1
,會先將 1 指向檔案,
然後把 1 的檔案指標拷到 2,讓 2 也指向同一個檔案。
如果寫成 find 2>&1 1>find.log
,
就會變成 stderr 指到 stdout,但 stdout 指到檔案。
重導向的目標
重導向時,若是要導向到檔案,可以直接寫 1>filename
,
若是要重導向到描述符,則必須在數字前加上 &
如 2>&1
,防止與檔名混淆。
但重導向的來源 fd,則是不必加 &
的。
同時,在使用 >
導向時,若是導向的來源為 stdout 1,
則 1 可以省略不寫;使用 <
導向 stdin 時也是同樣可以省略 0。
可能是因為這是最常見的用法,
或者是用數字代表 fd 的寫法是後來才添加的功能。
exec 的用法
在 shell 中除了預設的 fd 0 1 2,還有 3 到 9 可以用, 可以使用 exec 將 fd 對應到某一檔案。
exec 2>error.log
# following command stderr will send to error.log
exec 可以將操作 shell 自身的 fd 對應表格, 前述在指令後重導向的作法,只對該指令有效, 而 exec 則是改變 shell 自身的對應表格, 對 exec 後執行的所有命令都有效。
若是要短暫重導向,可以預先將 stderr 備份到另一 fd,事後再回復。
最後的 &-
則是關閉該 fd 的意思。
exec 3>&2 # backup stderr to 3
exec 2>error.log
# command send stderr to error.log
exec 2>&3 # recover stderr
exec 3>&- # close 3
或者也可以將命令寫在子 shell 中,再在子 shell 中重導向, 子 shell 的重導向並不會影響父 shell 的狀態。
(
exec 2>error.log
# following command send stderr to error.log
)
# command still send stderr to stderr
又或者,既然已經開子 shell 了,那直接重導向子 shell 的輸出即可:
(
# command send stderr to error.log
) 2>error.log
將 fd 存成變數
shell 內建的 fd 有 0-9 供取用,是一種暫存器概念的設計, 基於 unix process 對 file descriptor 的抽像,幾乎全無包裝。 雖然暫存器是十分古老且見常的設計, 如 vi 也使用暫存器的概念,提供 26 個不同字母的暫存器作為剪貼簿使用, 但在寫程式時,我們還是比較喜歡以變數的方式儲存開啟的檔案,再多疊一層抽像。
stderr=2
echo warning >&$stderr
在一般命令使用重導向時,可以直接將 fd 的數字存到變數內,
並在 &
後面使用該變數。
但若是要在 >
前面使用變數時,卻會出現問題,
可能是 shell 的變數展開是在解析重導向的左值前進行的吧。
所以要開檔時不能這樣寫:
log=3
exec $log>file.log
只能直接把變數寫死,或者是用 shell 的 eval 命令, 可以在解析指令前多展開一次變數:
log_fd=3
log_path=my.log
eval "exec $log_fd>$log_path"
測試 fd 是否已開啟
把 fd 存成變數,最主要的目的是防止用到其它設定開到的 fd。
shell 沒有提供方法來判斷有哪些 fd,又指向哪些目標。
一個做法是從 linux kernel 提供的 /proc/self/fd
來找,
或者用 /proc/$$/fd
,或者 /dev/fd
。
但這不是 shell 的功能,所以如果到沒提供 /proc
的系統就不管用了。
另一個做法是直接寫入該 fd 看看,如果 fd 不存在,就會報錯。 但隨便寫垃圾到別人開的 fd 也不太對, 所以我的想法是嘗試寫入一個空字串。
fd_test=3
printf '' >&$fd_test
除了 printf,當然也有很多指令是不輸出東西的,都可以拿來用,
例如 echo -n
、 true
、 :
。
或者用一個讀取 0 byte 的程式:
head -c 0 <&$fd_test
之後就可以寫出一個函數,可以用來測試該 fd 存不存在,
因為如果 fd 不存在會報錯,所以可以再把 stderr 指到 /dev/null
。
fd_is_open() {
local fd=$1
( true 1>&$fd ) 2>/dev/null
}
fd_is_open 0
fd_is_open 1
fd_is_open 3
找出空的 fd
之後就可以更進一步,寫出一個函數來找空的 fd:
fd_first_free() {
local fd
if [ $# -gt 0 ]
then fd=$1
else fd=0
fi
while fd_is_open $fd
do fd=$((fd+1))
done
echo $fd
}
找空的 fd 並開檔:
file=my.log
fd=`fd_first_free`
eval "exec $fd>$file"
echo start executing >&$fd