作ってそこそこ便利だったボットとかまとめ

FOLIO Advent Calendar 2019 - Qiita 10日目代打です。

9日目はlotzさんの 挿入ソートと選択ソートは双対 - Qiitaでした。 recursion schemesの応用って融合変換くらいしか知らなかったんですが身近なアルゴリズムの性質を表現できるのは面白いですね。 自分で学ぶことで見通しよく効率化のアイディアを出せたりad-hocに見えていたアルゴリズムに横のつながりが見えてきて理解が深まりそうです。

最近ブログ書かなさすぎて書き方を忘れていた まっちゃら (@matsu_chara) | Twitterです(´・_・`)

今回は今まで作ってきたボットや社内用サービスをまとめようと思います。 やや内輪ネタっぽいですが、こういうのあると便利かもしれないというアイデアのきっかけになればと思います。 似たようなのあるけどうちはもっと便利だぜ!っていうのあったらぜひ教えて下さい。

社内ブックマーク gol

新しく入社したメンバーに、consulのurlはこれで、prometheusはこれで、ああサービス画面はこっちで。 と教えることはよくあると思いますが、それぞれのリンクを覚えるのは結構大変だったりします。

ドキュメント管理ツールにブックマーク集みたいなページを追加しても慣れてる人は使わないからメンテされない。なんてこともよく起こります。

golはそれらの問題を解決するブックマーク共有&検索サービスです。
まあ実際はただの管理画面付きkvsなんですが、結構便利だったりします。

例えば画像のように、consulと検索するだけで各環境のconsulがすぐに出てきたり

f:id:matsu_chara:20191214160516p:plain:w450

chrome拡張(これは同僚が作ってくれました!)を使うとgolのページに行かなくてもcommand + gとかでさっと検索できます。

検索キーワードをかなり雑に指定してもヒットするようになっていて、 space区切りで適当に「cons stg」と入れれば「consul_stg」だろうが「stg-consul」だろうがヒットしてくれるのが割と気に入っています。 こういう規約とか覚えるの面倒ですしね。

f:id:matsu_chara:20191214160846p:plain:w450

ちなみにgoを書き始めて初めて作ったものなので実装はあれな感じになっています。 許して

slack_user_avatar_emojis

GitHub - matsu-chara/slack_user_avatar_emojis: register slack user avatars to emoji

slackの全ユーザーのiconを取得して、slackのemojiとして登録するものです。 本文に含めてもいいですし、emoji reactionにしても良しで社内の文化作り的にも良いです。 emojiだと名字よりもちょっとほんわかしますしね。

アイコン変更イベントを取ってきて自動で更新し続けてくれるデーモンにしようと思って100年くらいたってるので誰かお願いします(他力本願)

というかこのemoji、公式で実装されて欲しいですね。

ちなみにトークン設定とかが結構必要なので動かすのは結構ダルめです(´・_・`)

reacjira

emoji reactionをつけると特定のチャンネルに通知してくれるreacji-channelerにインスパイアされた、 emoji reactionをつけると特定のjira projectにチケットを切ってくれるボットです。

reaction + jira で reacjiraってわけですね。

アイコンはいらすとやで見つけたかわいいクジラです。(ダジャレ・・・)

↓の感じで、 :○○_story: みたいなemojiをつけてやるとチケットが作成されスレッドとして投稿されます。

f:id:matsu_chara:20191214162615p:plain:w450

この際、チケットの中身として↓の画像のようにdescription欄にslackのスレッドのリンクがデフォルトで記載されます。

f:id:matsu_chara:20191214162623p:plain:w450

reacjiraを使うと依頼を受けた際にすぐチケットを作成できて以下のようなメリットが生まれます。

  • 頼んだ人: どのチケットを見れば進捗が分かるのか分かりやすい
  • 頼まれた人: 詳細を書くのを忘れてしまっても、slackリンクがあるのでどういう文脈で作られたチケットなのか最悪分かる*1

一説によるとzappierとかでできるらしいという話があったりなかったりするんですが、 自作botだとjiraのあらゆるフィールドをいじったり、結構強引なルールを設定したりできるので便利です。

