💬

シェルスクリプト(bash)でいい感じにログ出力する

2023/02/05に公開

冒頭で色々とぐだぐだ述べてますので、そのあたりすっ飛ばして「シェルスクリプト(bash)でいい感じにログ出力する」の最終的なコードはこちらにあります。

始めに

シェルスクリプトでログを出力する際に日時とログタイプを付けて出力、リダイレクトする場合が多いと思います。
例えば下記のようなものです。

#!/bin/bash

# ログの出力先指定
readonly INFO_LOG_FILE='/home/user/work/logs/info.log'
readonly ERROR_LOG_FILE='/home/user/work/logs/error.log'

echo "`date "+%Y-%m-%d %H:%M:%S"` [INFO] テストログメッセージ" >> $INFO_LOG_FILE
echo "`date "+%Y-%m-%d %H:%M:%S"` [ERROR] テストログメッセージ" >> $ERROR_LOG_FILE

ですが、echoする度に日付(dateコマンド)とログタイプ(INFOやERRORなど)、リダイレクト先などを指定するのは面倒です。
そこで標準出力、標準エラー出力、ファイル出力を一括で制御できる方法はないか悩んでいました。

解決策

下記の手順で解決できるようです。

  1. execコマンド(bashの組み込みコマンド)でリダイレクト先を変更する
  2. プロセス置換によって1でリダイレクトされてきた情報を受け取る
  3. 受け取った内容をawkコマンドによって日付とログタイプの情報を付与して出力する

以降で詳しく解説いたします。

execコマンド(bashの組み込みコマンド)でリダイレクト先を変更する

execコマンド(bashの組み込みコマンド)は引数で指定したコマンドを子プロセスを作成せずに現在のシェルのプロセスのまま起動させるために使用します。
また、その他の活用方法としてリダイレクト先を変更することもできます。execコマンドを呼び出す際に下記のようにリダイレクトだけを指定すると、現在のシェルのプロセスのリダイレクト先が指定されたものに変更されます。

#!/bin/bash

# ログの出力先指定
readonly INFO_LOG_FILE='/home/user/work/logs/info.log'
readonly ERROR_LOG_FILE='/home/user/work/logs/error.log'

exec 1>> $INFO_LOG_FILE
exec 2>> $ERROR_LOG_FILE

echo 'テストログメッセージ'
echo 'テストログメッセージ' 1>&2

execコマンドでリダイレクト先を変更した以降の標準出力と標準エラー出力はすべてexecコマンドで指定したリダイレクト先に記録されます。
ちなみに最後の1>&2は標準出力に指定されているファイルディスクリプタ1を標準エラー出力のファイルディスクリプタ2にするために指定しています。

プロセス置換によって1でリダイレクトされてきた情報を受け取る

プロセス置換は<(コマンド)>(コマンド)の形式で記述でき、主に一時ファイルを作成する際に用いられます。<(コマンド)は入力用の一時ファイルの作成、>(コマンド)は出力用の一時ファイルの作成といったイメージです。
<(コマンド)>(コマンド)の具体的な活用例としては下記になります。

<(コマンド)の活用例

<(コマンド)の活用例として異なったディレクトリのlsコマンドの実行結果をdiffコマンドによって差分確認したい場合があげられます。
プロセス置換を用いずに差分確認を行う場合は下記になります。

$ ls data/root1/ > ls_resutl_1.txt
$ ls data/root2/ > ls_resutl_2.txt
$ diff ls_resutl_1.txt ls_resutl_2.txt
0a1,2
> foo
> bar
$ rm -f ls_resutl_1.txt ls_resutl_2.txt

しかし、ls_resutl_1.txtとls_resutl_2.txtはdiffコマンドのためだけに必要な一時ファイルで本来であれば作成したくはありません。そのような場合はプロセス置換を用いることで一時ファイルを作成せずに実行可能です。

$ diff <(ls data/root1/) <(ls data/root2/)
0a1,2
> foo
> bar

>(コマンド)の活用例

>(コマンド)の活用例としてコマンドの実行結果をteeコマンドで標準出力での確認とログファイルに残したい場合にログファイルには特定の出力結果だけを残したい場合があげられます。
lsした結果を標準出力で確認及び、拡張子が.shのファイルだけをログファイルに書きたい場合は通常だと下記のようになるかと思います。

$ ls /home/user/work | tee tmp.log
bar
baz.js
foo.sh
$ cat tmp.log | grep '.sh$' --line-buffered > output.log 
$ cat output.log 
foo.sh
$ rm -f tmp.log

プロセス置換を行うことでtmp.logを用意せずに拡張子が.shのファイルだけをログファイルに書くことが可能です。

$ ls /home/user/work | tee >( grep '.sh$' --line-buffered > output.log )
bar
baz.js
foo.sh
$ cat output.log 
foo.sh

今回のログ出力時には>(コマンド)を用いてログ出力前に出力内容を加工するようにします。

受け取った内容をawkコマンドによって日付とログタイプの情報を付与して出力する

awkコマンドはAWKというテキスト処理用に開発されたドメイン固有言語(DSL)を起動するためのコマンドです。ここではAWKの詳しい説明は割愛いたしますが、下記の形式で文字列を表示させることができます。

入力1 入力2 入力3 | awk '{ print $1, $2, $3, $0 }'

入力を受け取ってそれを表示させることができます。$0と指定すると入力内容のすべてを指定したことになります。
例として下記に示します。

$ echo foo bar baz | awk '{ print $1, $2, $3, $0 }'
foo bar baz foo bar baz

上記例ではただ出力させているだけでしたが、出力前に様々な処理を加えることができます。
今回のログ出力時に行う際に下記の関数を用います。

strftime関数

strftime関数は時刻を文字列に変換します。

$ echo 'test' | awk '{ print strftime("[%Y-%m-%d %H:%M:%S]"), $1 }'
[2023-02-04 11:10:26] test

system関数

system関数は外部コマンドを実行することができます。

$ echo 'test' | awk '{ system("pwd") }'
/home/user/work

system関数は今回はawkで標準出力を行った際にバッファさせないようにsystem("")として使用します。バッファを出力するための関数としてfflush関数が存在するのですが、下記の引用[1]のように古いawkに対しては使用できない場合があるためです。

The fflush() function provides explicit control over output buffering for individual files and pipes. However, its use is not portable to many older awk implementations. An alternative method to flush output buffers is to call system() with a null string as its argument:

解決策を踏まえた最終的なコード

上記で述べた解決策を踏まえた最終的なコードを以降記載します。

#!/bin/bash

# ログの出力先指定
readonly INFO_LOG_FILE='/home/user/work/logs/info.log'
readonly ERROR_LOG_FILE='/home/user/work/logs/error.log'

# ログ出力設定
exec 1> >(awk '{print strftime("%Y-%m-%d %H:%M:%S [INFO] ") $0; system("");}' >> $INFO_LOG_FILE)
exec 2> >(awk '{print strftime("%Y-%m-%d %H:%M:%S [ERROR] ") $0;  system("");}' >> $ERROR_LOG_FILE)

echo "テストログメッセージ"
echo "テストログメッセージ" 1>&2
脚注
  1. Free Software Foundation. “Gawk: Effective AWK Programming”. I/O Functions (The GNU Awk User’s Guide). 2023-03-27. https://www.gnu.org/software/gawk/manual/html_node/I_002fO-Functions.html, (参照 2023-02-05) . ↩︎

Discussion