【Shell 教學】Redirect 讓人暈頭轉向的「重導向」 (2/2)

有了 stdin、stdout 和左右箭頭的概念後,今天來挑戰比較難的 stderr 囉~

另外,要看上一篇請前往這邊(https://weikaiwei.com/linux/redirect-1)
去了解基本的 redirection 指令再來看這一篇才會比較好理解唷~

在進入今天的主題之前,先複習一下上次提及的內容:

Recap of basic redirections

  1. Terminal 的所有資料流分成三種:stdin 吃使用者的輸入、stdout 把一般的輸出顯示出來、stderr 則是把錯誤訊息顯示出來
  2. > 是把程式的結果輸出存到某個檔案, < 則是把某個檔案當成程式的輸入
    有個記法:因為我們檔案名都是接在指令(command)的後面,所以有個記法是:
做了一點事情的指令 > 輸出檔名  # 可以想像成結果流向右邊檔名
做了一點事情的指令 < 輸入檔名  # 可以想像成把檔案推進左邊的程式

接下來進入今天的重頭戲——與 stderr 相關的各種 redirection!

stderr ? 為什麼需要這個東西?

現在我們如果有這樣的 Python 程式碼:

# --- filename: mypythoncode.py
print('1' + '2')  # "12"
print('1' + 2)    # TypeError

如果我們執行

python mypythoncode.py

出現的結果會是

12
Traceback (most recent call last):
  File "mypythoncode.py", line 3, in <module>
    print('1' + 2)    # TypeError
TypeError: can only concatenate str (not "int") to str

第一行沒有問題,很正常的顯示了 12 字串。
但第二行就不太妙了,這邊有個 bug,導致了 TypeError 錯誤,因此我們可以看到 Python 透過 terminal 把這個錯誤也一併的印在了螢幕上。

使用者在使用這類程式的時候,通常會認為「bug 是你們工程師的事情,我可不希望我的程式因為這樣掛掉或亂掉啊!OK 的顯示,不 OK 就閉嘴吧?(把訊息傳到工程師那邊去就好)」;當然另一個應用場景可能是在做資料處理時,會不希望把錯誤訊息混合到輸出資料裡破壞輸出的格式。因此 Linux 系統(和 Mac 等「類 Unix 系統」)的設計會將兩者分開,分別稱作 stdout 和 stderr。

回來看語法的部分

還記得 stdin、stdout 和 stderr 三者的編號嗎?

名稱中文名編號流向功能
stdin標準輸入0程式 ← 你接收使用者的輸入
stdout標準輸出1程式 → 你將處理結果顯示給使用者
stderr標準錯誤2程式 → 你將遇到的錯誤訊息顯示給使用者
資料流表格

注意到那個「流向」了嗎?其實標準輸出和標準錯誤都是輸出給使用者的,因此語法上也有一些共同之處。請嘗試以下的程式碼:

python mypythoncode.py > output.txt
                     # ^^^^^^^^^^^^ 沒什麼,就上次的 `>` 而已

按照上次的內容,我們應該會以為 terminal 不會吐任何東西給我們,而是把結果存在 output.txt 裡面對吧?⋯⋯

結果居然不是!!!

Traceback (most recent call last):
  File "mypythoncode.py", line 3, in <module>
    print('1' + 2)    # TypeError
TypeError: can only concatenate str (not "int") to str

與此同時 output.txt 只有

12

可以發現,雖然在直接執行時都是顯示在終端機上,但其實輸出已經被電腦區分成結果錯誤兩個部分了!而使用 > 只會把結果存下來,因此剩下的就只有錯誤顯示在螢幕上~

此時前面所說的「編號」就派上用場了!

請各位把 terminal 的 code 改成

python mypythoncode.py 1> output.txt
                     # ^ 就這邊加個 1 而已,
                     #   請注意 `1` 跟 `>` 中間不能有空白!

再跑一次,可以發現結果一模一樣⋯⋯

然後再試試看下面的 code

python mypythoncode.py 2> output.txt
                     # ^ 就這邊加個 2 而已,
                     #   請注意 `2` 跟 `>` 一樣中間不能有空白!

奇妙的事情發生了,這次反而錯誤訊息不見了!反倒是 12 被顯示在螢幕上,
然後開啟 output.txt ⋯⋯

Traceback (most recent call last):
  File "mypythoncode.py", line 3, in <module>
    print('1' + 2)    # TypeError
TypeError: can only concatenate str (not "int") to str

顯示的跟存下來的反過來了!

從這邊各位可以發現,其實 > 的前面是可以加上編號的
1> 表示「把1號輸出存到⋯⋯檔案」,也就是把 stdout 存下來
2> 則表示「把2號輸出存到⋯⋯檔案」,也就是改把 stderr 存下來

而在不寫號碼的時候,預設是存 stdout 的(畢竟結果需要被存下來的情況比較普遍,錯誤訊息通常是給人「看」的)

更甚者,可以試試看

python mypythoncode.py > output.txt 2> error_message.txt
                    # 1 在這邊被省略了
                                  # ^^^^^^^^^^^^^^^^^^^^ 錯誤也存下來

這樣可以發現

output.txt

12

error_message.txt

Traceback (most recent call last):
  File "mypythoncode.py", line 3, in <module>
    print('1' + 2)    # TypeError
TypeError: can only concatenate str (not "int") to str

分別把輸出跟錯誤都按照需求存下來了。

同理,在此補充一下 < 其實前面也可以接號碼,也就是輸入的對象「0 (stdin)」

所以以上次的例子來說

python age_lookup.py < inputfile.txt

也可以寫成

python age_lookup.py 0< inputfile.txt
                   # ^^ 這代表傳到 stdin (0)

>&」 與很常用的 「2>&1

有了 1>2> 的機制後,讓我們回到這個問題的上一步去想:

那究竟是什麼樣的訊息會被送到 stderr 呢?
我們有權利自己決定嗎?

要做到這點,其實只要我們能決定合併和交換兩道訊息流就行了,請嘗試以下的 code

# 印出 123
echo 123
# 確認究竟是誰在顯示
# 很抱歉,基於 shell 本身語法因素
#     需要先把整串程式碼用 `{}` 包起來
#     然後由於太長了,這邊先多用個 `\`
#     來讓指令可以繼續在下一行寫,請一併複製
{ echo 123;     } \
    > stdout_output_line__8.txt \
   2> stderr_output_line__8.txt

# 關鍵語法!請注意這行
echo 123 >&2
#        ^^^ 這裡!
# 嘗試導引到檔案,確認究竟是誰在顯示
{ echo 123 >&2; } \
    > stdout_output_line_16.txt \
   2> stderr_output_line_16.txt

# 看看跟上面那行有無差異
{ echo 123 1>&2; } \
    > stdout_output_line_21.txt \
   2> stderr_output_line_21.txt
#        ^^^^ 實際上,`>` 前面沒寫預設就是 1 (stdout)
# 1、2交換,換成這樣呢?
{ echo 123 2>&1; } \
    > stdout_output_line_26.txt \
   2> stderr_output_line_26.txt
# 多試試這行
{ python mypythoncode.py 2>&1; } \
    > stdout_output_line_30.txt \
   2> stderr_output_line_30.txt

大家可以觀察看看輸出檔案分別有什麼,來理解下面的內容。

A>&B」=「把A的資料輸出到B」= 「A output as B」

前面已經說過,「A>」會讓 A 的資料輸出到後面的檔名,其中 A 可以是 1 或 2。
在這邊加上後面的 &B ,就是指「當成 B 去輸出」的意思,其中 B 也可以是 1 或 2。

因此,1>&2 就是把原本輸出到 stdout 的內容輸出到 stderr
此時 stderr 的還是在 stderr,因此「兩邊的訊息」都被合併到 stderr 了!
也可以簡寫成 >&2 ,因此要讓 shell 的 echo 指令不再如預設的輸出到 stdout 而是改成 stderr 只要在前面或後面加上 >&2 就可以達成這樣的效果。

反過來,2>&1 也是很常用的語法,就是把 stderr 的東西合併到 stdout 裡去,也就是不區分正常結果和錯誤訊息全部變成輸出(然後顯示在螢幕或存成檔案,抑或是接到「管線結構(pipe)」去,但關於 pipe 的用法留待日後詳談)
但這邊就無法簡寫了,可以簡寫的只有用 > 代替 1>2>& 會和「⋯⋯&」的「在背景執行」混淆,因此不能省略!

這邊做個小提醒當作補充,> 的語法會「新創一個以後面文字為檔名的檔案」再儲存輸出

因此,如果該檔名已經存在

將會刪除覆蓋掉!

例如

echo 123 > file1.txt
echo 456 > file1.txt  # 原本的 file1.txt 的東西被蓋掉了!

因此 file1.txt 將只剩下

456

那如果希望把 456 合併在後面而不是直接取代掉呢?請使用 >>

echo 123 > file1.txt  # 當然你要這邊就改用 >> 也可以,如果 file1.txt 不存在
echo 456 >> file1.txt
#        ^^ 456 會因此接在後面

&> 檔名」= 「> 檔名 2>&1

最後提一個簡寫的方法,如標題所示,
&> 就是把 stdout 和 stderr 當成同一個輸出資料流輸出的意思

例如

python mypythoncode.py &> merged_output.txt

等同於

python mypythoncode.py > merged_output.txt 2>&1

終於結束了!~~~~嗎?

基本上和 redirect 最直接相關的用法就到此為止了,但實際用起來還需要和其他 shell 的語法相互配合,總之各位今天開始,可以嘗試使用 ><2>2>&1 等來幫助寫程式使用 Linux 時的各種檔案處理囉~

給想要學更多的人——

實際上由於不希望一次塞太多東西給各位,因此剩下和 redirect 一起使用的其他技巧就留待日後再講解了~其中最有趣的當屬 pipe (|)的用法。
劇透一下:我們今天每一行 command 都有輸入和輸出,那有沒有可能把某個程式碼的輸出直接接到下一個程式碼的輸入來完成一連串的工作呢?是可以的!也就是上面所說的 pipe 的功能

另外還有 HEREDOC (<<)、各種 operators (&&||)、return value ($?)等等各位敬請期待~

寫在後面⋯⋯的挑戰題!

這邊不算是本文內容,但可以提供各位去思考、嘗試和查詢看看結果會是如何⋯⋯

  1. python mypythoncode.py 2>&1
  2. python mypythoncode.py 2 >&1
  3. python mypythoncode.py 2> &1
  4. python mypythoncode.py 2>& 1
  5. python mypythoncode.py 2 > &1
  6. python mypythoncode.py 2> & 1
  7. python mypythoncode.py 2 >& 1
  8. python mypythoncode.py 2 > & 1
  9. python mypythoncode.py 2&>1
  10. echo 123 >&2 > stdout_output_line_10.txt 2> stderr_output_line_10.txt
  11. echo 123 1>&2 > stdout_output_line_13.txt 2> stderr_output_line_13.txt
  12. echo 123 2>&1 > stdout_output_line_16.txt 2> stderr_output_line_16.txt
  13. python mypythoncode.py 2>&1 > stdout_output_line_18.txt 2> stderr_output_line_18.txt

有興趣的話,還有一些參考資料,各位加油~:

3 Comments

  1. Hi ,你好
    謝謝版主分享,內容口語化且不會使用容易嚇到初學者的專有名詞
    目前也有在偶而寫script,但還是有需多不懂的用法
    如多個重導向應用的執行順序,當一行指令出現多個< > 時可能就會開始無法看懂執行順序
    另外請問可以分享here document跟here string的用法嗎?

    1. 哈囉您好,謝謝您的回饋~
      本文的目標就在於盡量使用淺白的語言解釋入門的概念,避免初學者看到一堆專有名詞和 code 就被嚇到望而卻步,因此能夠得到這樣的回應筆者覺得很有成就感~

      針對您的提問,這是個有趣的問題
      如果出現多個 <> 時,以我目前實測,
      原則上「輸入」會依序被讀進程式
      而「輸出」則是會把一樣的結果都寫進各檔案

      例如
      ./myprogram < in1.txt < in2.txt > out1.txt > out2.txt
      執行的效果就等效於把 in1.txt 跟 in2.txt 的東西依序拼在一起,送給 ./myprogram,然後輸出的東西既印到 out1.txt 也會印到 out2.txt(而且會是一樣的)
      有意思的是這類指令換順序也無所謂,也就是
      > out1.txt ./myprogram < in1.txt > out2.txt < in2.txt
      結果也會相同,都是
      「傳入:in1.txt、in2.txt」
      「執行:./myprogram」
      「傳出:out1.txt、out2.txt」

      最後,感謝您提議 here document 和 here string
      這也是 shell programming 會提及的主題之一
      (而且確實頗多初學者不知道在做什麼)
      有機會會再出一篇說明的~

留言討論區