Python | 自動化:CLI ツール作成

Python
スポンサーリンク

概要(CLI ツールは「自分専用コマンドを作る」こと)

CLI ツール作成は、
「python script.py」を叩く世界から一歩進んで、

mytool fetch --date 2025-01-01
report-maker --input data.csv --format excel

のように、自分でコマンド名・オプション・動きを設計していく作業です。

自動化と組み合わせると、

毎回ちょっとずつ条件を変えて実行したい処理
人に渡して使ってもらいたいスクリプト
cron やタスクスケジューラから呼び出す“バッチコマンド”

を、きれいな「道具」として整理できます。

ここでは、初心者向けに

CLI ツールの基本イメージ
argparse を使った引数処理の書き方
実用的な例(ファイル処理・日付指定バッチ)
設計で大事なポイント(構造化・エラー・ヘルプ)

を、かみ砕いて説明していきます。


CLI ツールの基礎イメージ(ただのスクリプトとの違い)

「引数で動きを変えられるスクリプト」が CLI の出発点

一番最初の Python スクリプトは、だいたいこういう形です。

# sample.py
def main():
    print("Hello")

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

これは、いつ実行しても同じことしかしません。
CLI ツールらしくしていく第一歩は、「引数で動きを変えられるようにする」ことです。

例えば、

python sample.py input.csv output.csv

と渡したら、input.csv を読んで output.csv に書き出す、という感じです。

この「引数」は、Python では sys.argv から取ることもできますが、
初心者が最初から触るなら argparse を使うほうが圧倒的に分かりやすくて安全です。


argparse の基本(「必須引数」と「オプション引数」を扱う)

最小の例:引数に渡した名前を挨拶に使う

まずは、構造を覚えるための超シンプルな例から。

# greet.py
import argparse

def parse_args():
    parser = argparse.ArgumentParser(
        description="名前を指定して挨拶するシンプルな CLI"
    )
    parser.add_argument("name", help="挨拶する相手の名前")
    return parser.parse_args()

def main():
    args = parse_args()
    print(f"こんにちは、{args.name} さん")

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

ターミナルから次のように実行します。

python greet.py 太郎

動きを一つずつ分解します。

ArgumentParser を作るときに description を書いておくと、-h--help をしたときに説明として出てきます。
add_argument("name", ...) と書くと、「位置引数」を 1 つ要求する CLI になります。
つまり、name は必須で、指定しないとエラーになります。
parse_args() を呼ぶと、コマンドラインの引数を解析し、args.name で参照できるオブジェクトが返ります。

試しに python greet.py --help と打つと、使い方が自動で表示されるはずです。
これが CLI ツール作りの土台になります。

オプション引数(–date みたいなやつ)を追加する

次に、「指定してもいいし、しなくてもいい」オプション引数を追加してみます。

# greet2.py
import argparse
from datetime import date

def parse_args():
    parser = argparse.ArgumentParser(
        description="名前と日付を指定して挨拶する CLI"
    )
    parser.add_argument("name", help="挨拶する相手の名前")
    parser.add_argument(
        "--date",
        help="挨拶の日付(YYYY-MM-DD)。省略時は今日。",
        required=False
    )
    return parser.parse_args()

def main():
    args = parse_args()

    if args.date:
        greeting_date = args.date
    else:
        greeting_date = date.today().isoformat()

    print(f"{greeting_date} 付で、こんにちは {args.name} さん")

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

この CLI は例えば、

python greet2.py 太郎 --date 2025-01-01
python greet2.py 花子

のように使えます。

ここで押さえておきたいのは、

位置引数(name)は必須
オプション引数(–date)は任意で、指定がなければコード側でデフォルトを決める

という分け方です。


実用例1:ファイルを処理する CLI(入出力パスを引数にする)

手動でパスを書き換えるスクリプトから卒業する

よくある“初心者のスクリプト”は、こんな感じで書かれます。

# bad_example.py
INPUT_PATH = "data/input.csv"
OUTPUT_PATH = "data/output.csv"

def main():
    # INPUT_PATH を読んで OUTPUT_PATH に書く処理
    ...
Python

これだと、ファイルを変えたいたびにソースを書き換えなければなりません。
CLI としてきちんと作るなら、「パスは引数で指定する」ようにします。

