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 腳本與伺服器運維時事半功倍、得心應手。