Python自動化 #7 自分だけのコマンドを作る — typerとrichでCLI化

読了 6分

スクリプトが 1〜2 個のうちは問題ありません。ところが第 6 回まで追いかけてきて、フォルダ整理、Excel レポート、スクレイピング、メール送信のスクリプトがたまってくると、新しい問題が生まれます。「あれ、どうやって実行するんだっけ」です。あるスクリプトは引数が 3 つで、あるスクリプトは特定のフォルダでしか動きません。シリーズ最終回では、この散らばったスクリプトたちを 1 つの CLI ツールにまとめ、ターミナルのどこからでも一言で呼べるコマンドにします。

argparseからtyperへ #

#1 では標準ライブラリの argparse で引数を受け取りました。parser.add_argument("folder") のように引数 1 つごとに宣言を 1 行ずつ積む方式で、ちゃんと動きますが、インターフェイスが大きくなるほど宣言部が長くなります。typer は同じ仕事を型ヒントで解決します。関数シグネチャがそのまま CLI 定義です。

cli.py — typer方式
from pathlib import Path
import typer

def organize(folder: Path, dry_run: bool = False):
    """ダウンロードフォルダを拡張子別に整理します。"""
    print(f"{folder} 整理開始 (dry_run={dry_run})")

if __name__ == "__main__":
    typer.run(organize)

宣言コードが別にないのに、typer がシグネチャから全部読み取ります。folder: Path は必須引数になって入力値が Path オブジェクトへ自動変換され、dry_run: bool = False--dry-run フラグになり、docstring はヘルプの説明文になります。

uv add typer rich で 2 つのライブラリをインストールした後、uv run cli.py ~/Downloads --dry-run のようにすぐ実行できます。

サブコマンドでまとめる #

本当の価値はここからです。typer.Typer() オブジェクトに関数を複数登録すると、git のようにサブコマンドを持つツールになります。#1〜#6 で作ったスクリプトたちを関数 1 つずつに移して 1 つのツールに集め、サブコマンドで呼びます。

mytools/cli.py
from pathlib import Path
import typer

app = typer.Typer(help="自分の自動化ツール集")

@app.command()
def organize(folder: Path, dry_run: bool = False):
    """ダウンロードフォルダを拡張子別に整理します。"""
    ...

@app.command()
def scrape(url: str, output: Path = Path("result.csv")):
    """掲示板の記事一覧を収集して CSV に保存します。"""
    ...

@app.command()
def report(month: str, send: bool = False):
    """月間 Excel レポートを作り、--send ならメールで送信します。"""
    ...

if __name__ == "__main__":
    app()
サブコマンドの使い方
uv run mytools/cli.py organize ~/Downloads --dry-run
uv run mytools/cli.py scrape https://example.com/board
uv run mytools/cli.py report 2026-07 --send

これでコマンド体系ができます。スクリプトファイル 7 個を覚える代わりに、ツール 1 つとサブコマンド名だけ覚えればよくなります。

–helpがタダで付いてくるということ #

typer でまとめると、ヘルプを別に書く必要がありません。

uv run mytools/cli.py --help の出力
 自分の自動化ツール集
 Commands:
   organize   ダウンロードフォルダを拡張子別に整理します。
   scrape     掲示板の記事一覧を収集して CSV に保存します。
   report     月間 Excel レポートを作り、--send ならメールで送信します。

この画面の価値は 3 ヶ月後の自分が証明します。スクリプト時代はコードを開いて引数を読み直す必要がありましたが、これからは --help 1 回で、何があってどう使うのかをツール自身が教えてくれます。サブコマンドごとに organize --help も別に生成されます。

richで見やすく #

typer と一緒にインストールした rich はターミナル出力を担当します。自動化ツールで特に役立つのは進捗バーと表です。organize コマンドの本体をこう書き換えます。

進捗バーと結果の表
from rich.console import Console
from rich.progress import track
from rich.table import Table

console = Console()
counts: dict[str, int] = {}
for file in track(list(folder.iterdir()), description="整理中..."):
    ext = file.suffix.lstrip(".") or "その他"
    counts[ext] = counts.get(ext, 0) + 1  # 移動ロジックは第1回のまま
table = Table(title="整理結果")
table.add_column("拡張子")
table.add_column("件数", justify="right")
for ext, count in sorted(counts.items()):
    table.add_row(ext, str(count))
console.print(table)

track() で包むだけでループに進捗バーが付き、Table は結果を整列した表で出力します。数百個のファイルを移す間、画面が固まったように見える問題が消え、終わったときに何をどれだけ処理したかが一目で分かります。console.print("[red]失敗[/red]") のように色を付けるのも 1 行です。

どこからでも呼べるコマンドとしてインストールする #

今はプロジェクトフォルダで uv run mytools/cli.py ... と呼ぶ必要がありますが、本物のツールならどのディレクトリからでも mytools の一言で起動するべきです。pyproject.toml[project.scripts] がそのつなぎ役です。

pyproject.toml
[project]
name = "mytools"
version = "0.1.0"
dependencies = ["typer", "rich"]

[project.scripts]
mytools = "mytools.cli:app"

[build-system]
requires = ["uv_build"]
build-backend = "uv_build"

mytools = "mytools.cli:app" は「mytools というコマンドが来たら mytools/cli.pyapp オブジェクトを実行せよ」という宣言です。これでツールとしてインストールします。

ツールとしてインストール
uv tool install .
mytools organize ~/Downloads

uv tool install は隔離された専用環境にパッケージをインストールし、コマンドだけを PATH に公開します。ほかのプロジェクトの仮想環境と混ざらず、どのフォルダからでも呼び出せます。uv を使わない環境なら pipx install . が同じ役割を果たします。

チームに配るのも難しくありません。社内 PyPI サーバーがなくても git リポジトリさえあれば、同僚は uv tool install git+https://github.com/our-team/mytools.git の 1 行で同じツールをインストールし、uv tool upgrade mytools で更新します。スクリプトファイルをメッセンジャーでやり取りしていた方式と比べると、バージョン管理と依存関係のインストールが全部自動で解決されるわけです。

シリーズを終えて #

7 本を 1 行ずつ振り返ります。

  • #1 ファイルを移す最初のスクリプトで、手作業をコードに移しました
  • #2 openpyxl で Excel の繰り返し作業を終わらせました
  • #3 httpx と BeautifulSoup で静的ページを収集しました
  • #4 Playwright で JavaScript レンダリングのページまで扱いました
  • #5 結果をメールとメッセンジャー通知で受け取れるようにしました
  • #6 スケジューラに載せて人がいなくても回るようにしました
  • #7 全部を 1 つの CLI ツールに包んで、コマンド 1 つにまとめました

振り返ると、ツールは脇役にすぎず、自動化の出発点は繰り返しを発見する目でした。毎週同じフォルダを整理していること、毎月同じ表をコピーしていることに気づく瞬間が始まりです。その繰り返しを 1 つずつコードに移していくうちに、いつの間にか今回のような自分だけのツールボックスができあがります。そしてツールが大きくなると、次の質問が付いてきます。「直したら、ほかのコマンドが壊れていないだろうか」です。そのとき必要になるのがテストで、Pythonテスト シリーズが pytest でその答えを扱います。自動化ツールを長く使うつもりなら、次の行き先としておすすめします。

X