概要(CLI ツールは「自分専用コマンドを作る」こと)
CLI ツール作成は、
「python script.py」を叩く世界から一歩進んで、
mytool fetch --date 2025-01-01report-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-01python 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.pypython daily_job.py --date 2025-01-01
の両方に対応でき、
「人間がテストもしやすい」「自動化もさせやすい」 CLI になります。
ここで深掘りしたいポイントは、日付の決定ロジックを get_target_date という関数に分離していることです。
こうしておくと、「昨日」ではなく「今日」にしたくなったときも、この関数だけ触れば済みます。
CLI とロジックをしっかり分けるのは、後々の変更に非常に強くなります。
サブコマンドを持つ CLI(「小さなコマンド群」をまとめる)
git みたいな「サブコマンド形式」のイメージ
もう一歩進んだ CLI の形として、
mytool fetch ...mytool report ...
のように、「一つのコマンドの下に複数のサブコマンドをぶら下げる」構成があります。
git の
git commitgit statusgit 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 api1python 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 などの自動実行の両方に柔軟に対応できる。
エラー時のメッセージや終了コード、ヘルプテキストを整えることで、「壊れにくく・使い方で迷わない」道具に育っていく。
ここまでを一度手で書いてみると、
「今までただのスクリプトだったもの」が、
一気に“ちゃんとしたツール”に変わる感覚があるはずです。