ちなみにjiraの公式integrationとかが進化してくると要らなくなる存在になりそうです(´・_・`) *2

jobdiff

これはかなり社内限定で便利なやつで、jenkins-job-dsl*3のjenkins反映済みcommit ~ 最新masterまでのcompareリンクを生成してくれるというただそれだけのものです。*4

f:id:matsu_chara:20191214163855p:plain:w450

正直bot化するものでもないんですが、jenkins見る => compareリンク作るみたいなのは手間だったので一発で生成してくれるようにしました。

jenkins叩くコードがすでに手元にあったので5秒くらいで完成したし、botデプロイ環境+token登録環境が割と整備されてるので思いついてサクッと作ってすぐデプロイみたいなことがしやすいんでこういう、微妙〜〜〜に不便ってのも解決できていいですね。(環境整備してくれているチームに感謝)

ところでjenkins-job-dslリポジトリが大きくなりすぎて、job-seedに時間がかかりすぎたりいろいろな問題があるので 脱jenkinsも含めて最高のジョブ基盤を作ろうとしています。

それができたらjobdiff君は消え去ることになります。(宿命)

consul bot

これも社内限定で便利なやつで、サービスディスカバリとして利用しているconsulから情報をとってきて、各環境の各サービスのバージョンを一覧として取ってきたり、ip+portを取ってきたりするやつです。

version stg,prod ac のように書くと「stg,prod環境」にある 「acから始まるサービス」のバージョン情報を取得します。*5

サービス名は省略してもいいしフルネームでも大丈夫です。

f:id:matsu_chara:20191217124557p:plain:w450

デプロイしたあとにちゃんと切り替わってるよね?といった確認に使ったり、トラブルシュートする際に参照したりしています。

consulは今の所困っていないんですがServicMeshとか入ってきた時にうちのconsulが生き残るかは不明です。頑張って(´・_・`)

まとめ

作ったけどボツになったやつとかも結構ある(公式機能で対応されてボツとか、使いにくかったりとか)んですが、 社内でも使ってもらえてるのを見るとちょっと嬉しいです。(新しく入ってきた方が早速使ってたりすると特に)

これからも地味だけどあるのが当たり前みたいな便利さを目指して頑張っていきます。

*1:もちろん書いたほうがいいんですが

*2:っていうか元から要らない感はあるんですがreacjiraっていう名前思いついたら作るしかなくない???

*3:groovyでjenkins jobを定義できるやつ。jenkins Pipelineとは異なるもの。

*4:なんでそんなものが必要なのか?みたいな背景書いたけど、社内でしか伝わらなさそうなので消しました。

*5:バージョンはconsulにregisterにするときにgit tagをconsul Noteに入れることで取得できるようにしています。

PFDS(純粋関数型データ構造)を読んだ

手を動かしたい気分だったのでだいぶ後回しになっていたPFDS (Purely Functional Data Structure, 純粋関数型データ構造)を読みながらScalaで実装を後追いしてみた。*1

少し時間がかかったがRedBlackTreeやTrieといった有名な物から、HoodMelvilleQueueやそれを利用したCatenableListなどちょっとおしゃれ(?)なデータ構造についての実装・計算量についての考え方を学ぶことができた。

本の前半はストーリーがあって、「永続的なデータ構造だと計算効率が悪い場合が多い!・・・が!償却計算量の考え方を使いながら実装を工夫していくと短命なデータ構造と同じくらいの性能が出る。(全てではないが償却だけでなく最悪計算量についても同じくらいにできるケースも多い)」ことを確認する旅になる。 軽くまとめてみたので興味がある人はちら見しつつ、実際に本を購入して読んでみてほしい。 https://gist.github.com/matsu-chara/75dd80e36172569343738153d0123bf2

後半は様々なデータ構造を設計するための考え方がまとまっている。 こちらについても雑多になっているわけではなく、前半の考え方(実装方法・計算量の証明方法)を踏襲しつつ、一個ずつ積み重ねになっているので読み応えがある。

2進数のゼロ無し表現というものが途中に出てくる。 これは以下のように普通の2進数では {0,1}で書くところを {1,2}を使って書く記法のことだ。

1,2,3,4,5 は 1, 2, 11, 21, 12, 22, 111 となる。

