【Python】簡単なCLIツールを作ってみる(argparse活用)

Python

こんにちは、かつコーチです。

「毎回同じフォルダ整理のスクリプトを書き換えて実行している」

そんな手間を感じたことはないでしょうか。

コマンドライン引数を受け取れるようにすれば、1つのスクリプトを使い回せるCLIツールに育てられます。

この記事では、標準ライブラリargparseでコマンドライン引数を受け取る方法と、これまでの記事で扱ったファイル操作を組み合わせて、実際に動くCLIツールを作る手順を解説します。

読み終える頃には、自分の作業に合わせたCLIツールを自作できるようになります。

基本の書き方・実装手順

手順1:argparseの最小構成を書く

argparseは標準ライブラリなので、追加インストールは不要です。

# greet.py
import argparse


def main():
    parser = argparse.ArgumentParser(description="挨拶を表示するツール")
    parser.add_argument("name", help="挨拶する相手の名前")
    args = parser.parse_args()

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


if __name__ == "__main__":
    main()
python greet.py かつコーチ
こんにちは、かつコーチさん!

add_argument("name", ...)で定義した位置引数が、args.nameとして取得できます。

手順2:オプション引数を追加する

--で始まるオプション引数も追加してみます。

# greet.py
import argparse


def main():
    parser = argparse.ArgumentParser(description="挨拶を表示するツール")
    parser.add_argument("name", help="挨拶する相手の名前")
    parser.add_argument(
        "--polite",
        action="store_true",
        help="丁寧な言い回しにする",
    )
    args = parser.parse_args()

    if args.polite:
        print(f"{args.name}様、いつもお世話になっております。")
    else:
        print(f"こんにちは、{args.name}さん!")


if __name__ == "__main__":
    main()
python greet.py かつコーチ --polite
かつコーチ様、いつもお世話になっております。

action="store_true"は、指定があればTrue、なければFalseになるフラグ用のオプションです。

手順3:–helpが自動生成されることを確認する

python greet.py --help
usage: greet.py [-h] [--polite] name

挨拶を表示するツール

positional arguments:
  name        挨拶する相手の名前

options:
  -h, --help  show this help message and exit
  --polite    丁寧な言い回しにする

add_argumentで指定したhelpの内容が、自動で使い方の説明として整形されます。

自作のCLIツールでも、標準のコマンドと同じように--helpが使えるのは大きな利点です。

手順4:これまでの記事のファイル操作と組み合わせる

ここからは、フォルダ内のファイルを拡張子ごとに整理する実践的なCLIツールを作ります。

「osモジュールでファイル・ディレクトリを操作する」で扱ったosモジュールを活用します。

# organize.py
import argparse
import os
import shutil


def organize_files(target_dir: str, dry_run: bool = False) -> None:
    """指定フォルダ直下のファイルを拡張子ごとのサブフォルダに整理する"""
    if not os.path.isdir(target_dir):
        raise NotADirectoryError(f"{target_dir} はフォルダではありません")

    for filename in os.listdir(target_dir):
        filepath = os.path.join(target_dir, filename)

        if not os.path.isfile(filepath):
            continue

        _, ext = os.path.splitext(filename)
        ext = ext.lstrip(".") or "no_extension"
        dest_dir = os.path.join(target_dir, ext)

        if dry_run:
            print(f"[dry-run] {filename} -> {ext}/")
            continue

        os.makedirs(dest_dir, exist_ok=True)
        shutil.move(filepath, os.path.join(dest_dir, filename))
        print(f"{filename} -> {ext}/")


def main():
    parser = argparse.ArgumentParser(description="フォルダ内のファイルを拡張子ごとに整理するツール")
    parser.add_argument("target_dir", help="整理したいフォルダのパス")
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="実際には移動せず、対象だけを表示する",
    )
    args = parser.parse_args()

    organize_files(args.target_dir, dry_run=args.dry_run)


if __name__ == "__main__":
    main()
