bonar note

京都のエンジニア bonar の技術的なことや技術的でない日常のブログです。

pcut - もう一つの cut

僕が一番好きな UNIXコマンド は cut です。

名前と機能が完全に一致していて、シンプルな動作(-d で指定したデリミタで行を区切り、-f で指定したフィールドを取り出す)を完璧に行っているからです。例えばログファイルからIPアドレスだけを抜き出してユニークな数を数えたりする時等に大活躍ですよね。

そんな素晴らしい cut コマンドですが、あとちょっとこれが出来たらいいのに!と思うことも多少あったりします。その殆どは sed/awk 等を使えば可能なことだったりするのですが、毎回同じような作業をしているなと感じたので「もう一つの(遅い)cut」として新たに実装しました。

以下のコマンドで install 出来ます。

sudo gem install pcut

github はこちら
https://github.com/bonar/pcut

pcut は ruby で書かれたもう一つの cut です。本家の cut に比べて動作はかなり遅いですが、以下のような変わった追加機能があります。

フィールド番号の表示

cut コマンドでは -f で表示するフィールド番号を指定しますが、apache の custom log 等で一行が長くなっている場合に、「UserAgent は何番だろう」というのがわからなかったりします。普段は 8-, 10- みたいな感じで寄せていって目的のフィールド番号を調べていたのですが、このようにパターゴルフ的に探索するのがなんだかちょっと気恥ずかしいと感じていました。

pcut では --preview(-p) でフィールド番号を表示します。また、--vertical(-v) で結果を縦に表示します。各行のフィールド番号と内容を確認する際は縦表示の方が見やすかったりします。

f:id:bonar:20120415233012p:image

head -n 1 | pcut -p -v で行の組成を下調べしてから、-f でフィールドを絞り込めるようになります。非常にどうでもいい感じがしますが、これが実は一番やりたかったことだったりします。

クォート

cut コマンドのデフォルトのデリミタはタブです。ただ、現実世界のほとんどのログファイルは white space で分割されている(様に見える)ので、cut -d " " で white space を指定して分割することになります。例えば以下のようなよくある apache の log file を分割すると

127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"

こうなります。

127.0.0.1 
- 
frank 
[10/Oct/2000:13:55:36 
-0700] 
"GET 
/apache_pb.gif 
HTTP/1.0" 
200 
2326 
"http://www.example.com/start.html" 
"Mozilla/4.08 
[en] 
(Win98; 
I 
;Nav)"

時間の部分が複数に分かれてしまいます。時間に関してはフィールド番号 4,5 なのでまだいいですが、UserAgent 部分に関しては中身によってフィールド番号も変わるのではかなり辛いです。これらは行のパース処理がクォートを意識していないことによるものです。

pcut では -q でクォートとして意識する文字を複数指定できます。D=double quote, S=single quote, [=[], (=() といった具体です。これを使うとログの組成の意味通りにフィールドを区切ることが出来ます。

$ cat test | pcut -d " " --preview --vertical -q "D["[1]127.0.0.1[2]-
[3]frank
[4]10/Oct/2000:13:55:36 -0700
[5]GET /apache_pb.gif HTTP/1.0
[6]200
[7]2326
[8]http://www.example.com/start.html
[9]Mozilla/4.08 [en] (Win98; I ;Nav)

デフォルトでは囲っているクォート文字を除去しますが、--keep-quote(-k) でそのまま残すことも出来ます。

サブクエリー

pcut では -f のフィールド指定部分で、ドットに続けてさらにカットを行うためのサブクエリーを指定する事が出来ます。例えば -f 3 は「フィールド番号3を表示」という意味ですが、-f 3.[/_/, 4] は「フィールド番号3を _ で分割してその4つめを表示」という意味になります。また、-f 3.[/_/, 4].[/ /, 2] 等のように連結していく事が出来ます。

$ cat test 127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326 "http://www.example.com/start.html" "Mozilla/4.08 [en] (Win98; I ;Nav)"
$ cat test | pcut -d " " -q D[ -f "9"
Mozilla/4.08 [en] (Win98; I ;Nav)
$ cat test | pcut -d " " -q D[ -f "9.[/ /,1]"
Mozilla/4.08$ cat test | pcut -d " " -q D[ -f "9.[/ /,1].[///,2]"
4.08
$ cat test | pcut -d " " -q D[ -f "9.[/ /,1].[///,2].[/./,1]"4

これらは項目が一つであれば cut コマンドをパイプで繋いでいって実現可能ですが、例えば apache のログで「IPアドレスとリクエストパスの一部」みたいな組み合わせの出力を行おうとすると苦しく、sed 等を使ってその場限りの正規表現を書くことになりがちです。

特殊文法を使っていたり、単純な分割のみ(クォートを意識しない)だったりとちょっと機能的には大味なのですが、意外と便利な局面があるのではと思っています。

連続するデリミタ、デリミタ以外での連結

見た目の桁を揃えるために空白が余分にあるようなケースがあります。例えば以下のようなファイルで先頭の month, day の二つのフィールドを取り出すようなことはかなりやり辛いです。

$ cat system.log 
Apr  4 23:47:40 localhost applepushserviced[558]: 
Apr  4 23:47:40 localhost applepushserviced[558]: 
Apr  4 23:47:40 localhost applepushserviced[558]: 
Apr  4 23:47:40 localhost UserEventAgent[11]: 
Apr  4 23:47:40 localhost UserEventAgent[11]: 
Apr  4 23:47:40 localhost UserEventAgent[11]: 
Apr 10 23:47:40 localhost configd[14]: network configuration changed.
Apr 11 23:47:40 localhost applepushserviced[558]: 
Apr 12 23:47:59 localhost configd[14]: network configuration changed.
Apr 15 00:58:00 localhost newsyslog[77885]: logfile turned over
$ cat system.log | cut -d " " -f 1,2
Apr 
Apr 
Apr 
Apr 
Apr 
Apr 
Apr 10
Apr 11
Apr 12
Apr 15

cut は -b でバイト単位で切り出すことが出来るので頑張れなくも無いですが、pcut では -c で連続したデリミタをスキップすることでより自然に切り出すことが出来ます。

$ cat system.log | pcut -c -d " " -f 1,2
Apr 4
Apr 4
Apr 4
Apr 4
Apr 4
Apr 4
Apr 10
Apr 11
Apr 12
Apr 15

また、-j で 最後に結果を表示する際にデリミタ意外の文字で連結することが出来ます。

$ cat system.log | pcut -c -d " " -f 1,2 -j ","
Apr,4
Apr,4
Apr,4
Apr,4
Apr,4
Apr,4
Apr,10
Apr,11Apr,12
Apr,15

まとめ

僕の場合はこんな機能が欲しかった、というのを実装してみたのですがどうですかねー。「いや、それ cut で出来るから」とか「それをやるならもっと良いツールがある」ということもあるかもしれません。バグ等、お気づきの事があればコメント下さい!

newgems で package を作ったのですが、rake package; gem push pkg/pcut-0.1.0.gem するだけで rubygems に上がるのでかなり楽ですね。CLI 部分は thor を使ってみようとしたのですが help の表示が微妙に思った通りに出来なくて optparse に戻してしまいました。落ち着きます。

マルチバイトのケアとか、全般の動作検証が足りてないので今後はその辺りを遣って行きたいです。