- 各桁が2^iを表すのは普通の2進数と同じ。
- 0を使わないのが前提になるので、ある数を表す表現は一位に定まる

一見特に意味のない記法に思えるが、これが記数法表現に基づくデータ構造の計算量の削減に役立つというのはなかなか面白かった。 気になる人は https://gist.github.com/matsu-chara/03fef6b59f60fefd5d052704cd578ac7 に概要をまとめたので読んでみて興味があれば本を買ってみてほしい。(ちなみにgistの内容の正確性については保証できない)

2-3 finger tree

2-3 finger treeといえばHaskellのData.Sequenceにも使われている(償却)計算量の優れたDequeだ。(追加・削除ともに償却定数時間で実行可能)

これはPFDSには載っていないが、ベースとなる 記数法表現暗黙再帰減速 の考え方・実装の勘所はPFDSで学ぶ事ができる。*2

読み終わったあとにwikipediaを開いてみたら、以前なんとなく理解していた物よりはるかに解像度高く理解できたので勢い余って実装してみた。*3

https://github.com/matsu-chara/finger_scala

とはいえ、finger treeの実装は世界に一億個くらいありそうなので特に自慢にもならないし、パフォーマンスやメソッドの豊富さよりはわかりやすさを重視している。(が、やはりこういうのは自分で実装してみると理解度が違うと思う。)

ちなみに実用的なFingerTreeに関してはscalazの物などがおすすめ

wikipediaにあるシナリオ 通りのテストを書いてみたがいい感じな見た目になったので記念スクショを貼って終わりとする

f:id:matsu_chara:20190715172055p:plain
テスト

next

本を読むなかでscalaでの実装をいくつかあたっていたところ以下のように、いくつかの実装には論文へのリンクがついていた。

PFDSでカバーされている物もややありそうだけど面白そうなのでいつか読みたい。(けどパタヘネ読み始めた)

*1:ちなみに実装するときはcats.Evalを使うと便利 https://typelevel.org/cats/datatypes/eval.html

*2:このデータ構造の実装自体は多少腕力があればwikipediaを見れば実装できそうだが、一方でなんでDigitといった名前が出てくるのか、このようなデータ構造での計算量の考え方はある程度背景知識がないと理解が難しい気がする。(個人の感想です)

*3:ちなみにwikipediaは英語版より日本語版のほうが図が多くて実装しやすかった

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のテストがかけないような気がするけどもしかしたら書けるかも。

Rosie Pattern Languageに入門(2)

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

前回の続きです。

前回までだと、semverやらurlなどを取り出せて便利なツールといった印象でしたが、 その真の価値は簡単にパターンを記述できる点にあります。

パターン定義

Rosieでは正規表現ではなくPEGベースの記法でパターンを記述します。 表現力は正規表現を上回りますが、一方で基本は正規表現と同じなので、(完璧に使いこなそうとしなければ)そこまで覚えることは多くないはずです。

パターン定義の方法は公式ドキュメントにあります。 https://gitlab.com/rosie-pattern-language/rosie/blob/d861ffd5805f9988d9ad430e7f124216f11df44e/doc/rpl.md

PEGについては PEG基礎文法最速マスター - kmizuの日記 を読めば最速でPEGれるようになれそうです。

まずは major.minor で構成される劣化semverを作ってみましょう。 https://gitlab.com/rosie-pattern-language/rosie/blob/d861ffd5805f9988d9ad430e7f124216f11df44e/rpl/ver.rpl を参照して少し変えてみましょう。

package myver

-- version like semver but less feature

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

major = numeric
minor = numeric

myver = { major "." minor }

numeric = [0] / { [1-9] [:digit:]* }正規表現でいうと /0|[1-9]\d*/ になります。 0に関する分岐を入れることで 0 にはマッチするけど 012 にはマッチしないといった条件を表現しています。

{} でくくると区切り文字なしでのマッチになります。( [1-9] [:digit:]*"1 2" にマッチ。 { [1-9] [:digit:]* } は"12"にマッチ)((Rosieでは内部で入力をトークナイズしています。そのため "1 2" などにもマッチします。))

preleaseを追加して 1.2-alpha2 などに対応してみると以下のようになります。

