背景
運用の現場での苦労として、シェルの設計規約で実行時のコマンドラインが冗長になってしまう、ミドルウェアのコマンド出力が行志向になっておらず、grep、sed、awkにうまく繋げられない、といったことがある。
これらをそういうものだと諦めている人もいるかもしれないが、コマンドに関係するものであれば、機能や出力を改変することで作業しやすくなる場合がある。
固定情報を自動で付ける
シェルでechoを使ってメッセージを出力させる場合、先頭に日時やシェル名を付けることが多いと思う。これは実行時間と対象シェルを特定するのに役立つため大切なことは間違いないのだが、本来のコーディングとは直接関係ない上に、後からコードを見なおした時に冗長で読みづらいといった欠点がある。
こんな時にはaliasや関数で自動的に情報を付け足すと、開発者が余分な記述をしなくて済むようになる。
サンプル①常時出力版
alias echo='echo `date +"%Y/%m/%d %H:%M:%S"` `basename $0 2>/dev/null` $@'
alias ECHO='echo `date +"%Y/%m/%d %H:%M:%S"` `basename $0 2>/dev/null` $@'
echoを完全に置き換えたい場合は一つめを、それ以外は二つめを使うと良い。
一つめはechoの定義にechoを使っているので、実行時に無限ループしてしまうのではないかと思うかもしれないが、エイリアスはデフォルトで再帰処理が行われないようになっているので問題ない。
サンプル②オプション版
# !/bin/sh
logfile=
ECHO() {
arg1="$1"
case "${arg1}" in
--term)
shift
echo `date +"%Y/%m/%d %H:%M:%S"` `basename $0 2>/dev/null` "$@"
;;
--log)
test -z "${logfile}" && return 8
shift
echo `date +"%Y/%m/%d %H:%M:%S"` `basename $0 2>/dev/null` "$@" >> ${logfile}
;;
--tee)
test -z "${logfile}" && return 8
shift
echo `date +"%Y/%m/%d %H:%M:%S"` `basename $0 2>/dev/null` "$@" | tee -a ${logfile}
;;
*)
echo "$@"
;;
esac
}
常に頭に日時が付くのは良くないと思うこともあるだろう。そこで、日時の出力をオプション化したバージョンも作ってみた。この場合は、条件分岐などの制御文を使う必要があるので関数を使用する。
サンプル①と違ってecho版が無いが、関数を使用する場合に組み込みコマンドのechoを上書くように作るのは難しい。こうした場合、関数はaliasと違って再帰呼び出しが行われてしまうので、ループしないよう通常はbuiltinコマンドが用いられるのだが、このコマンドは環境で正常に動作したりしなかったりするため移植性が悪くなってしまう。私の知識ではbuiltin以外の方法が思いつかなかったためECHO版のみとしている。
なお、せっかく関数を使っているので、この方が便利かな?と思うオプションにしてみた。
オプション | 機能 |
---|---|
--term | 日時とシェル名を先頭に出力+文字列を標準出力に書き込み |
--log | 日時とシェル名を先頭に出力+文字列を${logfile}に書き込み |
--tee | 日時とシェル名を先頭に出力+文字列を標準出力と${logfile}に書き込み |
上記以外、または無し | ただのecho |
使い方の例
% . ./ECHO.fnc
% logfile=/var/tmp/test.log # ログ出力させる場合は、シェル変数logfileに出力先をセットする。
% ECHO --tee "テスト"
2014/12/21 12:00:00 su テスト
% cat /var/tmp/test.log
2014/12/21 12:00:00 su テスト
%
出力を整形する
OSに同梱されているコマンドの出力は概ねUNIXの思想に沿ったものになっているが、業務アプリケーションやミドルウェアなどの製品コマンドは結構いい加減である。
フィルタとして使われることを考慮していない(行志向になっていない)出力が多く、そのようなコマンドを使う場合、パイプでgrepやsed、awkに渡しても整形が難しく、値の集計にえらく苦労させられたりする。
そんな時は、予め、フィルタしやすい形式に変換して出力するラッパーコマンドを作っておくと便利である。
ここでは、一例としてBorland VisiBrokerのコマンドであるosfind
の出力を改造してみる。
osfindの出力形式
VisiBrokerはCORBA規格を実装した製品の一つで、osfindコマンドはネームサービスと登録されたオブジェクト、実装リポジトリの情報を出力する。(ここでは技術的な説明はしない。詳細はリンクを参照して欲しい。→CORBA12、VisiBroker34、osfindコマンド5)
osfindを実行すると、以下のように表示される。
% osfind
osfind: Found one agent at port 14000
HOST: HostA
HOST: HostB
osfind: Found 1 OADs in your domain
HOST: HostC
HOST: HostD
osfind: Following are the list of Implementations registered with OADs.
HOST: HostC
REPOSITORY ID: IDL:Bank/Account:1.0
OBJECT NAME: Jack B. Quick
HOST: HostD
REPOSITORY ID: IDL:Bank/Account:1.0
OBJECT NAME: Jack C. Quick
osfind: Following are the list of Implementations started manually.
HOST: HostA
REPOSITORY ID: IDL:visigenic.com/Activation/OAD:1.0
OBJECT NAME: object1
HOST: HostD
REPOSITORY ID: IDL:visigenic.com/Activation/OAD:1.0
OBJECT NAME: object1
REPOSITORY ID: IDL:visigenic.com/Activation/OAD:2.0
OBJECT NAME: object2
OBJECT NAME: object3
%
すべての情報を扱うとわかりづらくなるため、オブジェクトの情報(osfind:で始まるブロックの四つ目)に絞って話をする。本来は一つ目のブロックも考慮しなければならないが、CORBAの理解が必要になるので省く。
CORBAやVisiBrokerを知らない方は出力が何を意味しているのかわからないと思うが、値が階層構造になっていることはなんとなくわかると思う。
HostA
└IDL:visigenic.com/Activation/OAD:1.0
└object1
HostD
└IDL:visigenic.com/Activation/OAD:1.0
└object1
└IDL:visigenic.com/Activation/OAD:2.0
└object2
└object3
実際の運用では、HOSTやREPOSITORY ID、OBJECT NAMEはもっと大量に表示されてくることになる。そのような出力から、例えば、object1がどのHostに属しているかを確認する必要があるとすると、かなり面倒な作業となる。
基本的に必要な情報だけ表示させたいとき、grepで対象行を、awkで対象列を絞って表示することが多いが、このような出力の場合、それは簡単にはできない。
少し考えてみてほしいが、単にobject1をキーワードにgrepするだけではどのHost、REPOSITORY IDに属すかがわからない。
現実的にはosfind | grep -e osfind -e HOST -e REPOSITORY -e object1
のようにするか、コマンドの結果は絞らずに上から順に目視で確認していくといったやり方になるだろう。
もし見やすい表示結果にできるとするなら、OBJECT NAME行、REPOSITORY ID行、HOST行を関連付けて表示した、以下のようなものがいいだろう。見やすいし、もし値を集計することになったとしても整理が簡単そうだ。
% osfind
HOST REPOSITORY ID OBJECT NAME
HostA IDL:visigenic.com/Activation/OAD:1.0 object1
HostD IDL:visigenic.com/Activation/OAD:1.0 object1
HostD IDL:visigenic.com/Activation/OAD:2.0 object2
HostD IDL:visigenic.com/Activation/OAD:2.0 object3
%
このような出力になるようなコマンドの組み立てをオペレーション時にパッと思いつくことは、不可能とまでは言わないが、相当に難しい。そこで、このような処理をスクリプト化して使えるようにしておけば、同僚からとても喜ばれることになる。
変換ロジックの設計
まず、一つ目から三つ目までのosfindブロックが今回対象外なので、除く必要がある。
行数が固定であればtailで絞ることが可能だ(例えば、osfind | head -n +19
)。しかし、osfindブロックの行数は可変のためこの手は使えない。osfindの結果をwhile文に渡し、一行一行grepで調べて行くのが妥当なやり方だ。
osfind行が来るたびにカウンターを+1して、4となった場合に変換処理のブロックに入るようにしよう。
count=0
osfind | while read line
do
if [ ${count} -lt 4 ]; then
echo ${line} | grep '^osfind' >/dev/null
if [ $? -eq 0 ]; then
count=`expr ${count} + 1`
continue
fi
else
# 変換処理
fi
done
次に変換処理のロジックである。
変換後の出力のイメージとしては、OBJECT NAME行の値にHOSTとREPOSITORY IDの値を付加して表示するものとなる。それぞれの行からcutやsed、awkを使って値を切り出し、echoで一行にまとめて書き出すのがいいだろう。
HOSTとREPOSITORY IDはそれぞれブロックに入ったタイミングでシェル変数に値を格納し、同じブロック内ではそれを使い回す。書き出すタイミングはOBJECT NAME行が来た時とする。
item=`echo ${line} | cut -f1`
case ${item} in
HOST:)
HOST=`echo ${line} | cut -d: -f2- | sed 's/^[[:blank:]]*//'`
;;
REPOSITORY)
REPO=`echo ${line} | cut -d: -f2- | sed 's/^[[:blank:]]*//'`
;;
OBJECT)
OBJ=`echo ${line} | cut -d: -f2- | sed 's/^[[:blank:]]*//'`
echo "${HOST}\t${REPO}\t${OBJ}"
*)
# 上記以外の行が紛れていた場合は無視する
;;
esac
以下は echo ${line} | awk '{print $NF}'
とすればよいのではと思うかもしれない。
HOST=`echo ${line} | cut -d: -f2- | sed 's/^[[:blank:]]*//'`
しかし抽出する値にはスペースを指定することが可能である。そのため、その指定方法では値の一部しか切り出せない可能性がある。
値を確実に切り出すには区切り文字に空白でなく、コロン(:)を指定する必要がある。しかしその場合、切り出し時に値の前にある空白部分が残ってしまう。そのため最後にsedで余白※を除くことをしている。これは、切り出しにcutでなくawkを使っても同じである。
※[:blank:]はPOSIXで規定された空白を表す正規表現メタ文字で、grep -E(=egrep)やsedで使用することができる。\sも空白文字を表し、最近のGNU sedでは利用できるが、ここでは移植性を優先した。
最後に細かいところを見ていこう。
上記二つのロジックでは、わかりやすさを優先して意図的に以下を無視した。しかし、シェルを完成させるには考慮しなければならない。
-
空行を処理対象から外す
-
osfindの出力は、実は末尾に空白文字が付いており、除去する必要がある
<1について>
説明した①②のロジックは、空行が入っていないことが前提であるため、そのブロックに入る前に除去してしまわなければならない。(各ロジック内の例外処理箇所で対応はできる。しかし、それはたまたま処理できるというだけで、先頭で除去しておくことに越したことはない)
空行の検知はgrepでチェックすればよいだろう。空行にマッチする正規表現は^$だが、さらに空白文字が入っていた場合を想定して^[[:blank:]]*$としておこう。
count=0
osfind | grep -v -E '^[[:blank:]]*$' | while read line
do
if [ ${count} -lt 4 ]; then
# osfind行のカウント処理
else
# 変換処理
fi
done
<2について>
なぜかは不明だが、末尾に空白が付いてきてしまうので除去する必要がある。単純に、値の切り出し処理に、末尾の余白部分の除去処理を加えればいいだろう。(tr -d
という横着をしてはいけない。値にはスペースが含まれるからだ)
HOST=`echo ${line} | cut -d: -f2- | sed 's/^[[:blank:]]*//' | sed 's/[[:blank:]]*$//'`
これでようやく完成である。
サンプル③osfindの出力整形シェル
# !/bin/sh
echo "HOST\tREPOSITORY ID\tOBJECT NAME"
count=0
osfind | grep -v -E '^[[:blank:]]*$' | while read line
do
if [ ${count} -lt 4 ]; then
echo ${line} | grep '^osfind' >/dev/null
if [ $? -eq 0 ]; then
count=`expr ${count} + 1`
continue
fi
else
item=`echo ${line} | cut -d ' ' -f1`
case ${item} in
HOST:)
HOST=`echo ${line} | cut -d: -f2- | sed 's/^[[:blank:]]*//' | sed 's/[[:blank:]]*$//'`
;;
REPOSITORY)
REPO=`echo ${line} | cut -d: -f2- | sed 's/^[[:blank:]]*//' | sed 's/[[:blank:]]*$//'`
;;
OBJECT)
OBJ=`echo ${line} | cut -d: -f2- | sed 's/^[[:blank:]]*//' | sed 's/[[:blank:]]*$//'`
echo "${HOST}\t${REPO}\t${OBJ}"
;;
*)
# 上記以外の行が紛れていた場合は無視する
;;
esac
fi
done
VisiBrokerは商用の製品であり個人が購入するなどあり得ない(と思う)ので、サンプルコードを試すには、この投稿のosfind出力をosfind.outなどに保存して、
osfind | grep -v -E '^[[:blank:]]*$' | while read line
これを
cat osfind.out | grep -v -E '^[[:blank:]]*$' | while read line
こう変えればコードを試すことができる。
このような変換処理は他の製品コマンドでもできるはず。出力が微妙なコマンドを前もって使いやすい表示に変えておけば作業効率を上げることができるし、何より色々いじくるのは楽しいものだ。
ぜひ自分でも試してみてほしい。