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で行数を絞っています
URLだけ欲しいという場合はoutputオプションとしてsubsを指定します。*1
$ curl -s https://github.com | rosie grep -o subs net.url
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 grep
と rosie match
は行内でもマッチするか行頭からマッチするかの違いがありそうです。
例えば"xxx1.2.3-alphabbb"
は rosie grep
ではマッチしますが、 rosie match
ではマッチしません。
実装的には rosie grep pat
は rosie 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, haskellやpythonバインディングがあるので解析結果を各種プログラミング言語で扱うことができます。
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"
このあと、パターン定義や実行、テストについても書きたかったんですがボリューミーになってしまったので一旦ここで区切ります。