コマンドの出力を改造して使いやすくする

# <i class="fa fa-info-circle" style="font-size:1em;"></i> 背景

運用の現場での苦労として、シェルの設計規約で実行時のコマンドラインが冗長になってしまう、ミドルウェアのコマンド出力が行志向になっておらず、grep、sed、awkにうまく繋げられない、といったことがある。
これらをそういうものだと諦めている人もいるかもしれないが、コマンドに関係するものであれば、機能や出力を改変することで作業しやすくなる場合がある。

# <i class="fa fa-cube" style="font-size:1em;"></i> 固定情報を自動で付ける

シェルでechoを使ってメッセージを出力させる場合、先頭に日時やシェル名を付けることが多いと思う。これは実行時間と対象シェルを特定するのに役立つため大切なことは間違いないのだが、本来のコーディングとは直接関係ない上に、後からコードを見なおした時に冗長で読みづらいといった欠点がある。
こんな時にはaliasや関数で自動的に情報を付け足すと、開発者が余分な記述をしなくて済むようになる。

## <i class="fa fa-file-text-o" style="font-size:1em;"></i> サンプル①常時出力版
```bash
alias echo='echo `date +"%Y/%m/%d %H:%M:%S"` `basename $0 2>/dev/null` $@'
```
```bash
alias ECHO='echo `date +"%Y/%m/%d %H:%M:%S"` `basename $0 2>/dev/null` $@'
```
echoを完全に置き換えたい場合は一つめを、それ以外は二つめを使うと良い。
一つめはechoの定義にechoを使っているので、実行時に無限ループしてしまうのではないかと思うかもしれないが、エイリアスはデフォルトで再帰処理が行われないようになっているので問題ない。


## <i class="fa fa-file-text-o" style="font-size:1em;"></i> サンプル②オプション版

```bash:ECHO.fnc
#!/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                           |

## <i class="fa fa-edit" style="font-size:1em;"></i> 使い方の例

```
% . ./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 テスト
% 
```



# <i class="fa fa-cube" style="font-size:1em;"></i> 出力を整形する

OSに同梱されているコマンドの出力は概ねUNIXの思想に沿ったものになっているが、業務アプリケーションやミドルウェアなどの製品コマンドは結構いい加減である。
フィルタとして使われることを考慮していない（行志向になっていない）出力が多く、そのようなコマンドを使う場合、パイプでgrepやsed、awkに渡しても整形が難しく、値の集計にえらく苦労させられたりする。
そんな時は、予め、フィルタしやすい形式に変換して出力するラッパーコマンドを作っておくと便利である。
ここでは、一例としてBorland VisiBrokerのコマンドである`osfind`の出力を改造してみる。

## <i class="fa fa-edit" style="font-size:1em;"></i> osfindの出力形式

VisiBrokerはCORBA規格を実装した製品の一つで、osfindコマンドはネームサービスと登録されたオブジェクト、実装リポジトリの情報を出力する。（ここでは技術的な説明はしない。詳細はリンクを参照して欲しい。→CORBA[^1][^2]、VisiBroker[^3][^4]、osfindコマンド[^5]）

osfindを実行すると、以下のように表示される。

```bash: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を知らない方は出力が何を意味しているのかわからないと思うが、値が階層構造になっていることはなんとなくわかると思う。

```bash:出力結果の値の構造
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行を関連付けて表示した、以下のようなものがいいだろう。見やすいし、もし値を集計することになったとしても整理が簡単そうだ。

```bash:見やすい出力
% 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
%
```

このような出力になるようなコマンドの組み立てをオペレーション時にパッと思いつくことは、不可能とまでは言わないが、相当に難しい。そこで、このような処理をスクリプト化して使えるようにしておけば、同僚からとても喜ばれることになる。

## <i class="fa fa-cubes" style="font-size:1em;"></i> 変換ロジックの設計

まず、一つ目から三つ目までのosfindブロックが今回対象外なので、除く必要がある。
行数が固定であればtailで絞ることが可能だ（例えば、`osfind | head -n +19`）。しかし、osfindブロックの行数は可変のためこの手は使えない。osfindの結果をwhile文に渡し、一行一行grepで調べて行くのが妥当なやり方だ。
osfind行が来るたびにカウンターを＋１して、４となった場合に変換処理のブロックに入るようにしよう。

```bash:ロジック①１～３つめのosfindブロックを無視する（４つめのosfindブロックから変換処理を開始する）
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行が来た時とする。

