マークダウンベースのタスクランナー saku を作った話


tl;dr

  • make をタスクランナーとして使うことに疲れた (.PHONY ってもう書きたくない)
  • yaml ベースのタスクランナーは幾つかあったがしっくり来ない (go-task / robo)
  • そもそもタスクの記述言語をマークダウンにした方が良いのでは?
  • マークダウンベースのタスクランナーを作った! saku! (update: Go言語バージョンも作った!)

モチベーション

自分は普段、趣味も仕事もほとんど JavaScript ばかり書いているため、タスクランナーとしては npm run-script を使っています。 run-script はタスクのドキュメントが書けないとか、タスクにコメントをつけれないとか(JSON の文法の制約上)、不満点は色々ありますが、npm-run-all などの支援ツールを利用することで、必要最低限のタスク定義は十分にできるツールと個人的には思っています。

とあるプロジェクトで terraform スクリプトのレポジトリを作ることになったのですが、terraform のベストプラクティスによれば、 Makefile に terraform コマンドを書くことがベストプラクティスとされているようです。確かに terraform はコマンドとしては長すぎなので、タスクランナーを使うというのは良いと思いますが、2018年に Makefile はどうなんだという気がしました。謎のディレクティブ .PHONY をまた書くのかと思うと辛いです。そもそも make はタスクランナーとして作られたツールではなく、コンパイル言語をコンパイルすることに特化されたシステムなので、それをタスクランナーとして使うこと自体がハックです。そこで、もう 2018年なので、タスクランナーとしての make をリプレースする何かが存在するはずだと信じてググり始めました。

go-task

幾つかのツールを見つけましたが、もっとも自分の要求に近そうなツールは go-task でした。go-task は yaml にタスクを定義していくシンプルなツールです。

でも何かが違うと感じました。それは yaml の中のキーです。 cmds, desc などのキーにタスクのコマンドと説明を書くのですが、これらのキーが "ノイズ" に感じるのと、キーの名前自体に恣意的な印象を受けました。

go-taskのタスク例
tasks:
  build:
    cmds:
      - go build -v -i main.go

  assets:
    cmds:
      - minify -o public/style.css src/css

Makefile では、コマンドはただのインデントで表されていて、cmds のような "ノイズ" はありません。これだったら Makefile の方がスッキリ書けるのではないか。make 置き換えを検討しているのに、書き方が逆にまどろっこしくなるのでは、本末転倒です。

同じものをmakefileで書いた例
build:
    go build -v -i main.go

assets:
    minify -o public/style.css src/css

Think syntax

yaml でもっと別な syntax は組めないかと考えてみましたが、yaml という特性上、あるオブジェクトの中身を記述する際には key: value という形が必須になってしまうため、go-task のデザイン以上にシンプル化は出来ないと感じました。

自分の知っている syntax を幾つか考慮した結果、コマンドをクオートなしの裸の状態で書ける文法でかつ yaml のように key: value のような構造を省ける記法を持った言語というとマークダウンしかないという結論になりました。

Markdown-based task runner

マークダウンでタスク定義ができるタスクランナーをググりましたが見つかりません。であれば作るしかないということで saku を作りました。

マークダウンのパーサは最近だと remark が元気が良さそうに見えたため remark を選択。CLI のベース部分は以前 bulbo, kocha などの為に作った自分のツールチェインがあるのでその辺を流用しました。

プログラムの対象はタスクランナーということで、Task というモデルクラスがひとつだけ存在して、それをベースに DDD のビルディングブロック (Factory など) で周りを固めていくとあとは自然につなぎ部分をコーディングしていって実装完了。node ecosystem 万歳 🙌 DDD 万歳 🙌

Dogfooding

12コミット目から saku の開発に必要なタスクを saku.md で記述してドッグフーディングを開始しました。

# test
> Runs unit tests

    npx kocha --require src/__tests__/helper src{/,/**/}__tests__/*.js

# lint
> Runs lint checks

    npx standard

# fix
> Runs auto fixer of lint tool

    npx standard --fix

# cov
> Makes coverage reports

    npx nyc --reporter=text-summary --reporter=lcov saku test

# codecov
> Posts reports to codecov.io

    saku cov
    npx codecov

ドッグフーディングしてみると、思った通りタスクが記述しやすいと感じたのと、また、saku.md を github の機能でマークダウンとしてレンダリングした時の見栄えの良さが予想外の発見でした。

ロードマップ

  • 今後できれば windows サポートをしたいと思っています。 -> windows 対応しました。
  • npm-run-all のような saku -p build:* のようなワイルドカードタスク実行が欲しいです。

感想

  • 0 から何かを作るのは楽しい!
  • マークダウンは、yaml のような自然な論理的構造を持って居ないですが、逆にクオートなしで書ける値のバリエーションが多く、マークダウン AST に任意の意味付けを与えることで、非常に記述がしやすく可読性の高い DSL を作れる可能性を秘めた syntax だと感じました。