prerelease = [[:alnum:]]+

myver = { major "." minor {"-" prerelease}? }

実行

実行時は以下のようにパターンを指定して行います。

$ cat vers
12.01
1
2.3.4
3.52
1.2-alpha2
1.3-b
1.4-

$ cat vers | rosie  -f myver.rpl match -o subs  '{myver.myver $}'
3.52
1.2-alpha2
1.3-b

12.011 , 1.4- などバージョンとしてふさわしくないものを除外できています。

パターンがファイルで管理でき、かつ個別のパターンの組み合わせで新しいパターンが構築できています。 また、aliasキーワードを付けずに宣言したパターンについては -o json の出力で前回のようにマッチした文字列のどこがmajorやminorなのかを把握することができるため、 その後の処理にも使えます。

テスト

先程のように毎回versのようなファイルを作ってtest.shのようなスクリプトを書くのはそこそこ大変です。 Rosieにはdoctestの仕組みがあるのでこれも書いてみましょう。

公式ドキュメントはここにあります。 https://gitlab.com/rosie-pattern-language/rosie/blob/d861ffd5805f9988d9ad430e7f124216f11df44e/doc/unittest.md

書き方は簡単で test pattern accepts/rejects 入力 を書いてやればOKです。

package myver

-- version like semver but less feature

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

-- test major accepts "0", "10", "99"
-- test major rejects "01", "1.2"
major = numeric
minor = numeric

-- test prerelease accepts "alpha1", "1"
-- test prerelease rejects "beta1.2", "1.2"
prerelease = [[:alnum:]]+

-- test myver accepts "3.52", "1.2-alpha2", "1.3-b"
-- test myver rejects "12.01", "1" "2.3.4", "1.4-"
-- test myver includes prerelease "1.2-alpha2"
-- test myver excludes prerelease "1.2"
myver = { major "." minor {"-" prerelease}? }

includes, excludesはmyverでマッチしたときにprereleaseが含まれているかどうか?をチェックします。

あとは以下のようにテストを実行してやればOKです。

$ rosie test myver.rpl
myver.rpl
    All 16 tests passed

パターン自体もわかりやすく、テストには具体例が列挙してある。 これで半年前の自分が書いた意味不明な正規表現から卒業できそうです。*1

今回使ったsampleはここにおいています。 https://github.com/matsu-chara/rosie-example

豆知識

find

.* のような指定があると入力をすべてconsumeしてしまうので、 { .* "something" } のように書きたくなった場合は {find:something} と書きましょう。((findはmacroで、{ !"something" . }* "something" に展開されます。 https://gitlab.com/rosie-pattern-language/rosie/blob/d861ffd5805f9988d9ad430e7f124216f11df44e/doc/i-know-regex.md))

この辺の正規表現との違いは以下のドキュメントに書いてあります。 https://gitlab.com/rosie-pattern-language/rosie/blob/d861ffd5805f9988d9ad430e7f124216f11df44e/doc/i-know-regex.md

look ahead, look behind

look ahead, look behindは以下のように書けます

## look ahead. 先頭がhttpのみを出力
curl -s https://matsu-chara.hatenablog.com | rosie grep -o subs '{ >"http:" net.url }'

## look behind. 末尾が/aboutのみを出力
curl -s https://matsu-chara.hatenablog.com | rosie grep -o subs '{ net.url <"/about" }'

look aheadではパースした位置で、"https://matsu" があるかを確かめ、成功したら(解析する文字の位置は変えずに)そのままnet.urlでのマッチを試みる。 look behindはnet.urlでのマッチに成功したら、(解析する文字の位置は変えずに)戻って "/about" にマッチするかをチェックし、成功したら全体をマッチさせる。 という意味で、正規表現よりわかりやすいんじゃないかな?と思ったりしています。(もちろんこの辺は主観ですが)

ということでRosieの紹介でした。

次回 matsu-chara.hatenablog.com

*1:とはいえ、ワンライナーで使おうとすると少し難しい時があるのでgrepの利点が失われるわけではありません。

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/&quot;,&quot;referrer&quot;:null,&quot;user_id&quot;: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でやるのをやめたレベル