Rosie Pattern Languageに入門(3)

正直第2回で終わる予定だったんですが Rosie Pattern Languageに入門(2) - だいたいよくわからないブログ の続きです。

前回サンプルファイルのsemverをパースしてみたりしたと思うんですが、そんなにしっくり行く例でもないなと感じていたり、 かといってがっつりパースしたくなるログも特にないなーと思っていたりしました。

ところが、さっき何気なく自宅macbrew upgrade コマンドでいろいろなライブラリをがっつりアプデしていたところ ちょうどよい感じのログが出ていました。*1

==> Upgrading 100 outdated packages:
llvm@6 6.0.1 -> 6.0.1_1, ocaml 4.07.0_1 -> 4.07.1, git-lfs 2.5.2 -> 2.7.2, pyenv 1.2.8 -> 1.2.11, tig 2.4.1 -> 2.4.1_1, plantuml 1.2018.12 -> 1.2019.6, tree 1.7.0 -> 1.8.0, terraform 0.11.10 -> 0.12.1, php@7.1 7.1.23 -> 7.1.30, coreutils 8.30 -> 8.31, cppcheck 1.85 -> 1.87, wget 1.19.5 -> 1.20.3_1, ocaml-num 1.1_3 -> 1.1_4, mycli 1.16.0 -> 1.19.0, python@2 2.7.15_1 -> 2.7.16, go 1.11.2 -> 1.12.5, docker-machine 0.15.0 -> 0.16.1, ponyc 0.25.0 -> 0.28.1, gnu-getopt 1.1.6 -> 2.33.2, cmake 3.12.3 -> 3.14.5, opus 1.3 -> 1.3.1, freetype 2.9.1 -> 2.10.0, python 3.7.0 -> 3.7.3, elixir 1.7.4 -> 1.8.2, redis 5.0.0 -> 5.0.5, highlight 3.47 -> 3.52, boost 1.67.0_1 -> 1.69.0_2, cscope 15.8b -> 15.9, libyaml 0.2.1 -> 0.2.2, docker-completion 18.06.1 -> 18.09.6, jemalloc 5.1.0 -> 5.2.0, maven 3.5.4 -> 3.6.1, jo 1.1 -> 1.2, wata727/tflint/tflint 0.7.2 -> 0.8.2, shared-mime-info 1.10 -> 1.12, unzip 6.0_3 -> 6.0_4, zeromq 4.2.5 -> 4.3.1_1, composer 1.7.3 -> 1.8.5, perl 5.28.0 -> 5.30.0, nkf 2.1.4 -> 2.1.5, rust 1.30.0 -> 1.35.0, zsh-completions 0.29.0 -> 0.30.0, readline 7.0.5 -> 8.0.0_1, erlang 21.1.1 -> 22.0.2, llvm 7.0.0 -> 8.0.0_1, awscli 1.16.40 -> 1.16.170, teleport 3.0.0 -> 3.2.6, binutils 2.31.1_1 -> 2.32, gcc 8.2.0 -> 9.1.0, crystal 0.26.1_1 -> 0.29.0, webp 1.0.0 -> 1.0.2, ruby-build 20181019 -> 20190423, sqlite 3.25.2 -> 3.28.0, gobject-introspection 1.58.0 -> 1.60.1, eigen 3.3.5 -> 3.3.7, yarn 1.12.1 -> 1.16.0, numpy 1.15.3 -> 1.16.4, grep 3.1 -> 3.3, emacs 26.1_1 -> 26.2, docker-compose-completion 1.23.0 -> 1.24.0, moreutils 0.62 -> 0.63, thrift 0.11.0 -> 0.12.0, gradle 4.10.2 -> 5.4.1, llvm@3.9 3.9.1_1 -> 3.9.1_2, ansible 2.7.1 -> 2.8.1, llvm@5 5.0.2 -> 5.0.2_1, leiningen 2.8.1 -> 2.9.1, openblas 0.3.3 -> 0.3.6_1, curl 7.62.0 -> 7.65.1, ghc 8.4.4 -> 8.6.5, opencv 3.4.3 -> 4.1.0_2, gnu-sed 4.5 -> 4.7, consul 1.3.0 -> 1.5.1, llvm@4 4.0.1 -> 4.0.1_1, ghq 0.8.0 -> 0.12.6, source-highlight 3.1.8_9 -> 3.1.8_11, nginx 1.15.5 -> 1.17.0, gawk 4.2.1 -> 5.0.0, hadolint 1.15.0 -> 1.16.3, haskell-stack 1.7.1 -> 1.9.3, mercurial 4.8 -> 5.0.1, shellcheck 0.5.0 -> 0.6.0_1, clisp 2.49_1 -> 2.49_2, oniguruma 6.9.0 -> 6.9.2, httpie 0.9.9_4 -> 1.0.2, groovy 2.5.3 -> 2.5.7, node 11.0.0 -> 12.4.0, zsh 5.6.2_1 -> 5.7.1, neovim 0.3.1 -> 0.3.7, ripgrep 0.10.0 -> 11.0.1, coq 8.8.2 -> 8.9.1, fish 2.7.1 -> 3.0.2, hub 2.6.0 -> 2.11.2, bison 3.2 -> 3.4.1, protobuf 3.6.1 -> 3.7.1, rbenv 1.1.1 -> 1.1.2, git 2.19.1 -> 2.22.0, ctop 0.7.1 -> 0.7.2, ethereum/ethereum/ethereum 1.8.17 -> 1.8.27, gnutls 3.5.19 -> 3.6.8, diff-so-fancy 1.2.0 -> 1.2.5, libvterm 681 -> 726, packer 1.3.2 -> 1.4.1, flyway 5.2.1 -> 5.2.4, pony-stable 0.1.6 -> 0.2.0

