README内部のコマンドを読み込んで実行するツール「I Read U」で、READMEをメンテする習慣を作った


この記事はTypeScript Advent Calendar 2018の7日目です。

作ったもの

README.mdファイル内部に記載されたコマンドを読み込んで実行するツールです。

こんなふうに動きます。

経緯

READMEを書く習慣を作りたい

とあるRuby on Rails(以下Rails)のプロジェクトを開発しようとした時の事でした。

Railsでは、必要なパッケージをインストールするbundle installや、データベーススキーマを適用するrails db:migrateといったコマンドを打ったあと、最終的にbundle exec rails serverというコマンドを実行すると、だいたいサーバが立ち上がります。
いずれも「Railsではよくある」コマンドです。他のWebアプリケーションフレームワークでもだいたい一緒だと思います。

ですが、何らかの事情によりカスタムされたバージョンのコマンドを使わないといけなかったり、特定のseedファイル読み込みが初期起動に必要だったり、その他のドメイン特有の問題で特定のタスクを実行しないと起動しなかったりします。
そういった問題でハマった時に「このコマンドがREADMEに書いてあれば・・・」と思うことが何度かありました。

そこで私は、どんな実験的なプロジェクトでも、必ずREADMEを用意していくことにしました。

最も単純な例:
https://github.com/s2terminal/azure__computer_vision_ocr_sample


# Usage

```
$ bundle exec ruby app.rb
```

READMEのメンテナンス

しかし「READMEをメンテしていくの面倒臭い・・・」と思い始めます。実際に使ったコマンドをわざわざREADMEに書き写していかなければならず、また利用時もREADMEからコピペしていかなければならないからです。

ちょうどそのあたりの時期に私はWebフロントエンドの勉強を始め、npm scriptsというJSONファイルに記載したタスクを実行できる仕組みに感激しました。
Railsでもこういったタスク管理がプロジェクトに含められれば良いなあ・・・と思いVisual Studio Codeのカスタムタスク機能でRailsを起動 しようとしたりもしたのですが、設定の冗長さや環境依存の強さもあり、長続きしませんでした。

そんな中 「READMEにコマンドが書いてあるなら、READMEを実行すれば良いのでは?」 というふとしたアイデアから、開発を始めました。

READMEが「読め」と言っているので、読めば良いのです。

I Read U

$ npm install --global i-read-uすれば使えます。

Linux/Windows Subsystem for Linux/maxOSで動作するCLIアプリケーションです。
README.mdと同じディレクトリで$ i-read-uと打つと、READMEファイル内のコマンドが選択可能になり、Enterを押すとコマンドを実行します。

このようなREADME.mdがあると


```
$ ls
```

こんな感じに。

$ i-read-u
? コマンドを選択してください  ls
LICENSE
README.md
bin
dist
node_modules
package-lock.json
package.json
src
tsconfig.json
webpack.config.js

特徴

  • インライン記法とブロック記法の両方に書かれたコマンドに対応
  • h1~h6の見出しとコマンドとをあわせて表示
  • インタラクティブな操作でコマンド選択
  • -fオプションでファイル指定
  • 日本語対応

使用技術

TypeScriptで開発したCLIアプリケーションです。

MarkdownファイルをmarkedjsでHTMLに変換し、jQueryライクなHTMLパーサのcheeriojsで解析してコマンドを取り出しています。

public static generateFromMarkdownContent(content: string): StringCompiledHTML {
  const html = new this();
  html.string = marked.parse(content);

  return html;
}
public toCommandSections(): Article {
  const $ = cheerio.load(this.string);

  $("*").map((i, e) => {
    const $e = $(e);
    if ($e.is("h1,h2,h3,h4,h5,h6")) {
    // ※以下略

引数管理にはcommander.jsを使っています。

function configureCommander(): IArgv {
  const packagejson: any = require("../package.json");

  commander.option("-f, --file <filename>", "Specify the file name", "README.md");
  commander.version(packagejson.version);
  commander.parse(process.argv);

  return { file: commander.file };
}

抽出したコマンドの選択には、Inquirer.jsを使ってユーザからの入力を受け付けました。

// 注: choiceOne(msg: string, f: (question: inquirer.Question, name: string) => void): void
commands.choiceOne(__("question"), (questionCommand, questionName) => {
  inquirer.prompt([questionCommand]).then(answerCommands => {
    const cmd = child_process.exec(answerCommands[questionName]);
    cmd.stdout.pipe(process.stdout);
    cmd.stderr.pipe(process.stderr);
  });
});

Jestで書いたテストをCircleCIで回しています。

TypeScriptでのCLIアプリケーション開発方法は、拙稿ですが下記記事にまとめています。記事の通り、webpackでビルドしています。
TypeScriptでCLIツールを作って、npmパッケージにする - Qiita

ほとんどすべてがはじめて使う技術でしたが、静的型付けと自動テストがあるおかげでスムーズに開発を進めることができました。
下記拙稿でも触れましたが、Node.jsの豊富なライブラリと静的型付けとは特に相性が良いと感じます。
たのしいTypeScript - Qiita

既知の問題点

仕様上、ユーザからの入力を受け付けるインタラクティブなタイプのコマンドを実行できません。$ npm init$ yum installなど、意味は無いですが$ i-read-u自身にも$ i-read-uは使えません。必要に応じて--yes等の質問をスキップするオプションを併せてREADMEに記載したり、yesコマンドを組み合わせて使ったりしています。

また、現時点では改行を含むコードに対応していなかったり、コマンド以外の<code>タグに書かれたプログラムを読み取ってしまったりと、問題もあります。GitHubにissueとして発行していますが、私自身があまり困らないので修正予定はありません。
Issues · s2terminal/i-read-u

脆弱性

このプログラムはNode.jsを通して任意のシェルコマンドを実行します。つまり文字通りの危険を伴います。

たとえば下記のような 悪意あるREADMEファイル の配置には十分注意してください。

# 行末に意図しないコードが仕込まれている
$ ls -la                                                                                                                                                                             && rm -rf /

特性上、信頼のおける環境下で開発目的だけに利用するべきだと思います。プルリクは歓迎ですが、たいしたコードではないので使うならForkして各自信頼のおけるバージョンをメンテしながら使った方が安心かもしれません。

作ってみてどうだったか

数ヶ月ほど自分で使ってみましたが、思ったよりも便利でした。

READMEをnpm scriptsのように使えるのは楽です。とくに小さなリポジトリを行き来しながら進めるような開発時に、いちいちそのプロジェクトのREADMEを読まなくともアプリケーションの起動コマンドがすぐに打てるので、効果が高いと感じています。

これを使うようになってからREADMEに記載するコマンドのメンテナンスは怠らないようになりました。とくにタイポに厳しくなりました(READMEのタイポ、意外と多かったです)。

一方、README内でもDescriptionやRequirementsなどコマンド以外の内容が充実しないのは、このツールでは解決しませんでした。当たり前ですが。

READMEをメンテする習慣を

READMEはどんな言語のプロジェクトでもだいたいルートディレクトリに置いてあり、GitHub等のリポジトリのページトップに表示されます。たとえばnpm scriptsやVS Codeのタスク機能などに比べ、READMEはメンテナンスの恩恵が受けやすいのが非常に大きいです。
i-read-uを使っていない人でもREADMEは読みますし、今後i-read-uが無くなったとしても書いたREADME.mdは腐りません。

この記事がREADMEを書く一助になりましたら幸いです。