docopt

ドキュメント文字列からコマンドライン引数パーサーを自動生成する革新的なライブラリ。宣言的なアプローチを採用。

pythonclidocstringdeclarative

GitHub概要

docopt/docopt

Create *beautiful* command-line interfaces with Python

スター7,984
ウォッチ161
フォーク563
作成日:2012年4月7日
言語:Python
ライセンス:MIT License

トピックス

なし

スター履歴

docopt/docopt Star History
データ取得日時: 2025/7/25 02:04

フレームワーク

docopt

概要

docoptは、Pythonスクリプトのdocstringから自動的にコマンドラインインターフェース(CLI)を生成する革新的なライブラリです。「あなたのプログラムのヘルプメッセージがそのまま仕様になる」という哲学に基づいて設計されており、宣言的なアプローチでCLIを定義できます。オリジナル版は開発が停滞しており、現在はdocopt-ngへの移行が推奨されています。

詳細

docoptは、従来のCLIライブラリとは全く異なるアプローチを採用しています。プログラムの使用方法を記述したdocstringをパースし、その記述に基づいて自動的に引数解析を行います。この方法により、ドキュメントとコードの乖離を防ぎ、常に最新の使用方法が保証されます。

メンテナンス状況(2024年)

  • オリジナルdocopt: 2013年のバージョン0.6.2以降更新されておらず、28のプルリクエストが未処理のまま放置されている状況です。
  • docopt-ng: Jazzbandプロジェクトによって維持されているフォークで、型ヒント、完全なテストカバレッジ、そして継続的なメンテナンスが提供されています。最新リリースは2023年5月です。
  • 将来の展望: docopt-ngも長期的なメンテナンス継続に不安があり、より現代的なCLIフレームワークへの移行が検討されています。新規プロジェクトでは、型ヒントや入力検証などの機能を持つより現代的なフレームワークの使用が推奨されています。

主な特徴

  • Docstring駆動開発: プログラムのdocstringにUsage:セクションを記述するだけでCLIを定義
  • 自己文書化: ヘルプメッセージとパーサーが同じ場所に存在
  • パターンベース構文: 慣習的なコマンドラインヘルプメッセージの記法を使用
  • 最小限のコード: わずか数行でCLIを実装可能
  • 辞書形式の出力: パース結果は扱いやすい辞書として返される
  • POSIX準拠: 標準的なコマンドライン規約に従う

