Rosie Pattern Languageに入門(1)

Rosie Pattern Languageを触ってみたのでメモを残します。

なおversion 1.1.0の情報なので詳細・更新後の情報は Rosie Pattern Language / Rosie · GitLab を御覧ください。

Rosie Pattern Languageとは

RosieはPEG(Parsing Expression Grammars)を使ってパターンを記述することで、正規表現よりも強力に検索や置換・パースなどを行うための言語・ツール群です。

作ったパターンのテストも可能であり、ad-hocな正規表現拡張を駆使するよりも、より保守しやすく・より複雑なデータを処理することを可能にすることを目的としているようです。

使ってみる

インストール

まずはインストール

$ brew tap rosie-community/rosie https://gitlab.com/rosie-community/packages/homebrew-rosie.git
$ brew install rosie

簡単な使い方

そして、以下のコマンドを実行します。

$ curl -s https://github.com | rosie grep -o color net.url

※ わかりやすくするためにoutputオプションとしてcolorをつけたりheadで行数を絞っています

f:id:matsu_chara:20190525151423p:plain

URLだけ欲しいという場合はoutputオプションとしてsubsを指定します。*1

$ curl -s https://github.com | rosie grep -o subs net.url

f:id:matsu_chara:20190525152938p:plain

subsは grep -o に似ていますが、httpだったりhttpsだったり、urlの終端判定はどうするのか?などを考えるとなかなか面倒です。 以下のようにすると一見うまくいきそうですが・・・

$ curl -s https://github.com | grep -o 'https\?://[^"]*'
...
https://github.githubassets.com/favicon.ico
https://github.com/
https://github.com/","referrer":null,"user_id":null}}
...

と地味に目的どおりに行かない場合があります。

net.url以外にも例えば

  • "6.02e23", "3.00E08", "0.123"なども拾えるfloatを含めたnumber
  • "2015-10-14T22:11:20+00:00" などrfc3339や、その他いろいろにマッチできるtimestamp
  • "1.2.3-alpha.7.8.9" など1.2.3以外をパースしようとすると意外とサクッとできないsemver

など様々なパターンが標準で組み込まれています。

使い方は例えば以下です。

$ cat vers
1.2.3-alpha2.4
12.0.5
1
2.3.4

$ cat vers | rosie grep ver.semver
1.2.3-alpha2.4
12.0.5
2.3.4

このような便利な標準パターンが組み込まれていて便利ですね!

RPL

便利ですね!・・・で終わっても十分に便利なんですが、rosieはプログラミング言語のようにパターンを記述したファイル(.rpl)を用意して、 自分の欲しいパターンを柔軟に記述できる点を特徴としています。 正規表現みたいですがrosieはPEGベースで記述できるのでより強力です。*2

semverパターンを定義しているrplファイルの中では semverは以下のように定義されています。

local alias numeric = [0] / { [1-9] [:digit:]* }

major = numeric
minor = numeric
patch = numeric 

-- prerelease,buildmetadataの定義は省略...

semver = { major "." minor "." patch {"-" prerelease}? {"+" buildmetadata}? }

このようにaliasや各パターンを個別に定義して、連結させていくことでパターンを定義しています。

キャプチャ

パターンの書き方に入る前に、rosieは入力に対してどのようにパターンをマッチさせているのか?を見るために、 semverの中でminorバージョンだけ取り出したい!といったケースを考えてみます。

正規表現ではcaptureを使いますが、rosieではマッチした情報を以下のように出力することが可能です。

$ echo "1.2.3-alpha" | rosie match -o jsonpp ver.semver
{"s": 1,
 "e": 12,
 "type": "ver.semver",
 "subs":
   [{"s": 1,
     "e": 2,
     "type": "ver.major",
     "data": "1"},
    {"s": 3,
     "e": 4,
     "type": "ver.minor",
     "data": "2"},
    {"s": 5,
     "e": 6,
     "type": "ver.patch",
     "data": "3"},
    {"s": 7,
     "e": 12,
     "type": "ver.prerelease",
     "data": "alpha"}],
 "data": "1.2.3-alpha"}

余談: ところで rosie greprosie match は行内でもマッチするか行頭からマッチするかの違いがありそうです。 例えば"xxx1.2.3-alphabbb"rosie grep ではマッチしますが、 rosie match ではマッチしません。 実装的には rosie grep patrosie match findall:pat になりそうです。(多分) https://gitlab.com/rosie-pattern-language/rosie/blob/d861ffd5805f9988d9ad430e7f124216f11df44e/src/lua/builtins.lua#L98

