Bash I/O 重新導向完全指南:從檔案描述符 0/1/2 到進階管線技巧
在 Unix/Linux 的世界裡有一句著名的設計哲學:「一切皆為檔案(Everything is a file)」。
無論是螢幕輸出、鍵盤輸入、硬碟上的文件、乃至網路 Socket 與硬體設備,在作業系統核心看來,全部都是可以讀寫的位元組串流(Byte Streams)。
而 I/O 重新導向(I/O Redirection),就是 Linux 開發者與維運人員手中最基礎卻也最強大的武器。掌握導向技術,你就能自如地控制資料的流向:把背景任務的報錯安靜丟棄、將長日誌雙向分流、或是將多道指令無縫串接。
一、核心原理:檔案描述符(File Descriptors, FD)
每當一個 Linux 行程(Process)被啟動時,核心會自動為它打開三個標準資料流,並賦予對應的整數代號——這就是檔案描述符(File Descriptor):
| 檔案描述符 | 名稱 | 預設來源/目的地 | 說明 |
|---|---|---|---|
0 |
stdin(標準輸入) |
鍵盤(Keyboard) | 程式讀取使用者輸入的入口 |
1 |
stdout(標準輸出) |
終端機螢幕(Terminal) | 程式正常執行時印出的結果 |
2 |
stderr(標準錯誤) |
終端機螢幕(Terminal) | 程式發生警告或崩潰時的報錯訊息 |
flowchart LR
subgraph Process ["Linux 行程 (Process)"]
direction TB
FD0["FD 0 (stdin)"]
FD1["FD 1 (stdout)"]
FD2["FD 2 (stderr)"]
end
InputSrc["鍵盤 / 檔案 (< file) / Here-Doc (<<EOF)"] --> FD0
FD1 -->|"標準輸出 (> / >>)"| StdoutFile["日誌檔案 (output.log)"]
FD1 -->|"終端螢幕 (預設)"| Screen["終端機螢幕 (TTY)"]
FD2 -->|"錯誤輸出 (2>)"| StderrFile["錯誤日誌 (error.log)"]
FD2 -->|"靜音丟棄 (2> /dev/null)"| BlackHole["/dev/null (黑洞設備)"]
FD2 -->|"合併串流 (2>&1)"| FD1
為什麼作業系統要將 stdout (1) 與 stderr (2) 分開?
想像一下,如果你執行了一道管線指令:
cat data.txt | grep "user" | sort
如果 grep 發生錯誤時也是透過標準輸出送出,錯誤字串「grep: memory error」就會直接被送進 sort 進行排序,污染整個下游數據!
透過將正常資料(FD 1)與診斷報錯(FD 2)徹底拆開,管線 | 預設只傳遞 FD 1,而讓 FD 2 獨立印在螢幕上提醒工程師,確保了 Unix 模組化組合的健壯性。
二、基礎導向操作符號
1. 輸出導向:> 與 >>
>(覆寫截斷 Truncate):如果目標檔案存在,直接清空原有內容並寫入;若不存在則建立新檔。>>(追加 Append):保留原有內容,在檔案最末尾繼續寫入。
echo "Hello World" > greetings.txt # 覆寫
echo "Another line" >> greetings.txt # 追加
[!TIP] 防手滑小技巧:在 Bash 設定
set -o noclobber,可以防止>意外覆寫既有檔案。如果確定要強制覆寫,可改用>|。
2. 輸入導向:<、<< 與 <<<
< file:將檔案內容作為標準輸入餵給指令。wc -l < /etc/passwd<< EOF(Here-Document):在腳本中內嵌多行文本,直到遇到終止標記(常用EOF):
(若終止符加上引號cat << EOF > config.yaml server: host: 127.0.0.1 port: 8080 EOF<< 'EOF',Bash 將不會解析字串內的$VAR變數)<<< "string"(Here-String):將單行字串直接餵進stdin,省去echo ... |的額外子行程開銷:tr 'a-z' 'A-Z' <<< "hello bash redirection" # 輸出: HELLO BASH REDIRECTION
三、精確掌控 FD 1 與 FD 2
在導向符號前加上檔案描述符數字,即可指定要轉向哪個資料流。若省略數字,預設等同於 1。
1. 分別分流標準輸出與錯誤
# 正常的結果存入 output.txt,錯誤的報錯存入 error.txt
./deploy-script.sh 1> output.txt 2> error.txt
# 等同於:
./deploy-script.sh > output.txt 2> error.txt
2. 安靜模式:將報錯丟入黑洞 /dev/null
/dev/null 是 Linux 的特殊虛擬設備,寫入它的任何資料都會被直接吞噬丟棄。
# 在根目錄全盤搜尋特定設定檔,隱藏所有 Permission denied 權限不足的雜訊
find / -name "nginx.conf" 2> /dev/null
# 檢查 Ruby Gem 是否存在,若找不到則安靜結束而不噴錯
gem spec zurb-foundation 2> /dev/null
四、終極陷阱:> file 2>&1 vs 2>&1 > file
當你想把標準輸出與錯誤輸出全部寫入同一個檔案時,這道面試必考題是所有新手最容易踩空的坑。
正確寫法:
command > all.log 2>&1
錯誤寫法:
command 2>&1 > all.log # 錯誤!錯誤訊息依然會印在螢幕上!
為什麼順序這麼致命?
flowchart TD
subgraph Correct ["> file 2>&1 (正確:由左至右評估)"]
direction TB
C1["初始狀態:FD 1 -> 終端機, FD 2 -> 終端機"]
C2["1. 讀到 > file:FD 1 重導向至 all.log"]
C3["2. 讀到 2>&1:FD 2 指向 FD 1 目前的目標 (即 all.log)"]
C4["最終結果:1 與 2 皆寫入 all.log ✅"]
C1 --> C2 --> C3 --> C4
end
subgraph Wrong ["2>&1 > file (錯誤順序)"]
direction TB
W1["初始狀態:FD 1 -> 終端機, FD 2 -> 終端機"]
W2["1. 讀到 2>&1:FD 2 指向 FD 1 目前的目標 (此時依然是終端機)"]
W3["2. 讀到 > file:FD 1 重導向至 all.log"]
W4["最終結果:FD 1 寫入檔案,但 FD 2 依然印在螢幕上 ❌"]
W1 --> W2 --> W3 --> W4
end
[!IMPORTANT] Bash 的簡寫語法(Bash 4.0+): 現代 Bash 提供了更優雅的簡寫,直接用
&>或&>>即可同時導向 FD 1 與 FD 2:command &> all.log # 覆寫 command &>> all.log # 追加
五、實戰避坑與高級技巧
1. sudo 與重導向的權限陷阱
初學者最常犯的經典錯誤:
sudo echo "nameserver 1.1.1.1" > /etc/resolv.conf
# ❌ 報錯: Permission denied
原因:Shell 是在執行 sudo 指令之前,就以當前登入使用者的普通權限去嘗試開啟 /etc/resolv.conf!
正確解法:使用 tee 搭配 sudo 接管管道輸入:
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf > /dev/null
2. 管線中的標準錯誤:|&
預設的管線 | 僅會將前者的 stdout 接到後者的 stdin。如果你想把 stderr 也一起送入管線(例如進行關鍵字過濾):
# 傳統寫法:
make 2>&1 | grep "error"
# 現代 Bash 簡寫:
make |& grep "error"
3. 進程替換(Process Substitution):<()
想要比較兩道指令的輸出差異,卻不想建立任何磁碟暫存檔?
diff -u <(curl -s https://api.site.com/v1/users | jq .) <(curl -s https://api.site.com/v2/users | jq .)
<(...) 會把指令的輸出偽裝成一個具名的 FIFO 管道檔案,非常強大。
4. exec 全域導向:一勞永逸記錄整個腳本
如果你在寫一個長達幾百行的自動化部屬腳本,不需要每一行都寫 >> deploy.log 2>&1。直接在腳本開頭使用 exec:
#!/usr/bin/env bash
# 從這一行開始,腳本的所有 stdout 與 stderr 全自動寫入日誌檔
exec > >(tee -a "/var/log/deploy.log") 2>&1
echo "這行會同時印在螢幕並寫入日誌"
ls -la /some/directory
六、Bash 重導向操作速查表
| 操作符 | 作用 | 範例 |
|---|---|---|
> |
覆寫標準輸出 (FD 1) 到檔案 | ls > files.txt |
>> |
追加標準輸出 (FD 1) 到檔案 | echo "log" >> app.log |
2> |
覆寫標準錯誤 (FD 2) 到檔案 | find / 2> err.log |
2>> |
追加標準錯誤 (FD 2) 到檔案 | command 2>> err.log |
2> /dev/null |
靜音隱藏錯誤訊息 | grep "text" file 2> /dev/null |
> f 2>&1 |
標準輸出與錯誤一同寫入檔案 | cmd > all.log 2>&1 |
&> 或 &>> |
現代簡寫:合併導向 | cmd &> all.log |
< |
標準輸入重導向(讀取檔案) | wc -w < doc.txt |
<< EOF |
Here-Doc(內嵌多行輸入) | cat << EOF > file |
<<< |
Here-String(單行字串輸入) | base64 -d <<< "U3RhcmNr" |
| ` | &` | 合併管道(stdout + stderr 傳給下一指令) |
<(cmd) |
進程替換(輸出模擬為檔案) | diff <(cmd1) <(cmd2) |
結語
I/O 重新導向是 Linux 命令列的骨骼與血液。從最簡單的 2> /dev/null 靜音過濾,到複雜的 exec 全域日誌分流,深刻理解檔案描述符的傳遞邏輯,將使你在撰寫 Shell 腳本與伺服器運維時事半功倍、得心應手。