Pythonでタスクランナーを自作しませんか? Pythonは、関数に@デコレータを付けると効果を持たせられるtaskrunner向けの機能に加え、shell実行のためのDSLライブラリもあり、taskrunnerとして使いやすい言語です。ライブラリの数も全言語随一です。
Makefile、Justfile、Taskfileと渡り歩いてきましたが、結局tasks.pyという1ファイルにタスクを関数として書き、chmod +xで実行する自作のtaskrunnerに落ち着きました。
既存のtaskrunnerで感じる不満
Makefile・Justfile・TaskfileはタスクをDSLの制約のなかで書く必要があります。条件分岐、動的な依存関係、型チェック付きの引数バリデーションなど、プログラミング言語なら当たり前にできることが、DSLの表現力の範囲でしかできません。
なぜPythonが自作に向いているか
Pythonはtaskrunnerとして最低限必要な「タスクを名前で登録する」「シェルコマンドを実行する」「タスク間の依存関係を扱う」という機能はもちろん備えていますし、decoratorは、関数の中身を書き換えずに、その関数の前後に処理を足せる仕組みです。この仕組みのおかげで、たとえばcycloptsは関数をCLIコマンドとして登録する処理を、関数の外側から追加できます。さらに、事前・事後条件を保証する契約プログラミングも、icontractのdecoratorを重ねるだけで足せます。
| 機能 | 説明 | 実現方法 |
|---|---|---|
| タスク登録 |
@app.command を付けた関数がタスクになる。docstringがヘルプに表示される |
cyclopts |
| 引数の型変換・バリデーション | CLI経由の呼び出し時に型アノテーションで自動変換・バリデーションされる | cyclopts |
| タスク間の依存関係 | 関数呼び出しで自由に表現できる。条件付き実行や動的な依存も素直に書ける。呼び出し先の関数に付いた @require / @ensure もそのまま発火するため、関数間のバリデーションも自然に連鎖する |
Python / icontract |
| シェルコマンドのパイプ実行 | plumbumの | 演算子でシェルと同じ感覚で書ける |
plumbum |
| 契約プログラミング |
@require / @ensure で事前・事後条件を宣言できる。タスクの前提と保証をコードで強制できる |
icontract |
ライブラリのインストールという手間は、uvのshebang方式で解消できます。#!/usr/bin/env -S uvx --with cyclopts,plumbum,icontract pythonと先頭に書けば、依存ライブラリはuvが隔離環境に自動でインストールするため、事前のセットアップなしにそのまま実行できます。必要な機能をライブラリで組み合わせ、書いてすぐ動かせる専用のtaskrunnerを組み立てられます。
実例:専用taskrunnerを自作する
tasks.py 1ファイルで完結します。build・ci・deploy・audit・cleanは説明用の例です。
実用では本体とタスク記述を別ファイルに分けたり、特定ディレクトリ配下を自動でタスクとして登録したりすることも多いです。以下は本質的な部分のみ抜粋しています。
tasks.py
#!/usr/bin/env -S uvx --with cyclopts,plumbum,icontract python
import icontract
from cyclopts import App
from pathlib import Path
from plumbum import FG, local
from plumbum.cmd import wc
app = App()
_say = local["echo"]
def _echo(message: str) -> None:
_say[message] & FG
# ビルド
@app.command
def build() -> None:
"""プロジェクトをビルドする。"""
_echo("==> build")
# git状態確認
@app.command
def ci() -> None:
"""未コミットの変更を確認する。"""
result = local["git"]["status", "--short"](retcode=(0, 1))
_echo(f"==> {result.strip() or 'クリーン'}")
# デプロイ
@app.command
def deploy(env: str = "staging") -> None:
"""ENV へデプロイする。"""
_echo(f"==> deploy env={env}")
# 行数レポート生成(事前条件: src存在、事後条件: 出力生成)
@app.command
@icontract.require(lambda src: Path(src).exists(), "事前: src が存在する")
@icontract.ensure(lambda result: Path(result).exists(), "事後: 出力が生成される")
def audit(src: str) -> str:
"""SRC の行数を {SRC}.report に書く。"""
n = (wc["-l"] < src)(retcode=(0, 1)).split()[0]
out = f"{src}.report"
Path(out).write_text(f"source={src}\nlines={n}\n")
_echo(f"==> wrote {out} (lines={n})")
return out
# dist/ 削除
@app.command
def clean() -> None:
"""dist/ を削除する。"""
for p in sorted(Path("dist").glob("**/*"), reverse=True):
p.unlink() if p.is_file() else p.rmdir()
_echo("==> clean done")
if __name__ == "__main__":
app()
以下、このtasks.pyを実行し、自作したtaskrunnerとして動くことを実例で示します。
実行
chmod +x tasks.py
./tasks.py # タスク一覧と引数を表示
./tasks.py build
./tasks.py ci
./tasks.py deploy --env production
./tasks.py audit src/main.py
引数なしで実行すると cyclopts が次を出力します。docstringがそのまま説明文になっています。
Usage: tasks.py COMMAND
╭─ Commands ───────────────────────────────────────────────╮
│ audit SRC の行数を {SRC}.report に書く。 │
│ build プロジェクトをビルドする。 │
│ ci 未コミットの変更を確認する。 │
│ clean dist/ を削除する。 │
│ deploy ENV へデプロイする。 │
│ --help (-h) Display this message and exit. │
│ --version Display application version. │
╰──────────────────────────────────────────────────────────╯
まとめ
- Makefile・Justfile・TaskfileのDSLに不満があるなら、そのタスク専用のtaskrunnerをPythonで自作しましょう
- decoratorは関数の中身を変えずに処理を足せる仕組みで、タスク登録(cyclopts)や契約プログラミング(icontract)などの機能を関数に持たせられます
- uvのshebang方式なら、依存ライブラリを隔離環境に自動インストールでき、事前セットアップなしにそのまま実行できます
-
tasks.pyを1つ置いてchmod +xするだけで始められます
このtasks.pyを汎用のベースとして、自身の専門タスクに合わせたtaskrunnerを自作してみてください。