なぜちょっとした作業をやるときにMakefileを書いてしまうのか

はじめに

なぜちょっとした作業をやる時にMakefileを書きたくなってしまうのかということについて頭を整理してみることにした。

ちょっとした作業?

その前にちょっとした作業とは何かということについて触れることにする。ちょっとした作業というのはぱっと見た感じで「一瞬でしょ」と思うような作業。具体的には以下のようなもの。

件の記事は、特定のユーザーが持つrepositoryをcloneしていきたいというかなり単純な作業。このような作業を行うのにMakefileを書いてしまうというお話。

本当にちょっとした作業?

ちょっとした作業というつもりで始めたものであっても結構手間取ることがある。先程の全てのrepositoryのcloneを行うという所でも幾つかハマり所があるように思う。もう少しタスクを詳細化すると以下の様な形になる。

  1. 特定のユーザーの持つrepositoryの一覧を取得する方法を探す
  2. 特定のユーザーのrepositoryの情報をAPI越しに取得してみる
  3. APIのresponseを使いやすい形に整形する
  4. (gitで実行可能なシェルスクリプトの形式に変換して出力する)
  5. 全てをつなげて実際に全てのrepositoryをcloneする

この中でハマりどころはAPIのresponseを整形する部分とAPIを呼ぶ部分。全てのことが頭の中に入っていたら一回でできるのかもしれないけれど。そんなことはまずなくドキュメントを見ながら試行錯誤をすることになる。

試行錯誤をするということは幾つかの候補からあれでもないこれでもないと幾つか試してみて上手くいったものを採用するということ。ここでの試行錯誤が手軽であればちょっとした作業と言えるかもしれないし。そうでないのならばちょっとした作業ではないと言えるのかもしれない。

例えば、「コマンドのオプションをタイポしてしまった。修正後再実行した」みたいな作業はちょっとした作業と言えるかもしれない。

記事中で最終的に見つかったワンライナーはそこそこ複雑なもののように見える。そして一番のポイントとして実行結果を確認してからでなければ次の作業が(本来的には)できないあるいはやりづらいものがあるように思える。具体的にはAPIのresponseを整形する部分。APIから返ってきたresponseの内容を確認してからようやくどのように整形するか見当がつき整形用の記述を追加するといったように。

試行錯誤をやりやすく

一時ファイルがほしい

さて、APIから何らかの情報をとることができた。とは言えどのようなresponseの構造になっているか把握するのがめんどくさい。どこかに一時的に保存しておくと便利。

$ http -b <url> | tee data.json

data.jsonというファイルに一時的に保存してくことにする。lessなどで中を覗いてみよう。

$ less data.json

ここで中の構造を確認する(あるいはドキュメント中にresponseサンプルが用意されていることもあるかもしれない)。欲しい情報はX,Y,Zのどれかに含まれているかもしれない。ここで先程の保存していたファイルを見ることができれば以下で済む。

整形用の記述を追加する。実行して上手くいくか確認したい。

# 何らかの整形用の記述
$ jqfpy '[(d["X"],d["Y"],d["Z"]) for d in get()]' data.json

一時的に保存したファイルに対して実行してみて上手くいくことが確認できた。

一方、一時ファイルにresponseの結果を保存していなかった場合には何度も何度もAPIを叩くことになる。パイプでつなげてlessで確認してというのは馬鹿馬鹿しい。シェルの履歴から結果の確認をして、確認ができたらまたシェルの履歴から整形用のコマンドを調整してを繰り返すという感じになる。

(別の方法もある。複数の端末を同時に開いて(タブでターミナルを開いたり、screenやtmuxを使ったり)2つの端末を行ったり来たりしながらやってみるという方法もある)

何らかのAPIを叩いてその結果のresponseを見比べながら作業をするという時には、結果を一時ファイルに保存しておくと良い。

そんなわけで、全体の実行を毎回繰り返して確認するのが面倒なら、部分部分で個別に実行して上手くいったかどうか確認したい。

その一時ファイルは信用できる?

一時ファイルがあれば、データの整形をするのにその都度毎回APIを呼び出す必要がなくなる。手軽だし早い。一方で整形用のコマンドが判明した段階でふと立ち止まって考えた時に、その一時ファイルは本当に正しいのか?という疑問が出てくる。

とくに整形作業の手軽さのために入力を整えたりちょっとした変更をエディタでやってしまった場合など。一時ファイルから望んだ形の整形ができたとしても、元の入力(ここではAPIへのrequestのURL)から期待した結果(ここでは整形後の形)が取り出せるか不安になってしまう。

不安を解消するには全部を繋げなくてはいけない。そのためにまたシェルのコマンドの履歴から取り出して一個一個順に実行というのはちょっとだるい。人間はミスをする生き物で自分は人間であるからミスをする。実行するコマンドの順序を間違えてしまったりでイライラをつのらせてしまうということがあったりする。

そんなわけで、部分部分で試行錯誤して上手くいったのなら全体をつなげて確認したい。

他の入力に対しても試してみたくなったりしない?

さて上手く実行できそうなことを確認したなら、他の入力でも試してみたくなる。元の記事の内容があるユーザーが保持するrepositoryのcloneであるなら、対象のユーザーを件の記事のjupyterから自分などにしてみたり。

先程までの内容で全部を1つのスクリプトにしているのなら、引数を変えてあげるだけで済む。ところで今までの作業をスクリプトにしてみるのはどれだけ大変なんだろうか?あるいはそのスクリプトと今まで試してみた結果が同じものである保証はどれだけあるのだろうか?

