iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0

上一篇我們為 mytool 加上了 Flag,執行後會把結果印在螢幕上。但在 Shell 中,mytoolpsgrepgit 一樣,都能透過標準的輸入輸出互相串接。串接的方式主要有兩種:管道(Pipeline) 用來連接「程式與程式」,重導向(Redirection) 則用來連接「程式與檔案」。

重導向(Redirection):改變輸入輸出的目的地

在 Shell 中執行命令時,程式的標準輸入(stdin)預設連接到終端機(讀取鍵盤輸入),標準輸出(stdout)則預設顯示在螢幕上。重導向就是改變這條預設路徑。最常見的是將輸出寫入檔案或讀取檔案,也可以把資料導向特殊裝置(如丟棄輸出的 /dev/null)或另一個串流。

>(覆蓋寫入):把前面的輸出結果存入指定檔案。如果檔案本來有內容,會被完全清空覆蓋。

$ echo "Hello World" > output.txt

這會建立一個名為 output.txt 的檔案,內容是 Hello World

>>(附加寫入):把輸出結果加到指定檔案的最末端,不會動到原本的內容。

$ echo "New Line" >> output.txt

<(讀取輸入):改變輸入來源,讓程式從檔案讀取資料,而不是從鍵盤。

$ cat < output.txt

<<(Heredoc,多行輸入):輸入來源不是現成的檔案,而是接下來幾行文字,直到遇到指定的結束字串為止,這段文字就直接變成標準輸入。

$ cat <<EOF
第一行
第二行
EOF

EOF 是自訂的結束標記,換成 ENDDONE 或其他不會跟內容本身撞名的字串也行。cat 收到的輸入就是 EOF 之間那兩行,不用先把內容存成檔案再用 < 讀進來。實務上常拿來組多行內容直接餵給命令,像是把 SQL 交給 psql

$ psql -d mydb <<SQL
SELECT * FROM users WHERE active = true;
SQL

結束標記加不加引號,會影響內容裡的變數和命令代換要不要展開。<<EOF(不加引號)裡的 $變數`指令` 會被展開;<<'EOF'(加引號)則整段當純文字,不做任何展開:

$ name="Evan"
$ cat <<EOF
Hello $name
EOF
Hello Evan

$ cat <<'EOF'
Hello $name
EOF
Hello $name

<<-:允許用 Tab 縮排:一般 Heredoc 對每一行前面的空白很敏感,<<- 允許在每一行前面加 Tab(不是空白)縮排,方便寫在腳本裡本來就有縮排的區塊:

if true; then
    cat <<-EOF
	縮排後的內容
	EOF
fi

<<<(Herestring):只是想把一個字串丟給程式當標準輸入,不需要整段多行內容時用這個:

$ grep "John" <<< "$names"

相較於用 echo "$names" | grep "John" 透過管道傳遞,<<< 會由 Shell 直接把字串內容送進 grep 的標準輸入,寫法更乾淨。

管道(Pipeline):把上一個命令的輸出接成下一個命令的輸入

管道的符號是 |。它把前一個命令的輸出結果,直接變成下一個命令的輸入資料。

假設有個檔案 names.txt,裡面是一堆人名,你想找出包含 "John" 的名字,並依字母排序:

$ cat names.txt | grep "John" | sort
  1. cat names.txt:讀取並輸出檔案內容。
  2. |:把內容傳給下一關。
  3. grep "John":從收到的內容中,篩選出有 "John" 的那幾行。
  4. |:再把篩選後的結果傳給下一關。
  5. sort:把結果排序,顯示在螢幕上。

三個命令是三個獨立的 Process,Shell 把它們同時啟動,資料一邊產生一邊往下流,不用等前一個命令全部跑完。

命令印錯誤訊息走的則是另一條路徑,叫 stderr,不會被 | 接走。如果 catgrep 中途印錯誤訊息,那些字還是會直接顯示在畫面上,不會混進下一個命令收到的資料裡——這也是為什麼我們需要特別把「正常結果」(stdout)和「錯誤訊息」(stderr)分成兩條路徑:分流之後,| 才有辦法只接對那一條。

兩者混用

管道和重導向常常混在一起用,比如把處理好、排序完的 John 們,最後存進檔案:

$ cat names.txt | grep "John" | sort > j_names.txt

前面兩段 | 把資料一路傳到 sort,最後 > 把排序完的結果導進 j_names.txt,畫面上什麼都不會印。串接的順序就是資料流動的順序。

單獨處理 stderr

前面提到的 > 預設只會導出正常輸出(stdout),這是有意為之的設計:它能確保存進檔案的資料保持乾淨,不會混入半路的警告或錯誤;同時,錯誤訊息留在畫面上,讓操作者能即時察覺有異常發生。

例如在搜尋檔案時,常會因為部分目錄權限不足而產生錯誤訊息:

$ find / -name "*.log" > result.txt
find: /System/Volumes/Data/.Spotlight-V100: Permission denied
find: /Library/Application Support/com.apple.TCC: Permission denied
...

