段階的に学ぶ Python の argparse：最小構成から実用的な実装まで

コマンドライン引数の処理は、多くの Python スクリプトで必要となる重要な機能です。
特にデータ処理やファイル変換など、バッチ処理を行うスクリプトでは適切な引数処理が不可欠です。
今回は、データ形式を変換するスクリプトを例に、Python の標準ライブラリ `argparse` の実践的な使い方を紹介します。

## 対象となるデータ変換スクリプト

`convert_data` スクリプトは、入力に渡された ファイルを指定されたフォーマットに変換する架空のスクリプトです。
コマンドライン引数として、下記の項目を受け取ります。

| 引数     | 概要                         | 想定する値                 | デフォルト値 | 必須 |
| -------- | ---------------------------- | -------------------------- | ------------ | ---- |
| --input  | 入力ファイルパス             | 既存のファイルパス         | なし         | ○    |
| --output | 出力ファイルパス             | 書き込み可能なファイルパス | なし         | ○    |
| --format | 出力フォーマット             | csv, json, markdown        | csv          | ×    |
| --force  | 出力ファイルを上書きするか？ | フラグ（指定のみ）         | False        | ×    |

例えば下記のように実行する想定です。

```bash
$ python convert_data.py --input /input/file.csv --output /output/file.json --format json --force
```

## 1. 最小構成での実装

まずは、必要最低限の機能を持つ argparse の実装例を見てみましょう。

```python:convert_data_minimal.py
import argparse


def parse_args_minimal():
    # ArgumentParserのインスタンスを作成
    parser = argparse.ArgumentParser()

    # 各引数を定義
    parser.add_argument("--input")
    parser.add_argument("--output")
    parser.add_argument("--format", choices=["csv", "json", "markdown"], default="csv")
    parser.add_argument("--force", action="store_true")

    return parser.parse_args()


def main():
    args = parse_args_minimal()

    # 引数の使用例
    print(f"入力ファイル: {args.input}")
    print(f"出力ファイル: {args.output}")
    print(f"出力形式: {args.format}")
    print(f"上書き: {args.force}")


if __name__ == "__main__":
    main()
```

この最小構成では、必要な引数の定義のみを行っています。
これだけで、コマンドライン引数を意味のある変数に読み込む目的は果たすことができますので、下記のように引数の指定順序を入れ替えても同じ結果が得られます。

```bash
$ python convert_data_minimal.py --input /input/path --output /output/path --format json --force
入力ファイル: /input/path
出力ファイル: /output/path
出力形式: json
上書き: True
$ python convert_data_minimal.py --force --input /input/path --format json --output /output/path
入力ファイル: /input/path
出力ファイル: /output/path
出力形式: json
上書き: True
```

ただ、引数の指定が無くてもエラーになりません。

```bash
$ python convert_data_minimal.py
入力ファイル: None
出力ファイル: None
出力形式: csv
上書き: False
```

usage の表示もしてくれますが、各項目に意味を与えていないため、ちょっと不親切です。

```bash
$ python convert_data_minimal.py -h
usage: convert_data_minimal.py [-h] [--input INPUT] [--output OUTPUT] [--format {csv,json,markdown}] [--force]

options:
  -h, --help            show this help message and exit
  --input INPUT
  --output OUTPUT
  --format {csv,json,markdown}
  --force
```

多くの人が使用するスクリプトを用意する場合は、もう少し手を加えておくと良さそうです。

## 2. 実用的な機能を追加した実装

最小構成の実装では、以下のような実用上の課題があります：

- 必須パラメータのチェックができない
- 入力ファイルの存在確認ができない
- 出力先の書き込み権限チェックができない
- ヘルプメッセージが不親切

これらの課題に対応するため、実用的な機能を追加していきます。