# csv_tool.py
import argparse
from pathlib import Path

def parse_args():
    parser = argparse.ArgumentParser(
        description="CSV を読み込んで、簡単な加工をして別ファイルに保存するツール"
    )
    parser.add_argument("input", help="入力 CSV ファイルパス")
    parser.add_argument("output", help="出力 CSV ファイルパス")
    return parser.parse_args()

def process_csv(input_path: Path, output_path: Path):
    import csv

    rows = []
    with input_path.open("r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            row["processed"] = "yes"
            rows.append(row)

    if rows:
        fieldnames = list(rows[0].keys())
    else:
        fieldnames = []

    with output_path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(rows)

def main():
    args = parse_args()
    input_path = Path(args.input)
    output_path = Path(args.output)
    process_csv(input_path, output_path)

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

この CLI は、次のように使えます。

python csv_tool.py data/input.csv data/output.csv

ここでの重要ポイントを整理します。

処理本体(process_csv)は、CLI に依存していない純粋な関数として書く。
引数パース(parse_args)と、path の解釈(Path(args.xxx))は main で行い、処理本体に渡す。

こう分けておくと、

スクリプトとして直接実行する
他の Python コードから import して関数呼び出しだけ行う

の両方ができるようになり、再利用性が一気に上がります。


実用例2:日付指定バッチの CLI(自動化と相性がいい形)

「ターゲットの日付」を引数で渡せるようにする

自動化バッチでよくあるパターンに、「特定の日付のデータだけ処理する」というものがあります。

例えば、

python daily_job.py --date 2025-01-01

と渡したら「2025-01-01 のデータを処理する」。
引数を省略したら「昨日」のデータを処理する。

こういう CLI にしておくと、

テスト時:任意の日付を指定して試せる
本番:cron やタスクスケジューラから実行するときは、引数なしで「昨日分」を処理させる

という使い方ができて、とても気持ちいいです。

# daily_job.py
import argparse
from datetime import date, timedelta

def parse_args():
    parser = argparse.ArgumentParser(
        description="日付ごとのデータを処理する日次バッチ"
    )
    parser.add_argument(
        "--date",
        help="処理対象日(YYYY-MM-DD)。指定がなければ昨日。",
        required=False
    )
    return parser.parse_args()

def get_target_date(date_str: str | None) -> date:
    if date_str:
        return date.fromisoformat(date_str)
    else:
        return date.today() - timedelta(days=1)

def run_for_date(target_date: date):
    # ここに対象日付の処理を書く
    print(f"{target_date} のデータを処理します")

def main():
    args = parse_args()
    target_date = get_target_date(args.date)
    run_for_date(target_date)

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

この構成にしておくと、

python daily_job.py
python daily_job.py --date 2025-01-01

の両方に対応でき、
「人間がテストもしやすい」「自動化もさせやすい」 CLI になります。

ここで深掘りしたいポイントは、日付の決定ロジックを get_target_date という関数に分離していることです。
こうしておくと、「昨日」ではなく「今日」にしたくなったときも、この関数だけ触れば済みます。
CLI とロジックをしっかり分けるのは、後々の変更に非常に強くなります。


サブコマンドを持つ CLI(「小さなコマンド群」をまとめる)

git みたいな「サブコマンド形式」のイメージ

もう一歩進んだ CLI の形として、

mytool fetch ...
mytool report ...

のように、「一つのコマンドの下に複数のサブコマンドをぶら下げる」構成があります。
git の

git commit
git status
git push

と同じイメージです。

argparse では、add_subparsers を使うことでこれが表現できます。

例:データ収集と集計をまとめた mytool

# mytool.py
import argparse

def fetch_command(args):
    print(f"fetch を実行します。source={args.source}")

def report_command(args):
    print(f"report を実行します。format={args.format}")

def build_parser():
    parser = argparse.ArgumentParser(
        prog="mytool",
        description="データ収集とレポート出力を行う CLI ツール"
    )

    subparsers = parser.add_subparsers(dest="command", required=True)

    fetch_parser = subparsers.add_parser("fetch", help="データを収集する")
    fetch_parser.add_argument("--source", required=True, help="データソース名")
    fetch_parser.set_defaults(func=fetch_command)

    report_parser = subparsers.add_parser("report", help="レポートを出力する")
    report_parser.add_argument("--format", choices=["csv", "excel"], default="csv", help="出力形式")
    report_parser.set_defaults(func=report_command)

    return parser

def main():
    parser = build_parser()
    args = parser.parse_args()
    args.func(args)

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

この CLI は次のように使えます。

python mytool.py fetch --source api1
python mytool.py report --format excel

仕組みを少し深掘りします。

parser.add_subparsers でサブコマンドのグループを作り、dest=”command” で選択結果を args.command に入れています。
subparsers.add_parser(“fetch”, …) で fetch サブコマンド用のパーサを作り、その上に引数を定義します。
fetch_parser.set_defaults(func=fetch_command) としておくと、そのサブコマンドが選ばれたとき、args に func という属性で対応する関数がセットされます。
main では最終的に args.func(args) を呼ぶだけで、適切な処理(fetch_command または report_command)が実行されます。

この形を覚えると、“小さな CLI ツールだらけ”になるのを防いで、一つの「道具箱」コマンドとしてまとめることができます。


CLI ツール設計で大事なポイントを深掘りする

1. main とロジックを分ける(テストしやすくする)

CLI ツールを書くとき、一番やりがちな“残念パターン”は、
すべての処理を main の中に書いてしまうことです。

def main():
    parser = ...
    args = parser.parse_args()
    # この中に全部処理を書く(巨大化)
Python

これをやると、

ロジック部分だけを別のスクリプトから再利用できない
単体テストを書きにくい
CLI の変更とロジックの変更がごちゃごちゃになる

という状態になります。

避け方はシンプルで、

引数の解釈(文字列 → Path や date への変換)は main でやる
処理本体は「普通の関数」にして切り出し、そこに型付き引数を渡す

という分離を徹底することです。

2. エラーメッセージと終了コード

CLI ツールは、「うまくいったかどうか」を終了コード(exit code)で表現します。

Python では、例外を投げずにスクリプトが最後まで正常に終われば 0。
sys.exit(1) などで明示的に終了すれば、その値が終了コードになります。

例えば、「ユーザーの使い方のミス」は、丁寧なメッセージを出したうえで exit するのが親切です。

import sys

def main():
    args = parse_args()
    if not Path(args.input).exists():
        print(f"入力ファイルが存在しません: {args.input}", file=sys.stderr)
        sys.exit(1)
Python

ここでは、標準エラー出力(sys.stderr)にメッセージを書き、終了コード 1 で終わるようにしています。
他のツールやシェルスクリプトからこの CLI を呼んだとき、「失敗した」ことを検知しやすくなります。

3. ヘルプとデフォルト値を丁寧に書く

argparse の強みの一つは、「使い方の説明を自動で出してくれる」ことです。

そのためには、description、help、choices、default などをできるだけ丁寧に書いておくのが大事です。

description には、そのツールが何をするのか一文で書く。
各引数の help には、「値の意味」「例」を短くでも書く。
choices=["csv", "excel"] のように、取りうる値を制限すると、使い方ミスを防げる。
default を指定しておくと、「よくあるケース」では引数を省略できて便利。

CLI ツールは、「自分や他人が明日また見たときに迷わないこと」が重要です。
ヘルプメッセージは、未来の自分への手紙だと思って、少しだけ丁寧に書いておくと後で効きます。


まとめ(CLI ツールは「自動化の顔」を作る作業)

Python の CLI ツール作成を、自動化の文脈でまとめるとこうなります。

CLI ツールとは、「引数で動きを変えられるスクリプト」であり、自動化処理を人間や他ツールから使いやすい形にする“インターフェース”。
argparse を使えば、位置引数・オプション引数・サブコマンドを簡潔に定義でき、ヘルプも自動生成される。
処理本体は普通の関数に分離し、main では「引数パース → 型変換 → 関数呼び出し」だけにすると、テストと再利用がしやすい。
日付や入出力パスを引数にしておくと、人間のテストと cron などの自動実行の両方に柔軟に対応できる。
エラー時のメッセージや終了コード、ヘルプテキストを整えることで、「壊れにくく・使い方で迷わない」道具に育っていく。

ここまでを一度手で書いてみると、
「今までただのスクリプトだったもの」が、
一気に“ちゃんとしたツール”に変わる感覚があるはずです。

タイトルとURLをコピーしました