シェルスクリプト ログ出力関数あれこれ

スクリプトの実行ログ関係

シェルスクリプトを実行した際、デバッグするなら bash -x xxxx.sh という感じで、デバッグ実行することが多い。
でも、一部の管理職には不評なんだよね。
実行ログで出してくれと。

そんな理由で、スクリプトログを出力する関数を使うことが多いため、自分で使いやすい関数を汲んでおきたくなった。
毎回、現場に合わせて作るのも結構辛くなってきたので。

仕様

・指定のディレクトリにスクリプトログを置く。
・ログファイル名は、スクリプト名の拡張子を .log に置き換え、日付を含めたもの。
・ログは、yyyy/mm/dd hh:mm:ss のあとにメッセージを入れる。
・出力先は、変数指定がなければ自動で決める。


この仕様から、細かい条件を各環境に合わせれば良い。


自分の趣味を色濃く反映するものとして、
・関数名は fnLogger とする。
・出力先変数は pLogFile とする。

関数は英語で function と言うので、関数定義には fn を付けている。
変数なら、特に例外規定がなければ Parameter とか Variable なので p を。


fXxxx だと、フラグ関係に。
nXxxx だと、数値関係に。


それから、使い慣れているキャメルケースで変数名を定義する。
(スネークケースは好きではないが、使わないとは言っていない)


他のスクリプト等で変数が重複する確率が低いので、特に規定がない場合はこれを使っていたりする。
word とかの MS-Office 製品で、説明資料を作るときに下手に _ が入っていると、波線で見えなくなることがあるんだよね。
いや、全部大文字にしていれば問題ないんだけど、スネークケースを使う人って、なぜか小文字にしたがる……。


ということで、シェルスクリプトの変数は、会社で使うなら全部大文字が良いです。

スクリプト(関数)

基本形となる関数が、こちら。

fnLogger() {
    pLogFile="${pLogFile:=$(/usr/bin/basename ${0} .sh)_$(date +'%Y%m%d').log}"
    echo "$(date +'%Y/%m/%d %H:%M:%S') $*" >> ${pLogFile}
}

上記 pLogFile という変数を、関数外で定義しておけば、ログ出力先はその指定に従う。



フラグ次第で、出力先を変えるタイプ。

### Parameter and Variable

## Directory
pLogDir=/var/log/script

## File
pLogFile=$(/usr/bin/basename ${0} .sh)_$(date +'%Y%m%d').log

## Path
pLogPath=${pLogDir}/${pLogFile}

## Flag
# 0 = Script log
# 1 = /var/log/messages
fOutput=0

### Function
fnLogger() {
    if [ "${fOutput}" -eq "0" ]; then
        echo "$(date +'%Y/%m/%d %H:%M:%S') $*" >> ${pLogPath}
    else
        logger "$*"
    fi
}


作りこもうとすると、ログを出力したいだけでもこんなに長くなってしまうんだよね……
まぁ、他のファイルで定義しておいて、それを . で読み込むことが多くなってきたから気にならないんだけど。


いくつか出力先を選びたいなら、こんな手法も。

### Parameter and Variable

## Parameter
pID=$(/usr/bin/basename ${0} .sh)
pYYYYMMDD=$(date +'%Y%m%d')

## Directory
pLogDir=/var/log/script

## File
pLogStdFile=${pID}_${pYYYYMMDD}.log
pLogErrFile=${pID}_${pYYYYMMDD}_Err.log

## Path
pLogStdPath=${pLogDir}/${pLogStdFile}
pLogErrPath=${pLogDir}/${pLogErrFile}

## Flag
# 0 = Script log - Standard
# 1 = Script log - Error
# 2 = /var/log/messages
fOutput=0

### Function
fnLogger() {
    p1st=$1
    shift

    case ${p1st} in
        0 ) echo "$(date +'%Y/%m/%d %H:%M:%S') $*" >> ${pLogStdPath};;
        1 ) echo "$(date +'%Y/%m/%d %H:%M:%S') $*" >> ${pLogErrPath};;
        2 ) logger "$*";;
    esac
}

### Main

# 0 Standard
fnLogger 0 "Message"

# 1 Error
fnLogger 1 "Error Message"

# 2 /var/log/messages
fnLogger 2 "/var/log/messages output message"


パッと見て、0~2 の数字も一緒に出力されそうに見えるけど、実行結果はこんな感じ。

$ bash ./sample.sh
$ 
$ cat sss_20190519.log
2019/05/19 14:04:13 Message
$ 
$ cat sss_20190519_Err.log
2019/05/19 14:04:13 Error Message
$ 
$ tail -1 /var/log/messages
May 19 14:04:13 (ホスト名) (ユーザ名): /var/log/messages output message


shift コマンドが引数をひとつ消してくれる効果を持っている。


それから、コードを利用したログ出力方法もこんな一案。
コード自体を変数名としたもの。

$ cat mcode.txt
A000E="Sample essage."
A001E="Message 1st"
A002E="Message 2nd"
$ 
$ cat sample2.sh
#!/bin/bash

. ./mcode.txt

fnLogCode() {
    echo "${1}: $(eval echo '$'${1})"
}

fnLogCode A000E
$ 
$ bash ./sample2.sh
A000E: Sample essage.
$ 


コードは csv 形式で記載されているもの。

$ cat code.csv
I0000,Sample message
I0001,Information message.
W0001,Warning message.
E0001,Error message.

これを抽出する方法を考えてみる。まずはシンプルに grep で。

$ grep I0000 code.csv
I0000,Sample message
$ 
$ grep I0000 code.csv | cut -d "," -f 1
I0000
$ 
$ grep I0000 code.csv | cut -d "," -f 2
Sample message
$ 
### Parameter and Variable

## Parameter
pID=$(/usr/bin/basename ${0} .sh)
pYYYYMMDD=$(date +'%Y%m%d')

## Directory
pLogDir=/var/log/script

## File
pLogCSVFile=code.csv
pLogStdFile=${pID}_${pYYYYMMDD}.log

## Path
pLogStdPath=${pLogDir}/${pLogStdFile}
pLogCSVPath=${pLogDir}/${pLogCSVFile}

### Function
fnLogger() {
    [ -r "${pLogCSVPath}" ] || return 1
    pMessage=$(grep $1 ${pLogCSVPath} | cut -d "," -f 2)
    [ "${pBaseMessage}" == "" ] && return 1

    echo "$(date +'%Y/%m/%d %H:%M:%S') ${pMessage}" >> ${pLogStdPath};;
}

これだとメッセージ内容に , を含められないので、含められるようにしたバージョン。

### Parameter and Variable

## Parameter
pID=$(/usr/bin/basename ${0} .sh)
pYYYYMMDD=$(date +'%Y%m%d')

## Directory
pLogDir=/var/log/script

## File
pLogCSVFile=code.csv
pLogStdFile=${pID}_${pYYYYMMDD}.log

## Path
pLogStdPath=${pLogDir}/${pLogStdFile}
pLogCSVPath=${pLogDir}/${pLogCSVFile}

### Function
fnLogger() {
    [ -r "${pLogCSVPath}" ] || return 1
    tMessage=$(grep $1 ${pLogCSVPath})
    [ "${tBaseMessage}" == "" ] && return 1

    pMessage=${tMessage#*,}

    echo "$(date +'%Y/%m/%d %H:%M:%S') ${pMessage}" >> ${pLogStdPath};;
}

考えていくと、いろいろとやり方はあるもんだね。