```python:convert_data_advanced.py
import argparse
import os
import sys
from pathlib import Path


def validate_input_file(path):
    """入力ファイルの存在確認とパスの絶対パス化"""
    path_obj = Path(path).resolve()
    if not path_obj.exists():
        raise argparse.ArgumentTypeError(f"入力ファイルが存在しません: {path}")
    return str(path_obj)


def validate_output_path(path):
    """出力パスのバリデーションと絶対パス化"""
    path_obj = Path(path).resolve()
    dir_path = path_obj.parent
    # 出力ディレクトリの存在確認
    if not dir_path.exists():
        raise argparse.ArgumentTypeError(
            f"出力先ディレクトリが存在しません: {dir_path}"
        )
    # 書き込み権限の確認
    if not os.access(dir_path, os.W_OK):
        raise argparse.ArgumentTypeError(
            f"出力先ディレクトリに書き込み権限がありません: {dir_path}"
        )
    return str(path_obj)


def parse_args_advanced():
    parser = argparse.ArgumentParser(
        description="データ形式変換スクリプト",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
使用例:
  %(prog)s -i input.csv -o output.json --format json
  %(prog)s -i data.json -o result.md --force
        """,
    )

    # 入出力の設定
    parser.add_argument(
        "-i",
        "--input",
        required=True,
        type=validate_input_file,
        help="入力ファイルパス",
    )
    parser.add_argument(
        "-o",
        "--output",
        required=True,
        type=validate_output_path,
        help="出力ファイルパス",
    )

    # 変換オプション
    parser.add_argument(
        "--format",
        choices=["csv", "json", "markdown"],
        default="json",
        help="出力形式 (デフォルト: json)",
    )

    # 動作オプション
    parser.add_argument(
        "--force", action="store_true", help="既存の出力ファイルを上書きする"
    )

    args = parser.parse_args()

    # 出力ファイルの存在確認
    if not args.force and Path(args.output).exists():
        parser.error(
            f"出力ファイルが既に存在します: {args.output}\n"
            f"上書きする場合は --force オプションを指定してください"
        )

    return args


def main():
    args = parse_args_advanced()

    try:
        # ここに実際の変換処理を実装
        print(f"入力ファイル: {args.input}")
        print(f"出力ファイル: {args.output}")
        print(f"出力形式: {args.format}")
        print(f"上書き: {args.force}")

    except Exception as e:
        print(f"エラーが発生しました: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

```

### add_argument に指定するパラメータを追加

`required=True` の指定を適宜追加し、必須入力の指定が無ければエラーになるようにしました。
これにより、`--input` や `--output` の指定が無い場合の振舞いが下記のようになります。

```bash
$ python convert_data_advanced.py
usage: convert_data_advanced.py [-h] -i INPUT -o OUTPUT [--format {csv,json,markdown}] [--force]
convert_data_advanced.py: error: the following arguments are required: -i/--input, -o/--output
```

また、`help` の指定を追加して、引数に対する説明を記載しました。
こちらは `-h` オプション指定時に使用されます。

```bash
$ python convert_data_advanced.py -h
~ 略 ~
options:
  -h, --help            show this help message and exit
  -i, --input INPUT     入力ファイルパス
  -o, --output OUTPUT   出力ファイルパス
  --format {csv,json,markdown}
                        出力形式 (デフォルト: json)
  --force               既存の出力ファイルを上書きする
```

さらに、`type` の指定を追加して、入力値のバリデーションを行なうようにしました。
type には `str` などの型の名前を書くこともできますが、このように独自のメソッドを渡すことで独自のバリデーションや変換処理を行なうこともできます。

`--input` について、対象のファイルが存在しなければエラーとし、存在すれば parse 結果として絶対パスを返却します。

```bash
$ python convert_data_advanced.py --input notexist.csv --output output.json --format json
usage: convert_data_advanced.py [-h] -i INPUT -o OUTPUT [--format {csv,json,markdown}] [--force]
convert_data_advanced.py: error: argument -i/--input: 入力ファイルが存在しません: notexist.csv

$ python convert_data_advanced.py --input example.csv --output output.json --format json
入力ファイル: /path/to/example.csv
出力ファイル: /path/to/output.json
出力形式: json
上書き: False
```

どこまでを argparse の役目にするかも迷いどころですね。

### ArgumentParser 作成時のパラメータ追加

ヘルプ表示を改善するためにいくつかオプションを指定しています。

`description` にツールの説明を記載すると、usage の下に出力されます。
`epilog` を使用するとヘルプの末尾に文字列を出力できますので、今回は補足説明としてコマンドの使用例を記載しました。

`formatter_class` の指定はこれらのヘルプ文字列の表示方法を調整するものです。

## まとめ

このように、実用的なコマンドラインツールを作成する際には、単純な引数処理だけでなく、以下の点に注意を払う必要があります：

1. **入力値の検証**: ファイルの存在確認やパーミッションチェックなど
2. **エラー処理**: 適切なエラーメッセージとエラーコード
3. **使いやすさ**: ヘルプメッセージや使用例の提供

これらの機能を適切に実装することで、より信頼性の高い、使いやすいツールを作成することができます。