使用パターン構文

  • 位置引数: <file>, <host> のように山括弧で囲む
  • オプション: -h, --help, または -h, --help の形式
  • コマンド: add, ship のような単純な単語
  • 必須要素: () で囲む(例: (add|rm)
  • オプション要素: [] で囲む(例: [--timeout=<seconds>]
  • 排他的要素: | で区切る(例: (-a|-b)
  • 繰り返し要素: ... を付ける(例: NAME...

メリット・デメリット

メリット

  • ドキュメントとコードの一体化により保守性が向上
  • 直感的で読みやすい構文
  • 学習コストが低い
  • コードが簡潔で美しい
  • 標準的なコマンドライン規約に準拠

デメリット

  • 複雑なCLIには向かない場合がある
  • データ検証機能が組み込まれていない(別途実装が必要)
  • エラーメッセージのカスタマイズが困難
  • 型ヒントが本質的に制限される(docstring操作のため)
  • オリジナル版は完全に開発停止(2013年以降更新なし)
  • docopt-ngも長期的なメンテナンス継続に不安がある
  • デバッグが難しい場合がある
  • 新規プロジェクトには他の現代的なCLIライブラリが推奨される

主要リンク

書き方の例

基本的な使用例

"""シンプルな挨拶プログラム

Usage:
    hello.py <name> [--times=<n>]
    hello.py -h | --help
    hello.py --version

Options:
    -h --help     このヘルプメッセージを表示
    --version     バージョンを表示
    --times=<n>   挨拶を繰り返す回数 [default: 1]
"""
from docopt import docopt

def main():
    arguments = docopt(__doc__, version='Hello 1.0')
    
    name = arguments['<name>']
    times = int(arguments['--times'])
    
    for _ in range(times):
        print(f'こんにちは、{name}さん!')

if __name__ == '__main__':
    main()

複数コマンドの例

"""ファイル管理ツール

Usage:
    file_manager.py list [<directory>]
    file_manager.py copy <source> <destination> [--force]
    file_manager.py delete <file>... [--confirm]
    file_manager.py (-h | --help)
    file_manager.py --version

Commands:
    list            ディレクトリの内容を表示
    copy            ファイルをコピー
    delete          ファイルを削除

Options:
    -h --help       このヘルプメッセージを表示
    --version       バージョンを表示
    --force         既存ファイルを上書き
    --confirm       削除前に確認
"""
from docopt import docopt
import os
import shutil

def main():
    args = docopt(__doc__, version='File Manager 1.0')
    
    if args['list']:
        directory = args['<directory>'] or '.'
        files = os.listdir(directory)
        for f in files:
            print(f)
    
    elif args['copy']:
        source = args['<source>']
        destination = args['<destination>']
        if os.path.exists(destination) and not args['--force']:
            print(f'エラー: {destination} は既に存在します。--forceを使用してください。')
        else:
            shutil.copy2(source, destination)
            print(f'コピー完了: {source} -> {destination}')
    
    elif args['delete']:
        files = args['<file>']
        if args['--confirm']:
            response = input(f'{len(files)}個のファイルを削除しますか? (y/n): ')
            if response.lower() != 'y':
                print('削除をキャンセルしました。')
                return
        
        for file in files:
            os.remove(file)
            print(f'削除: {file}')

if __name__ == '__main__':
    main()

Pydanticを使用した検証付きの例

"""座標計算ツール

Usage:
    calculator.py distance <x1> <y1> <x2> <y2>
    calculator.py midpoint <x1> <y1> <x2> <y2>
    calculator.py (-h | --help)
    calculator.py --version

Options:
    -h --help     このヘルプメッセージを表示
    --version     バージョンを表示
"""
from docopt import docopt
from pydantic import BaseModel, Field, ValidationError
import math

class CoordinateArgs(BaseModel):
    x1: float = Field(..., alias='<x1>')
    y1: float = Field(..., alias='<y1>')
    x2: float = Field(..., alias='<x2>')
    y2: float = Field(..., alias='<y2>')
    distance: bool
    midpoint: bool

def calculate_distance(x1, y1, x2, y2):
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

def calculate_midpoint(x1, y1, x2, y2):
    return ((x1 + x2) / 2, (y1 + y2) / 2)

def main():
    raw_args = docopt(__doc__, version='Calculator 1.0')
    
    try:
        args = CoordinateArgs(**raw_args)
        
        if args.distance:
            result = calculate_distance(args.x1, args.y1, args.x2, args.y2)
            print(f'距離: {result:.2f}')
        
        elif args.midpoint:
            mx, my = calculate_midpoint(args.x1, args.y1, args.x2, args.y2)
            print(f'中点: ({mx:.2f}, {my:.2f})')
    
    except ValidationError as e:
        print('エラー: 無効な引数です。')
        print(e)
        exit(1)

if __name__ == '__main__':
    main()

高度なパターンの例

"""Git風のサブコマンドを持つツール

Usage:
    mytool init [<name>] [--bare]
    mytool clone <url> [<directory>]
    mytool commit -m <message> [--amend]
    mytool push [<remote>] [<branch>]
    mytool pull [<remote>] [<branch>] [--rebase]
    mytool status [--short]
    mytool log [--oneline] [--graph] [-n <number>]
    mytool (-h | --help)
    mytool --version

Options:
    -h --help       このヘルプメッセージを表示
    --version       バージョンを表示
    --bare          ベアリポジトリを作成
    -m <message>    コミットメッセージ
    --amend         前回のコミットを修正
    --rebase        リベースを使用してプル
    --short         短い形式で表示
    --oneline       各コミットを1行で表示
    --graph         グラフ表示
    -n <number>     表示するコミット数 [default: 10]
"""
from docopt import docopt

def main():
    args = docopt(__doc__, version='MyTool 1.0')
    
    # コマンドの判定と処理
    if args['init']:
        name = args['<name>'] or 'myproject'
        bare = args['--bare']
        print(f'プロジェクト "{name}" を初期化しました。')
        if bare:
            print('(ベアリポジトリ)')
    
    elif args['clone']:
        url = args['<url>']
        directory = args['<directory>'] or url.split('/')[-1]
        print(f'クローン中: {url} -> {directory}')
    
    elif args['commit']:
        message = args['<message>']
        amend = args['--amend']
        if amend:
            print(f'前回のコミットを修正: "{message}"')
        else:
            print(f'コミット: "{message}"')
    
    elif args['status']:
        if args['--short']:
            print('M  file1.txt\nA  file2.txt')
        else:
            print('変更されたファイル:\n  file1.txt\n新規ファイル:\n  file2.txt')

if __name__ == '__main__':
    main()