```bash:ロジック②変換処理
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}'` とすればよいのではと思うかもしれない。

```bash
HOST=`echo ${line} | cut -d: -f2- | sed 's/^[[:blank:]]*//'`
```

しかし抽出する値にはスペースを指定することが可能である。そのため、その指定方法では値の一部しか切り出せない可能性がある。
値を確実に切り出すには区切り文字に空白でなく、コロン（:）を指定する必要がある。しかしその場合、切り出し時に値の前にある空白部分が残ってしまう。そのため最後にsedで余白※を除くことをしている。これは、切り出しにcutでなくawkを使っても同じである。

※[:blank:]はPOSIXで規定された空白を表す正規表現メタ文字で、grep -E（＝egrep）やsedで使用することができる。\sも空白文字を表し、最近のGNU sedでは利用できるが、ここでは移植性を優先した。

最後に細かいところを見ていこう。
上記二つのロジックでは、わかりやすさを優先して意図的に以下を無視した。しかし、シェルを完成させるには考慮しなければならない。

1. 空行を処理対象から外す

2. osfindの出力は、実は末尾に空白文字が付いており、除去する必要がある

＜1について＞
説明した①②のロジックは、空行が入っていないことが前提であるため、そのブロックに入る前に除去してしまわなければならない。（各ロジック内の例外処理箇所で対応はできる。しかし、それはたまたま処理できるというだけで、先頭で除去しておくことに越したことはない）
空行の検知はgrepでチェックすればよいだろう。空行にマッチする正規表現は^\$だが、さらに空白文字が入っていた場合を想定して^[[:blank:]]*\$としておこう。

```bash:空行の除去処理
count=0
osfind | grep -v -E '^[[:blank:]]*$' | while read line
do
    if [ ${count} -lt 4 ]; then
        # osfind行のカウント処理
    else
        # 変換処理
    fi
done
```

＜2について＞
なぜかは不明だが、末尾に空白が付いてきてしまうので除去する必要がある。単純に、値の切り出し処理に、末尾の余白部分の除去処理を加えればいいだろう。（`tr -d`という横着をしてはいけない。値にはスペースが含まれるからだ）

```bash:末尾の余白除去
HOST=`echo ${line} | cut -d: -f2- | sed 's/^[[:blank:]]*//' | sed 's/[[:blank:]]*$//'`
```

これでようやく完成である。

## <i class="fa fa-file-text-o" style="font-size:1em;"></i> サンプル③osfindの出力整形シェル

```bash: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`
こう変えればコードを試すことができる。

このような変換処理は他の製品コマンドでもできるはず。出力が微妙なコマンドを前もって使いやすい表示に変えておけば作業効率を上げることができるし、何より色々いじくるのは楽しいものだ。
ぜひ自分でも試してみてほしい。

## <i class="fa fa-arrow-circle-right" style="font-size:1em;"></i> 参考

[^1]: [ORB の輪（オージス総研サイト）](http://www.ogis-ri.co.jp/otc/hiroba/technical/ORBRing/index.html)

[^2]: [■ CORBA 解説 ■（NECサイト）](http://techpubs.borland.com/am/)

[^3]: [Borland Technical Publications - Application Middleware（Borlandサイト：英語）](http://techpubs.borland.com/am/)

[^4]: [Borland(R) Enterprise Server VisiBroker(R)　デベロッパーズガイド（日立製作所サイト：日本語）](http://itdoc.hitachi.co.jp/manuals/3020/30203M4500/EM450001.HTM)

[^5]: [Borland(R) Enterprise Server VisiBroker(R)　デベロッパーズガイド osfindコマンドの説明（日立製作所サイト）](http://itdoc.hitachi.co.jp/manuals/3020/30203M4500/EM450229.HTM)