執行後,result.txt 裡只會保存找到的檔案路徑,不會被報錯文字污染;畫面上顯示的權限錯誤,則即時提醒我們哪些目錄沒有被搜到。

如果我們想進一步控制這些錯誤訊息——例如確定不需要看報錯,想把它們直接丟棄,或是想把正常結果與錯誤訊息一起存檔——就必須手動指定 stderr 的去向。

Shell 是用三個數字(檔案描述符,file descriptor)來代表資料串流的通道:

  • 0:stdin(標準輸入)
  • 1:stdout(標準輸出)
  • 2:stderr(標準錯誤)

平常用的 > 其實是 1> 的簡寫,代表只將編號 1 的 stdout 導向目標。如果想單獨操作 stderr,就把編號 2 寫出來。

單獨丟棄錯誤訊息:2>

如果確定不需要查看錯誤訊息,可以把 stderr 導向 /dev/null(系統的黑洞裝置,任何寫進去的資料都會直接被丟棄):

$ find / -name "*.log" > result.txt 2> /dev/null

這樣搜尋結果會存進 result.txt,不需要的報錯直接丟棄,畫面上不會印出任何干擾。

合併輸出:2>&1

想把 stdout 和 stderr 合併寫進同一個檔案時,可以使用 2>&1

$ mytool > out.log 2>&1

這裡有兩個關鍵的運作機制:

  1. 為什麼要加 &
    如果只寫 2>1,Shell 會以為你想把錯誤訊息寫進一個檔名叫做「1」的檔案。加上 &,Shell 才會知道後面的 1 指的是檔案描述符(通道 1),而不是檔名。
  2. 為什麼是先寫 > out.log 再寫 2>&1
    因為 Shell 是「由左至右」依序設定重導向的。在命令啟動前,預設 stdout(1) 和 stderr(2) 都連向畫面。

由左至右拆解 > out.log 2>&1 的設定過程:

  1. > out.log(即 1> out.log):先把 stdout(1) 改接到 out.log
  2. 2>&1:再把 stderr(2) 接到「stdout(1) 當前接的地方」,也就是 out.log

兩步完成後,兩個通道都連向 out.log,正常資料與錯誤訊息順利合併寫進同一個檔案。

如果順序反過來寫成 mytool 2>&1 > out.log

  1. 2>&1:先把 stderr(2) 接到「stdout(1) 當前接的地方」。但這時 stdout(1) 還指著畫面,所以 stderr(2) 也被接到畫面。
  2. > out.log:這時才把 stdout(1) 改接去 out.log

結果只有 stdout 寫進檔案,stderr 依然留在畫面上,兩者並沒有合併進同一個檔案。

在較新的 Bash 或 Zsh 中,如果想合併兩者,也可以直接使用簡寫 &>(例如 mytool &> out.log)。但在撰寫需要跨環境相容的 Shell 腳本時,> out.log 2>&1 依然是最通用的標準寫法。

設計可被管線串接的 CLI 輸出

理解了管道與 stderr 的分流機制後,回過頭來看我們自己正在開發的 mytool:當使用者把我們寫的工具接上管道時(例如 mytool list | wc -l 統計筆數,或是 mytool list --json | jq . 解析 JSON),管道預設只會接走 stdout 的內容。

如果我們在程式裡把進度提示(如 Fetching...)或完成訊息直接印在 stdout,這些非資料文字就會混入管道,導致後續命令處理錯誤:

# 錯誤設計:進度與狀態混在 stdout(導致 wc -l 算錯成 4 行)
$ mytool list | wc -l
4

# 正確設計:狀態走 stderr,只有資料留在 stdout
$ mytool list | wc -l
Fetching...   # 印在 stderr,畫面上看得見,但不會進入管道
2            # stdout 只留 2 行資料,計算結果正確

只要確保 stdout 只有純粹的資料,stderr 處理進度與日誌,mytool 就能無縫融入 Shell 的管道生態中。

符號對照

以下整理本篇介紹的常用符號、連接對象與具體作用:

機制 符號 連接對象 作用
重導向 > 命令 → 檔案 覆蓋寫入
重導向 >> 命令 → 檔案 附加寫入
重導向 < 檔案 → 命令 讀取輸入
重導向 << 多行文字 → 命令 Heredoc,多行輸入
重導向 <<- 多行文字 → 命令 Heredoc,允許 Tab 縮排
重導向 <<< 字串 → 命令 Herestring,單一字串輸入
重導向 2> 命令(stderr) → 檔案 單獨導出錯誤訊息
重導向 2>&1 命令(stderr) → 命令(stdout 現在接的地方) 合併 stdout 與 stderr
重導向 &> 命令(stdout + stderr) → 檔案 合併 stdout 與 stderr(簡寫)
管道 | 命令 → 命令 把輸出接成下一個命令的輸入

上一篇
設計 CLI Flag 的型別、縮寫與作用範圍
下一篇
Flag、環境變數與設定檔
系列文
30 天學會做一個 CLI:打造人類與 AI 都友善的現代 CLI 應用11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言