キャプチャの話に戻りますが、json内にあるtypeに一致したpatternの情報が入っているので、あとは適当に取り出してやれそうです。

$ cat vers
1.2.3-alpha2.4
12.0.5
1
2.3.4
xx1.2.3mmm

$ cat vers | rosie grep -o json 'ver.semver'
{"type":"*","s":1,"subs":[{"type":"ver.semver","s":1,"subs":[{"type":"ver.major","s":1,"e":2,"data":"1"},{"type":"ver.minor","s":3,"e":4,"data":"2"},{"type":"ver.patch","s":5,"e":6,"data":"3"},{"type":"ver.prerelease","s":7,"e":15,"data":"alpha2.4"}],"e":15,"data":"1.2.3-alpha2.4"}],"e":15,"data":"1.2.3-alpha2.4"}
{"type":"*","s":1,"subs":[{"type":"ver.semver","s":1,"subs":[{"type":"ver.major","s":1,"e":3,"data":"12"},{"type":"ver.minor","s":4,"e":5,"data":"0"},{"type":"ver.patch","s":6,"e":7,"data":"5"}],"e":7,"data":"12.0.5"}],"e":7,"data":"12.0.5"}
{"type":"*","s":1,"subs":[{"type":"ver.semver","s":1,"subs":[{"type":"ver.major","s":1,"e":2,"data":"2"},{"type":"ver.minor","s":3,"e":4,"data":"3"},{"type":"ver.patch","s":5,"e":6,"data":"4"}],"e":6,"data":"2.3.4"}],"e":6,"data":"2.3.4"}
{"type":"*","s":1,"subs":[{"type":"ver.semver","s":3,"subs":[{"type":"ver.major","s":3,"e":4,"data":"1"},{"type":"ver.minor","s":5,"e":6,"data":"2"},{"type":"ver.patch","s":7,"e":8,"data":"3"}],"e":8,"data":"1.2.3"}],"e":8,"data":"xx1.2.3"}

cat vers | rosie grep -o json 'ver.semver' | jq -r '.subs | map(.subs | map(select(.type=="ver.minor") | .data))[][]'
2
0
3
2

※ 先程の例では-o jsonppでしたがpretty printする必要もないので-o jsonとしています。

少しjqが複雑ですが、ここまで情報があればpatch以降を切り落とした上で、patchバージョンをincrementしたsemerを作ることも可能です。

$ cat vers | rosie grep -o json 'ver.semver' | jq -r '.subs | map(.subs | map(select(.type=="ver.major")))[][] as $major | map(.subs | map(select(.type=="ver.minor")))[][] as $minor |map(.subs | map(select(.type=="ver.patch")))[][] as $patch | $major.data + "." + $minor.data + "." + ($patch.data | tonumber | .+1 | tostring)'
1.2.4
12.0.6
2.3.5
1.2.4

language support

と、ここまでいくとjqが複雑すぎますが、なんとrosieはCやgo, haskellpythonバインディングがあるので解析結果を各種プログラミング言語で扱うことができます。

goだとこんな感じになります。 https://gitlab.com/rosie-community/clients/go/blob/18b09df4802ba6018bf816cf05a48ed777396c40/src/rtest/rtest.go#L113-121

本格的にやる場合は使えそうですね。

さらに余談: gronというツール(JSONをgrepしやすくするコマンドラインツールgronの紹介 - Qiita)を使うという手もあります。*3

node -i -e "$(cat vers | rosie grep -o json 'ver.semver'  | gron)"
> let major = json.subs[0].subs.find(s => s.type == "ver.major").data;
> let minor = json.subs[0].subs.find(s => s.type == "ver.minor").data;
> let patch = json.subs[0].subs.find(s => s.type == "ver.patch").data;
> `${major}.${minor}.${Number(patch) + 1}`;
"1.2.4"

このあと、パターン定義や実行、テストについても書きたかったんですがボリューミーになってしまったので一旦ここで区切ります。

次回は Rosie Pattern Languageに入門(2) - だいたいよくわからないブログ

*1:オプションにはsubs, data, jsonpp, byte, bool, color, json, lineが指定できます。

*2:なのでJSONのような再帰的な構造にもマッチできるわけですね https://gitlab.com/rosie-pattern-language/rosie/blob/master/rpl/json.rpl

*3:というかgoでやるかーと思ったけどgronすればいいなと思ってgoでやるのをやめたレベル