PythonのClickとRichでプロ仕様のCLIツールを作る:シンプルなスクリプトから洗練されたコマンドラインアプリへ

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

PythonでCLIツールを作る3つの方法(そしてなぜ多くの人が間違った選択をするのか)

Pythonスクリプトをきちんとしたコマンドラインツールにしたいとき、繰り返し候補に上がるのが3つのアプローチです。sys.argvの手動パース、標準ライブラリのargparse、そしてClickのようなサードパーティライブラリ。私は数十の社内ツールでこの3つすべてを使ってきましたが、この選択は見た目以上に大きな影響を持ちます。

4つ目の要素——CLIを見た目よくする——はたいていの場合、完全に後回しにされます。そこで登場するのがRichです。ClickとRichを組み合わせることで、動作と表示の両方をカバーできます。チームメンバーが毎日使いたいと思うツールと、こっそりシェルエイリアスに置き換えられるツールの差は、多くの場合、進捗やエラーをどれだけわかりやすく伝えられるかにかかっています。

アプローチ比較:sys.argv vs argparse vs Click

生のsys.argv

「とにかく動かす」アプローチです。sys.argv[1:]を取得し、手動でパースしておしまい。

import sys

def main():
    if len(sys.argv) < 2:
        print("使い方: tool.py <ファイル名>")
        sys.exit(1)
    filename = sys.argv[1]
    process(filename)

使い捨てスクリプトで単一の位置引数だけなら問題ありません。しかし、オプションフラグ、複数のサブコマンド、型バリデーションを追加しようとすると、たちまち辛くなります。

argparse(標準ライブラリ)

argparseは長年デファクトスタンダードでした。フラグ、位置引数、ヘルプテキスト生成、基本的な型変換を追加インストールなしで扱えます。

import argparse

parser = argparse.ArgumentParser(description="ファイルを処理する")
parser.add_argument("filename", help="入力ファイル")
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args()

小さなツールならargparseで十分です。ただし、複数のサブコマンド、ネストしたグループ、コールバック、テストを追加していくと、ボイラープレートが積み重なりコードが読みにくくなります。

Click

ClickはPythonのデコレータを使ってCLIインターフェースを関数に直接定義します。コードは自然に読め、ツールが大きくなっても保守しやすい状態を保てます。

import click

@click.command()
@click.argument("filename")
@click.option("--verbose", is_flag=True, help="詳細出力を有効にする")
def process(filename, verbose):
    """ファイルを処理して結果を報告する。"""
    if verbose:
        click.echo(f"処理中: {filename}")

同じ機能でも、意図が即座に伝わります。各パラメータは宣言された場所でドキュメント化されています。テストもすっきりしています——ClickにはCliRunnerという組み込みのテストランナーが付属しています。

選択を決める前に知っておくべきメリット・デメリット

argparseのメリット・デメリット

  • メリット:依存ゼロ——Pythonに同梱されている
  • メリット:安定しており、ドキュメントが充実し、広く理解されている
  • デメリット:サブコマンドはsubparsersによる冗長なセットアップが必要
  • デメリット:テストではsys.argvをモックするかサブプロセスを起動する必要がある
  • デメリット:カラーやスタイル付き出力は組み込まれていない

Clickのメリット・デメリット

  • メリット:デコレータベース——インターフェースがロジックの隣に書ける
  • メリット:サブコマンドは@click.group()でファーストクラスとして扱われる
  • メリット:テストユーティリティ(CliRunner)が組み込まれている
  • メリット:コンポーザブル——コマンドを別々のモジュールから組み立てられる
  • デメリット:外部依存がある(ただし軽量で安定している)
  • デメリット:一部の引数パースのエッジケースがargparseの慣習と異なる

Richのメリット・デメリット

  • メリット:テーブル、プログレスバー、シンタックスハイライト、パネル——すべて組み込み
  • メリット:ターミナルの自動検出(パイプ出力にカラーコードが混入しない)
  • メリット:Clickとクリーンに組み合わさる——競合なし
  • デメリット:依存関係に約2MBが加わる
  • デメリット:一度きりで使い捨てるスクリプトにはオーバースペック

私のおすすめ:一人が時々実行する小さな社内スクリプトにはargparseを使う。チームで共有したりパッケージとして配布するものにはClick + Richを使う。httpieblackpoetryはすべてClickで作られています——偶然ではなく、これらは人々が本当に使いたいと思うツールでもあります。明確な進捗表示と読みやすいエラーメッセージこそが、信頼されるツールと避けられるツールの差を生み出します。

推奨セットアップ

依存関係のインストール

pip install click rich

プロジェクト構成

CLIレイヤーは薄く保つ。ビジネスロジックはモジュールに属するものであり、Clickデコレータの中に書くべきではありません。

mytool/
├── cli.py          ← Clickコマンド
├── core.py         ← 実際のロジック
├── __init__.py
pyproject.toml       ← エントリポイントの設定

