はじめに
シェルスクリプトはお世辞にも読みやすいコードとは言えません。
なのでできる限りシンプルに、短く記述したいのですが、工夫をしないと ログ出力 処理がごちゃごちゃしてしまいます。
例えば、すべての出力、エラー出力をログとして保存しようとすると以下のような記述ができます。
1
2
3
4
5
6
7
8
9
10
11
12
| #!/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
|
実行結果は以下のようになります。
1
2
3
4
5
6
7
8
9
10
| $ ./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
|
スクリプトが小さな間は良いのですが、大きくなってきた場合、このような記述が乱立していると非常に見にくいです。
もっと見やすくするための記述方法があります。
検証環境
1
2
3
4
5
| $ 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 コマンドを使ったシンプルな記述に書き換えてみます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #!/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
|
実行結果は以下のようになります。( 先程の一緒ですね。 )
1
2
3
4
5
6
7
8
9
10
| $ ./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 コマンドを組み合わせることで実現できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| #!/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
|
実行してみます。
1
2
3
4
5
6
7
8
9
10
11
12
13
| $ ./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
|
ログにタイムスタンプを付与したい
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #!/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
|
実行結果は以下のようになります。
1
2
3
4
5
6
7
8
9
| $ ./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. { ... } 構文を利用する
{ ... } 構文を利用してジョブをまとめ、標準出力と標準エラー出力を書き出す方法もあります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| #!/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 の方法を知りました。
勉強になりました。