そんなわけで、全体にちょっとした修正を加えるのにrewriteのようなことはしたくない。

欲しいのはコマンド?それとも環境?

できたのがコマンドなら(件の記事ではワンライナー)それを実行すれば、やりたかった作業それ自体はできる。それ自体には不満もないのだけれど。

作ったコマンドの作成時に得た知識を使って他の類題を解くというようなことがしたくなることがある。それもある程度の時間が経過した後に。

そのような時に、手軽に過去に知り得た知識を上手く取り出すことことができるかどうか。それも定かではなくなった記憶から。おそらく難しく、それも完成したワンライナーを解きほぐして途中の状態をもう一度得てようやくと思うとやるのもめんどくさくなってくる。

(例えば、自分自身のrepositoryについてスター数でソートして、名前とスター数の一覧がほしいというような類題)

一方その時に試行錯誤をした環境が手に入るのなら始めから分割された状態になっているはず。あるいはちょっといじって途中部分の結果を拝借みたいなことも手軽にできるはず。

そんなわけで、環境が手に入るのならその環境内でちょっとした作業を行えば良い感じの知見が手に入る。手に入るのがコマンドなら中を解きほぐして理解をもう一度し直すというようなことをしなければいけない。

それをMakefile

今の所、自分自身の手持ちのカードでこれらの要件を満たすためにMakefileを利用しているということらしい(Makefileにこだわる必要はないかもしれない。それに良い代替があるのなら移行したいという気持ちもある)。

データの取得

データの取得部分は以下の様な感じでする。対応するMakefileは以下のようなもの。fetchタスクでデータを取得できるようにする。

TARGETNAME ?= jupyter
URL := https://api.github.com/users/${TARGETNAME}/repos

fetch: $(addsuffix .json,$(addprefix data/,${TARGETNAME}))

data/${TARGETNAME}.json:
    mkdir -p data
    http -b --pretty=format ${URL} sort==updated direct==desc per_page==100 | tee $@

これでmakeを叩くたびにAPIアクセスが行われて結果がdata/jupyter.jsonに格納される。

実際に実行されるコマンドが見たいなら -n 付きでmakeを実行する。

$ make -n
mkdir -p data
http -b --pretty=format https://api.github.com/users/jupyter/repos sort==updated direct==desc per_page==100 | tee data/jupyter.json

実行結果はキャッシュされるので毎回待つ必要もない。新規に取り直した買ったら -B 付きでmakeを実行する。

$ make fetch
make: Nothing to be done for 'fetch'.
$ make fetch
make: Nothing to be done for 'fetch'.
$ make fetch -B

データの整形

データの整形もMakefile上に書く。parseなどというタスクを作ることが多い。例えばAPIのresponseが複雑だった場合に必要なものだけを取り出したような出力を作ることがある。

parse: data/${TARGETNAME}.json
  jqfpy '[h.pick("html_url", "homepage", "stargazers_count", "updated_at", d=d) for d in get()]' $^ --squash | tee data/parsed.json

data/jupyter.parsed.json 整形部分のコマンドの実行を繰り返したい場合にはmakeを実行しまくる。ある程度良さそうということになったら以下の様に変える。

default: parse  # fetchからparseに

parse: $(addsuffix .parsed.json,$(addprefix data/,${TARGETNAME}))
data/${TARGETNAME}.parsed.json: data/${TARGETNAME}.json
    jqfpy '[h.pick("html_url", "homepage", "stargazers_count", "updated_at", d=d) for d in get()]' $^ --squash | tee $@

全てをつなげて実行

defaultで全てをつなげて実行できるようにしておく。例えばparseがタスクの依存関係の末端ならdefaultをparseにする。

引数の追加

引数の追加はTARGETNAMEの部分。環境変数経由で実行時に上書きすることができる。

TARGETNAME ?= jupyter
URL := https://api.github.com/users/${TARGETNAME}/repos
$ make
# 自分自身のものに変更
$ make TARGETNAME=podhmo

類題を解くのに整形を変える

Makefileをコピーしてparseを修正しても良いし。fetchで取り出した結果を使うparse2のようなタスクを作ってみても良い。複数のファイルを持った1つのプロジェクトを作ってしまうと全体のコピーが面倒になるし。ちょっとした手軽な複数のタスクを1つのファイルで持ち運びできるという点でMakefileは便利。

Makefileファイルではダメな場合

ただMakefileでダメな場合もある。それは実行にMakefileのダウンロードが必要な点。これが面倒。例えば件の記事のワンライナーだけが欲しいという場合にちょっと不便。 (そういう意味ではワンライナーを記事に貼るというのは一種のdeploy作業と言えなくもないのかもしれない)

例えば、全部をつなげた結果のワンライナーは以下のようなmakefileのタスクになる。

TARGETNAME ?= jupyter
URL := https://api.github.com/users/${TARGETNAME}/repos
BASH ?= cat

clone:
  http -b --pretty=format ${URL} sort==updated direct==desc per_page==100 | jqfpy '[f"""git clone --depth=1 {d["ssh_url"]} """ for d in get()]' --squash -r | ${BASH}

もちろんこれをコピペしてはダメで。 make -n clone の結果をコピペすることになる。

$ make -n clone BASH=bash
http -b --pretty=format https://api.github.com/users/jupyter/repos sort==updated direct==desc per_page==100 | jqfpy '[f"""git clone --depth=1 {d["ssh_url"]} """ for d in get()]' --squash -r | bash

(デフォルトでは実行せずcatなどするだけにしておくという形にしていることが多い。dry-run状態)