pyproject.tomlのエントリポイント

[project.scripts]
mytool = "mytool.cli:main"

pip install -e .後は、どのディレクトリからでもmytoolを直接実行できます——python -mのプレフィックスは不要です。

実装ガイド:実際のCLIツールを作る

ステップ1——基本的なコマンドグループ

最初からグループを使って複数のサブコマンドに対応しておく。コマンドをリリースした後からグループを追加するのは、思った以上に大変です。

import click
from rich.console import Console

console = Console()

@click.group()
@click.version_option(version="1.0.0")
def main():
    """MyTool — ファイル処理ユーティリティ。"""
    pass

@main.command()
@click.argument("filepath", type=click.Path(exists=True))
@click.option("--format", type=click.Choice(["json", "csv", "table"]), default="table")
@click.option("--verbose", "-v", is_flag=True)
def analyze(filepath, format, verbose):
    """ファイルを分析して結果を表示する。"""
    if verbose:
        console.print(f"[dim]読み込み中: {filepath}[/dim]")
    # ... ここでコアロジックを呼び出す

ステップ2——構造化出力のためのRichテーブル

普通のprint()でも動きますが、テーブルを使えば結果が一目でスキャンできるようになります。Richのテーブルはカラー、配置、行のハイライトをサポート——ボイラープレートゼロです。

from rich.table import Table

def display_results(records):
    table = Table(title="分析結果", show_lines=True)
    table.add_column("ファイル", style="cyan", no_wrap=True)
    table.add_column("サイズ", justify="right")
    table.add_column("ステータス", justify="center")

    for r in records:
        status_color = "green" if r["ok"] else "red"
        table.add_row(
            r["name"],
            f"{r['size']:,} bytes",
            f"[{status_color}]{r['status']}[/{status_color}]"
        )

    console.print(table)

ステップ3——長時間処理のためのプログレスバー

半秒以上かかる処理には視覚的なフィードバックが必要です。Richのプログレスバーはループとクリーンに統合でき、追加のスレッド処理なしで複数の同時タスクも扱えます。

from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn

def process_files(files):
    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        BarColumn(),
        TextColumn("{task.completed}/{task.total}"),
    ) as progress:
        task = progress.add_task("ファイルを処理中...", total=len(files))
        for f in files:
            process_single(f)
            progress.advance(task)

ステップ4——一貫したエラーハンドリング

ClickExceptionを発生させると、フォーマットされたエラーメッセージを表示してゼロ以外のステータスコードで終了します。Richのマークアップと組み合わせることで、エラーを視覚的に目立たせられます。

@main.command()
@click.argument("config", type=click.Path())
def run(config):
    """設定ファイルを使って実行する。"""
    try:
        settings = load_config(config)
    except FileNotFoundError:
        console.print(f"[bold red]エラー:[/bold red] 設定ファイルが見つかりません: {config}")
        raise click.Abort()
    except ValueError as e:
        raise click.ClickException(str(e))

ステップ5——CliRunnerによるテスト

Clickの組み込みテストランナーを使えば、コマンドをプログラム的に呼び出せます。サブプロセスのオーバーヘッドもsys.argvのモックも不要——invoke()を呼び出して結果を検査するだけです。テストは高速に動作し、独立性が保たれます

from click.testing import CliRunner
from mytool.cli import main

def test_analyze_command():
    runner = CliRunner()
    with runner.isolated_filesystem():
        with open("sample.txt", "w") as f:
            f.write("テストコンテンツ")
        result = runner.invoke(main, ["analyze", "sample.txt"])
        assert result.exit_code == 0
        assert "Results" in result.output

大きな差を生む小さなパターン

  • 生の文字列引数の代わりにclick.Path(exists=True)を使う——Clickがコードの実行前にパスを検証し、クリーンなエラーを自動表示します。
  • 破壊的なコマンドには最初から--dry-runを追加する——実行前に何が起こるかプレビューできると、ユーザーはツールをより信頼します。
  • 表示にはconsole.print()を、パイプされる可能性のある機械可読出力にはclick.echo()を使う——RichのConsolestderrstdoutを正しく分離します。
  • --helpのテキストは短く動詞中心に書く。「ファイルを分析する」は「このコマンドはファイルを分析するために使われます」より優れています。Clickはdocstringをそのまま表示します——ドキュメント生成ツールではなく、ユーザーのために書きましょう。

Clickを使わないほうがいいケース

CIパイプライン内で一度だけ実行され、出力が常に別のツールにパイプされるスクリプトなら、Clickのインタラクティブ性は何も加えません。--output json付きのシンプルなargparseスクリプトが正解です。Clickが真価を発揮するのは、ターミナルの向こうに人間がいるときです。

キーボードナビゲーション付きのフルスクリーンTUIが必要ですか?Textualを見てみてください——Richと同じチームが作った、まさにそのユースケースのために設計されたライブラリです。

Share: