BASHスクリプトにおけるメッセージング
目的
シェルスクリプトと言えども、メッセージング(コンソール出力・ログ出力)は割と重要なので、比較的どんな場合でも使えると思われる方針についてまとめてく。
どうしておきたいか?
- コンソールには最低限のコマンドを実行したユーザーに意味のあるメッセージのみ伝える。
- ただし特定のタグ等でメッセージのフィルタさせることができること。
- 標準出力は捨てて、標準エラーのみ確認したいケースのみ対応できること。
- ログには基本的に全ての内容を記録する。
- ただし特定のタグ等で出力にフィルタさせることができること。
出力内容のフィルタ
基本的にはgrepでフィルタする。
echo "INFO: any message" | grep "INFO:"
(結果)
INFO: any message
標準エラー
エラーについて標準エラーに出力する。
echo "ERROR: any error statement." >&2 | grep "INFO:"
(結果)
ERROR: any error statement.
尚、標準エラーは(標準出力にリダイレクトしない限り)|渡しの影響を受けないので、必ずユーザに通知させることができる。
メッセージングのログ出力
基本的にはリダイレクトを使う。
メッセージ出力と同時に行う場合はtee -aを用いる。
echo "INFO: any message" |tee -a output.log | grep "INFO:"
(結果)コンソールおよびログに下記のように出力される。
INFO: any message
ちなみにこのパターンでは、コンソール出力しないもののログ出力することも可能。
echo "ERROR: any message" |tee -a output.log | grep "INFO:"
(結果)ログのみに以下のように出力される。
ERROR: any message
このパターンによりデバッグやトレース的なメッセージを埋め込むことが可能。
コマンド・処理のログ出力
基本的にはリダイレクトを使う。
メッセージ出力と同時に行う場合はtee -aを用いる。
ls -la | tee -a output.log | grep "INFO:"
(結果)
※上記例は条件に合致しないので、ls -laの結果がログのみに出力される。
補足:パイプ処理した場合のコマンドの戻り値について
コマンドの結果を|渡しした場合の注意点として、$?によるコマンドの戻り値が取得できなくなる点がある。
この場合は${PIPESTATUS[n]}を使うことで、各コマンドの戻り値を検査できる。
例えば
ls -la not-found |tee -a output.log | grep "INFO:"
の場合
echo ${PIPESTATUS[@]}
2 0 1
順番に「ls -la not-found」の戻り値、「tee -a output.log」の戻り値、「grep "INFO:"」の戻り値となる。
尚、PIPESTATUS環境変数は生存期間が、|を使ったコマンド呼び出しの直後のみであるため、|渡しした各コマンドの戻り値が必要な場合は、工夫が必要。
調べたら以下のような感じでコピーしておける模様。
declare -a REMAIN=(${PIPESTATUS[@]})
コマンド・処理の標準エラーのコンソール出力・ログ出力の扱い方
コマンド処理が中断されるような状況では、標準エラー出力されても良いかもしれないが、検査処理等でエラー前提の場合まで標準出力をそのまま出すのは望ましくない。
一方で、検査結果がエラーであった旨はログに残したい。
以下のようにかなりややこしい方法で、メッセージング&ログ出力する必要がある模様。
{ { ls *.txt | tee output.log >&3; } 2>&1 | tee error.log 1>&2;} 3>&1
順番に見てくと
(1) コマンドの標準出力結果をログにのみ出力
ls *.txt | tee output.log >&3;
の部分で、まず「ls *.txt」コマンドの標準出力をログに出力しつつ&3にリダイレクト
(この時点では、画面には出力されていない)。
(2) コマンドの標準エラーをログ出力しつつ、標準エラーに出力。
{ ~ } 2>&1 | tee error.log 1>&2;
コマンド結果をteeで拾うために、標準エラーを標準出力にリダイレクト。
そのままログ出力・標準エラー出力。
(3) コマンドの標準出力を標準出力
{ ~ } 3>&1
上記の(1)で&3にリダイレクトした内容を、再び標準出力にリダイレクト。
こうすることで、標準出力と標準エラーを分けたままファイル出力が可能。
個人的にはファイル出力は分ける必要がなく、teeでファイル出力を行いつつ、標準出力/標準エラーと出したいため、この方法が最適であった。
スクリプトのメッセージング・構造に関する方針
1つ1つのステートメントで記述する方法
処理を上から順番に処理する際、都度メッセージング処理を記述する。
{ { ls *.txt | tee -a output.log >&3; } 2>&1 | tee -a output.log 1>&2;} 3>&1
細かい制御ができる反面、非常に冗長なスクリプトになってしまい保守性が下がる可能性がある。
処理全体を関数化し、関数の戻りに対して、上記のメッセージング・ログ出力の処理を記述する方法
処理は全てmain関数で処理する。
# $*
function main() {
ls *.txt
}
{ { main $* | tee -a output.log >&3; } 2>&1 | tee -a output.log 1>&2;} 3>&1
スクリプトでのメッセージング設計・実装に関して
以下に示す考え方は、必ずしも全ての状況・スクリプトについて「完全な正解」というものではなく、目的に応じて使い分けていくことが必要と思われる。
内部で呼び出してるコマンドの出力について
- 考え方(1) 標準出力も標準エラーも全てログ出力のみとする。
スクリプトの内部で呼び出している処理は全てリダイレクトして、ログ出力のみ行う。
コマンドを実行した旨および戻り値をメッセージングする。
echo "ls command started."
ls -la >> stdout.log 2>&1
RTN=$?
echo "ls command end (exit by $RTN)."
if [ ${RTN} -ne 0 ]; then
echo "any error occured.";
...
fiu;
場合によっては、戻り値に応じた、エラーハンドリングを行う。
- 考え方(2) 繰り返し処理される処理はループハンドリングして、途中経過を示すメッセージングを行う。
この方法では、コマンドの出力を直接ユーザーに示せないので、大量のファイルコピー(cp)・同期(rsync)など、時間がかかる処理の場合にはコマンドが実行されているのかどうか?分かりにくい、という側面で若干不親切である。
下記のように途中経過を示す出力を行う。
I=1
find ~ -type f 2>/dev/null | while read OUTPUT
do
let I=I+1;
if [ `expr ${I} % 10` -eq 0 ]; then
printf "Now in progress... (${I} of all)\r"
fi;
sleep 1;
done;
サンプルコードなので、処理自体には全く意味がないのと、処理件数も適切ではないが、上記のように繰り返し処理において、標準出力内容を一定件数ごとにハンドリングして、スクリプトが動いてることをユーザーに知らせる。
最終更新:2015年03月01日 13:31