Bashシェルスクリプトでログ出力をシンプルに実現する方法

2020-04-08Bash,Linux

はじめに

シェルスクリプトはお世辞にも読みやすいコードとは言えません。
なのでできる限りシンプルに、短く記述したいのですが、工夫をしないと ログ出力 処理がごちゃごちゃしてしまいます。

例えば、すべての出力、エラー出力をログとして保存しようとすると以下のような記述ができます。

#!/usr/bin/env bash

LOG_OUT=/tmp/stdout.log
LOG_ERR=/tmp/stderr.log

# 標準出力
echo foo1 >>$LOG_OUT 2>>$LOG_ERR
echo foo2 >>$LOG_OUT 2>>$LOG_ERR
echo foo3 >>$LOG_OUT 2>>$LOG_ERR
# 標準エラー出力
ls /foo >>$LOG_OUT 2>>$LOG_ERR
rm /foo >>$LOG_OUT 2>>$LOG_ERR

実行結果は以下のようになります。

$ ./test.sh

$ cat /tmp/stdout.log
foo1
foo2
foo3

$ cat /tmp/stderr.log
ls: cannot access '/foo': No such file or directory
rm: cannot remove '/foo': No such file or directory

スクリプトが小さな間は良いのですが、大きくなってきた場合、このような記述が乱立していると非常に見にくいです。

もっと見やすくするための記述方法があります。

検証環境

$ uname -moi
x86_64 MacBookPro11,4 Darwin

$ bash -version | head -n 1
GNU bash, バージョン 5.0.11(1)-release (x86_64-apple-darwin18.6.0)

1. exec コマンドを利用する

exec コマンドを利用することで、上記のようなログ出力処理をシンプルに記述できます。

先程のスクリプトを exec コマンドを使ったシンプルな記述に書き換えてみます。

#!/usr/bin/env bash

LOG_OUT=/tmp/stdout.log
LOG_ERR=/tmp/stderr.log

exec 1>>$LOG_OUT
exec 2>>$LOG_ERR

# 標準出力
echo foo1
echo foo2
echo foo3
# 標準エラー出力
ls /foo
rm /foo

実行結果は以下のようになります。( 先程の一緒ですね。

$ ./test.sh

$ cat /tmp/stdout.log
foo1
foo2
foo3

$ cat /tmp/stderr.log
ls: cannot access '/foo': No such file or directory
rm: cannot remove '/foo': No such file or directory

コンソールとログファイルの両方に出力したい

コンソールとログファイルの両方に出力したい場合はどうしたらよいでしょう?
特に echo などは意図的に出力しているメッセージであり、コンソールにも表示させたいところです。

tee コマンドを組み合わせることで実現できます。

#!/usr/bin/env bash

LOG_OUT=/tmp/stdout.log
LOG_ERR=/tmp/stderr.log

exec 1> >(tee -a $LOG_OUT)
exec 2>>$LOG_ERR

# 標準出力
echo foo1
echo foo2
echo foo3
# 標準エラー出力
ls /foo
rm /foo

実行してみます。

$ ./test.sh
foo1
foo2
foo3

$ cat /tmp/stdout.log
foo1
foo2
foo3

$ cat /tmp/stderr.log
ls: cannot access '/foo': No such file or directory
rm: cannot remove '/foo': No such file or directory

ログにタイムスタンプを付与したい

#!/usr/bin/env bash

LOG_OUT=/tmp/stdout.log
LOG_ERR=/tmp/stderr.log

exec 1> >(
  while read -r l; do echo "[$(date +"%Y-%m-%d %H:%M:%S")] $l"; done \
    | tee -a $LOG_OUT
)
exec 2>>$LOG_ERR

# 標準出力
echo foo1
echo foo2
echo foo3
# 標準エラー出力
ls /foo
rm /foo

実行結果は以下のようになります。

$ ./test.sh
[2020-01-06 09:17:19] foo1
[2020-01-06 09:17:19] foo2
[2020-01-06 09:17:19] foo3

$ cat /tmp/stdout.log
[2020-01-06 09:17:19] foo1
[2020-01-06 09:17:19] foo2
[2020-01-06 09:17:19] foo3

2. { ... } 構文を利用する

{ ... } 構文を利用してジョブをまとめ、標準出力と標準エラー出力を書き出す方法もあります。

#!/usr/bin/env bash

LOG_OUT=/tmp/stdout.log
LOG_ERR=/tmp/stderr.log

{
  # 標準出力
  echo foo1
  echo foo2
  echo foo3
  # 標準エラー出力
  ls /foo
  rm /foo
} \
  1> >(
    while read -r l; do echo "[$(date +"%Y-%m-%d %H:%M:%S")] $l"; done \
      | tee -a $LOG_OUT
  ) \
  2>>$LOG_ERR

実行結果は exec を利用したものと同じです。

ひとこと

今までは { ... } を使った方法を取ることが多かったのですが、インデントが必要なので、他にいい方法がないかと思っていたら、 exec の方法を知りました。
勉強になりました。

2020-04-08Bash,Linux