前言
透過標準輸出入,電腦程式可以和外部環境傳遞資料。標準輸出入將輸出入的功能抽象化,電腦程式可在不知道輸出入實際的來源和去處即可傳遞資料。
標準輸出入分為標準輸入、標準輸出、標準錯誤三種。這是電腦系統所提供的功能,而非 shell script 獨有的特性。本文介紹 shell script 處理標準輸出入的方式。
標準輸出 (Standard Output)
標準輸出由程式傳出,通常會傳向螢幕,也可能導向其他裝置。標準輸出用來傳輸一般的文字資料。錯誤訊息則不建議使用標準輸出來傳遞。
以下 shell script 使用 echo(1)
指令將字串 "Hello World"
傳到標準輸出:
greet="Hello";
target="World";
echo "$greet $target";
echo
僅適合傳輸簡單的文字資料。若需要對文字資料做細微的調整,則可改用 printf(1)
,如下例:
greet="Hello";
target="World";
printf "%s %s\n" greet target;
對於跨行的文字資料則可改用 cat(1)
傳遞:
#--------1---------2---------3---------4---------5---------6---------7---------8
#2345678901234567890123456789012345678901234567890123456789012345678901234567890
cat << EOF
Unix (trademarked as UNIX) is a family of multitasking, multiuser computer
operating systems that derive from the original AT&T Unix, development starting
in the 1970s at the Bell Labs research center by Ken Thompson, Dennis Ritchie,
and others
EOF
EOF
不是 shell 指令,而是程式設計者自訂的識別字,用來界定多行文字資料結束的位置。
標準錯誤 (Standard Error)
標準錯誤由程式傳出。如同標準輸出,標準錯誤常會傳到螢幕,或是導向其他裝置。標準錯誤僅用來傳遞錯誤訊息,不建議傳遞一般的文字資料。
Shell 沒有為標準錯誤設計額外的指令,而是藉由指定輸出代號的方式將文字資料導向標準錯誤。以下的 echo
指令將錯誤訊息傳向標準錯誤:
echo "Some error message" >&2
一般來說,標準輸出的代號為 1
,標準錯誤的代號為 2
。傳出文字訊息時,預設會傳到標準輸出。要將輸出改到標準錯誤,要寫額外的程式碼,像是上述程式碼片段的做法。
標準輸入 (Standard Input)
標準輸入則是由外部程式傳入的文字資料。對於電腦程式來說,不需要知道標準輸入的實際來源,只要留好開口即可。
以下 shell script 會偵測命令列參數是否存在,若命令列參數為空,則改從標準輸入取得文字資料:
#!/bin/sh
if [ -z "$@" ];
then
tr '[:lower:]' '[:upper:]' < /dev/stdin;
else
tr '[:lower:]' '[:upper:]' $@;
fi
exit 0;
tr(1)
指令會將字元進行一對一轉換,本範例程式是將小寫字母轉大寫字母。
重導 (Redirecting)
在預設情境下,標準輸入的來源是鍵盤,標準輸出和標準錯誤的標的是螢幕。但系統使用者可隨需求更改。
nohup(1)
用於執行長時間的指令。該指令在使用者離線後仍會繼續運行,常用於執行遠端指令。以下指令用 make
編譯程式:
$ nohup make </dev/null 1>build.log 2>build.err &
&
表示在背景執行指令。指令丟到背景後,會將主動權選給使用者。由於不需要標準輸入,故從 /dev/null 導入即可。標準輸出和標準錯誤則導向兩個不同的檔案,便於後續觀察。
管線 (Pipe)
管線的功能是將前一個程式的輸出重導為下一個程式的輸入。由於管線可在同一行內重覆使用多次,使用管線可以串連出緊湊且複雜的指令。
以下是編譯程式時常用的組合指令:
$ make | tee build.log
make(1)
需搭配 Makefile (或 GNUmakefile) 使用,通常用來編譯 C 或 C++ 應用程式。由於編譯程式的訊息很長,往往會超出螢幕捲動的上限,所以將 make
指令所得的訊息用管線重導緥 tee
。然後 tee
將這些訊息另存在 build.log 文字檔案中。這時系統使用者仍可以同時觀看編譯的訊息。
以下 shell script 碼片段用來檢查特定應用程式的 PID (process id):
if [ -n "$1" ];
then
ps aux | grep "$1" | grep -v grep | tr -s ' ' | cut -d' ' -f2;
fi
用管線組合的指令都比較複雜。一步步拆開來看就會理解了:
ps aux
以 BSD 風格輸出目前所有的行程grep "$1"
從前一個指令過濾出目標指令grep -v grep
用來去掉grep
本身tr -s ' '
將多個空白壓縮成一個空白cut -d' ' -f2
將輸出以空白切開後取第二個欄位
如果還是無法理解的話,建議實際上機操作,逐一輸入指令,就可以理解了。
以下 shell script 碼片段用來計算重覆輸入的文字的數量:
if [ -n "$@" ];
then
grep -oP "\w+" "$@" | tr '[:upper:]' '[:lower:]' | sort | uniq -c | sort -nr
fi
這裡也用到管線。同樣將其拆開來看:
grep -oP "\w+" "$@"
抓出所有輸入的文字tr '[:upper:]' '[:lower:]'
將文字正規化 (大寫轉小寫)sort
將文字排序uniq -c
取出不重覆文字並對其計數sort -nr
以數量排序,由小排到大
Unix 命令列工具的哲學
由本文可知,Unix 命令列工具除了單獨使用外,還可以組合出複雜的功能。在這樣的思維下,Unix 命令列工具的設計方式如下:
- 每個工具專注在單一任務上
- 僅輸出最少量的可用文字訊息
- 以「行」為單位來輸出文字訊息
- 不寫死輸出入,讓使用者自由選擇
- 不限制互動模式或批次模式
- 儘量不重覆實作已有的命令列工具
當然,還是有一些例外。像是 awk(1)
和 perl(1)
就是兼具程式語言的命令列工具。這些工具可以應付多種情境,而不限於單一任務。