scanコマンドというcliツールを作った

scanコマンドというcliツールを作った。

GitHub - genya0407/scan

scanコマンドは、標準入力の各行に対して正規表現を適用し、ほしい部分を取り出すコマンドだ。使い方は以下の通り。

$ scan --help
Usage: scan [options] OUTPUT_FORMAT
    -p [PATTERN]                     specify regexp
    -d [DELIMITER]                   specify delimiter

使用例を見てもらったほうが早いだろう。

使用例

正規表現を適用する例

例えばこういうファイルがあったとする。

$ cat data.txt
hogehoge_nyan
hohho_nyan

これに対して、アンダースコアの左だけを取り出す正規表現を適用するには、以下のようにする。

$ cat data.txt | scan -p "(.+?)_.+" {1}
hogehoge
hohho

アンダースコアの左と右を、 , で繋いで出力したいときは以下のようにする。

$ cat data.txt | scan -p "(.+?)_(.+)" {1},{2}
hogehoge,nyan
hohho,nyan

複雑な正規表現を適用するときは、名前付きキャプチャも使用できる。

$ cat data.txt | scan -p "(?<name1>.+?)_(?<name2>.+)" {name1}:{name2}
hogehoge:nyan
hohho:nyan

正規表現エンジンはRuby標準のものをそのまま使っているので、正規表現の詳しい仕様についてはリファレンスをあたってください。

正規表現 (Ruby 2.7.0 リファレンスマニュアル)

区切り文字を指定する例

また、正規表現を使うのではなく、区切り文字を指定することもできる。

例えばこういうファイルがあったとする。

$ cat hoge.csv
aaa,bbb,ccc
xxx,yyy,zzz

このとき、区切り文字として , を指定して、左から3番目のフィールドを切り出すためには以下のようにする。

$ cat hoge.csv | scan -d , {3}
ccc
zzz

また、scanコマンドに何もオプションを指定しない場合、区切り文字として \s+ つまり「1つ以上連続する空白文字列」が指定されていると解釈される。

例えばこういうファイルがあるとする。

$ cat hoge.tsv
aaa     bbb     ccc
xxx     yyy     zzz

このファイルを、オプションを何も指定しないscanコマンドに食わせると、空白文字を区切りと解釈して左からn番目の文字列を取り出すことができる。

$ cat hoge.tsv | scan {2}
bbb
yyy

インストール方法

Ruby 2.6.3 以上が動く環境を前提として、以下のファイルをパスが通った場所に配置して、実行権限を付与すれば動く。依存ライブラリとかはなく、Rubyがあれば動きます。

scan/scan at master · genya0407/scan · GitHub

もう少しいい感じのインストール方法を模索していますが、ひとまずはこれで許してください。

なぜこのコマンドを作ったのか

仕事柄(?)、バグが出たときとかにサーバーのログを漁る必要にかられることがよくある。そういうときに正規表現は便利だ。

$ cat server.log | (アクセスしたユーザーのIDをいい感じに取り出す正規表現) | sort | uniq -c
100 user_id_111 # user_id_111 が100回もアクセスしているのがわかる
 10 user_id_222
..

しかし、正規表現をシュッと書いて値を抜き出す適用するツールが見当たらなかった*1。多分awkとかperlとかのワンライナー正規表現をかけるとは思うのだが、僕はawkperlも使い方がわからないし、言語ごとに存在すると思われる正規表現の方言を覚えるのもなんだかなあという気持ちだった。

僕が一番使えるプログラミング言語Rubyなので、一時期はRubyワンライナーを書くということをやっていた。

$ cat server.log | ruby -ne 'puts $_[/user_id: (.+)\s+/, 1]' | sort | uniq -c

これも割といい線行ってるとは思うが、正規表現で値を抜き出したいだけなのに ruby -ne とか puts とか $_ とか書くのはイケてない。

次に使ってたのはrargsというツールで、これは限りなく正解に近い。

GitHub - lotabout/rargs: xargs + awk with pattern matching support. `ls *.bak | rargs -p '(.*)\.bak' mv {0} {1}`

rargsは、正規表現を指定して文字列を抜き出すことができ、抜き出した文字列を使ったコマンドを実行することができる。正規表現の代わりに区切り文字を指定することもできる。

$ ls *.bak | rargs -p '(.*)\.bak' mv {0} {1}
$ cat hoge.csv | rargs -d , echo {2}

しかし、rargsはコマンドを実行する都合上、入力される行の数だけプロセスを立ち上げる必要がある。環境にもよるが、プロセスの立ち上げはそこそこヘビーな処理で、業務で使っているMacBook Proではここがものすごく重かった*2。そのため、長めのログファイルをrargsに食わせると、ちょっと現実的ではないぐらい集計に時間がかかってしまう状態になっていた。

私が望む用途(=ログの集計)ではコマンドを実行する必要はない。そのため、rargsからコマンド実行の機能を削り、高速に正規表現を適用するだけのコマンドを作ればよいだろう、という発想に至った。

そして、今回説明したscanコマンドを作った。

まとめ

標準入力に正規表現をシュッと適用して、好きなフォーマットに整形して出力するコマンドである scan を作った。これは、ログの集計・整形などに使うことができ、実用的なレベルには高速であり、自身も便利に使っている。

*1:古参のエンジニアの皆さんはおすすめの正規表現ツールでも書いててください

*2:Instrumentsでプロファイルしたらposix_nspawnでめちゃめちゃ時間喰ってた。おそらくMacBook Proが悪いのではなく、セキュリティソフトかなにかが原因で遅いのではないかと疑っている。お家で使ってるThinkipad上のLinuxでは全然重くなかった。