一行で出るとちょっと読みづらい感じとか前後のバージョンを取り出すのとか難しそうな感じが、マシンリーダブルではないけどちょっと頑張るとマシンリーダブルになりそうないい塩梅のログになっていそうです。

普段なら以下のように改行を突っ込んでヒューマンリーダブルにするところで終わりにしちゃうんですが、今回はパースしてマシンリーダブルにしたいと思います。

$ cat brewout | gsed 's/, /\n/g'
cat brewout | gsed 's/, /\n/g' | head
llvm@6 6.0.1 -> 6.0.1_1
ocaml 4.07.0_1 -> 4.07.1
git-lfs 2.5.2 -> 2.7.2
pyenv 1.2.8 -> 1.2.11
tig 2.4.1 -> 2.4.1_1
plantuml 1.2018.12 -> 1.2019.6
tree 1.7.0 -> 1.8.0
terraform 0.11.10 -> 0.12.1
php@7.1 7.1.23 -> 7.1.30
coreutils 8.30 -> 8.31

といっても簡単なもんで以下のようにするだけです。

package brew

-- test package accepts "llvm@6", "ocaml", "php@7.1"
package = [[:alnum:] [@.] ]+

local alias brewver = { [:digit:]+ "." [:digit:]+ "." [:digit:]+ [^ [:space:] [,] ]* }

-- test before accepts "6.0.1", "23.0.1_1", "4.07.0_1", "7.1.30"
-- test after accepts "6.0.1", "23.0.1_1", "4.07.0_1", "7.1.30"
before = brewver
after = brewver

-- test brew accepts "llvm@6 6.0.1 -> 6.0.1_1", "ocaml 4.07.0_1 -> 4.07.1,", "php@7.1 7.1.23 -> 7.1.30,"
-- test brew rejects "12.01 -> 2.3.4",
brew = package before "->" after ","?

brewに登録されるパッケージのバージョンは厳密なsemverとは異なり07や0_1のような文字列が利用されるので標準のver.semverは使わず空気を読んでパースしています。(brew公式docとか見てないので今回のでパースできない例はいっぱいありそう)

llvm@6php@7.1, 23.0.1_1 など 怪しい部分は都度テストに突っ込むとデグレチェックにもなるので便利です。(実際php@6.1をパースしようとして [@.] とすべきところを [@ .] としてしまいスペースもpackageに飲み込まれるというデグレが起きるのを検知しました。)*2

テストは以下のように行います。

$ rosie test brew.rpl
brew.rpl
    All 15 tests passed

肝心のパースは以下のようにできます

 $ cat brewout | rosie  -f brew.rpl match -o jsonpp 'findall:brew.brew'
{"type": "*",
 "subs":
   [{"type": "brew",
     "subs":
       [{"type": "package",
         "s": 1,
         "data": "llvm@6",
         "e": 7},
        {"type": "before",
         "s": 8,
         "data": "6.0.1",
         "e": 13},
        {"type": "after",
         "s": 17,
         "data": "6.0.1_1",
         "e": 24}],
     "s": 1,
     "data": "llvm@6 6.0.1 -> 6.0.1_1,",
     "e": 25},
    {"type": "brew",
     "subs":
       [{"type": "package",
         "s": 26,
         "data": "ocaml",
         "e": 31},
        {"type": "before",
         "s": 32,
         "data": "4.07.0_1",
         "e": 40},
        {"type": "after",
         "s": 44,
         "data": "4.07.1",
         "e": 50}],
     "s": 26,
     "data": "ocaml 4.07.0_1 -> 4.07.1,",
     "e": 51},
...(snip)
}

ASTがあるのでpackageだけ取り出すやらなんやらは自由にできます。

$ cat brewout | rosie  -f brew.rpl match -o json 'findall:brew.brew' | jq -r '.subs | map(.subs) | flatten | map(select(.type == "package") | .data)[]'
llvm@6
ocaml
lfs
pyenv
tig
...

という感じで、日常にあるちょっとだけ取扱に困るログを取り扱ってみる回でした。

*1:なんかllvmがたくさん入ってるとか、こんなに一気に上げて大丈夫なのかとかはスルーで

*2:local のテストはかけるけどlocal aliasのテストがかけないような気がするけどもしかしたら書けるかも。