python organize.py ~/Downloads --dry-run
[dry-run] report.pdf -> pdf/
[dry-run] photo.png -> png/
[dry-run] notes.txt -> txt/

--dry-runオプションで、実際に移動する前に対象ファイルを確認できるようにしています。

実務のCLIツールでは、この「実行前に結果を確認できる仕組み」がとても重要です。

問題なければ、--dry-runを外して実行します。

python organize.py ~/Downloads
report.pdf -> pdf/
photo.png -> png/
notes.txt -> txt/

よくあるつまずきポイント・エラー対処

必須引数を渡し忘れてSystemExitで落ちる

筆者はorganize.pyを初めて共有したとき、引数なしで実行され「動かない」と連絡を受けたことがあります。

❌ Before(必須引数なしで実行してしまう)

python organize.py
usage: organize.py [-h] [--dry-run] target_dir
organize.py: error: the following arguments are required: target_dir

argparseは、必須の位置引数が足りないと自動でエラーメッセージを表示し、プログラムを終了します。

裏側ではSystemExitという例外が発生しており、try-exceptで捕まえない限りプログラムはそこで止まります。

✅ After(デフォルト値を設定するか、事前にヘルプを案内する)

parser.add_argument(
    "target_dir",
    nargs="?",
    default=".",
    help="整理したいフォルダのパス(省略時はカレントディレクトリ)",
)
python organize.py
. -> ./ 配下のファイルを整理します

nargs="?"default="."を組み合わせることで、引数省略時に「カレントディレクトリを対象にする」という挙動に変えられます。

必須にすべきか、デフォルト値を用意すべきかは、ツールの使われ方に応じて判断するとよいでしょう。

存在しないフォルダを指定して例外メッセージが分かりにくい

もう1つ、存在しないパスを渡したときのエラーが分かりにくいという指摘を受けたことがあります。

❌ Before(例外がそのまま表示される)

python organize.py /存在しないフォルダ
Traceback (most recent call last):
  File "organize.py", line 35, in <module>
    main()
  File "organize.py", line 31, in main
    organize_files(args.target_dir, dry_run=args.dry_run)
  File "organize.py", line 8, in organize_files
    raise NotADirectoryError(f"{target_dir} はフォルダではありません")
NotADirectoryError: /存在しないフォルダ はフォルダではありません

エンジニア以外がこのツールを使う場合、Tracebackはかえって不安を与えてしまいます。

✅ After(main側でtry-exceptしてメッセージだけ表示する)

def main():
    parser = argparse.ArgumentParser(description="フォルダ内のファイルを拡張子ごとに整理するツール")
    parser.add_argument("target_dir", nargs="?", default=".", help="整理したいフォルダのパス")
    parser.add_argument("--dry-run", action="store_true", help="実際には移動せず対象だけを表示する")
    args = parser.parse_args()

    try:
        organize_files(args.target_dir, dry_run=args.dry_run)
    except NotADirectoryError as e:
        print(f"エラー: {e}")
        raise SystemExit(1)
python organize.py /存在しないフォルダ
エラー: /存在しないフォルダ はフォルダではありません

Tracebackを出さず、必要な情報だけを表示することで、利用者にとって読みやすいツールになります。

raise SystemExit(1)で終了コードを1にしておくと、シェルスクリプトから呼び出したときにエラー検知もしやすくなります。

まとめ

この記事のポイント

  • argparseは標準ライブラリだけでCLIツールの引数解析ができる
  • add_argumentで位置引数・オプション引数の両方を定義できる
  • --helpdescriptionhelp引数から自動生成される
  • --dry-runのような確認用オプションを用意すると実務で安心して使える
  • 利用者向けのツールでは、例外をtry-exceptで捕まえて分かりやすいメッセージに変換する

次に読むべき記事

  • フォルダ内のファイルを自動で整理するスクリプトを作る
  • 循環importでハマった話と解決法
  • try-exceptで例外処理を書く基本

タグ: Python, 上級者向け, 実践プロジェクト

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