Python
cli

Python(Click)でデフォルト安全なコマンドラインツールを作る

サーバ管理業務の自動化のため、Pythonでコマンドラインツールを作るということはわりと多いかと思います。
普段自分がコマンドラインツールを作るときには、そのツールを利用した際にオペレーションミスが発生する可能性を低くするために以下の点を心がけています。

  • dry-runオプションを用意する
  • 実動作には明示的なオプション指定を必須にする(デフォルト動作をdry-runする)

これをPythonのサードパーティ製コマンドラインパーサーであるClickを使ったやり方をまとめました。

コマンドラインパーサー

Pythonのコマンドラインパーサーは、標準モジュールだとargparseがありますが、より少ない記述量でかつ実装が楽なClickがオススメです。

以下の例はバージョン6.7で試しています。

pip install click==6.7

次のようにエンドポイントとなる関数にoption()デコレーターでオプションを設定します。コマンドラインから指定された値は引数として受け取って関数の中で利用できます。

echo.py
#!/usr/bin/env python

import click


@click.command()
@click.option('--text', help='Print text')
def echo(text):
    print("%s" % text)


if __name__ == "__main__":
    echo()
$ python echo.py --text "Hello World"
Hello World

オプションのタイプも豊富に用意されていて、フラグオプションのBoolean flags、選択オプションのChoice Optionsなど、必要なものはひと通り揃っています。

dry-runオプションを用意する

コマンドラインツールを実行したときに、どのリソースに対してどんな操作が行われるかを事前に確認したいことはよくあると思います。
そのようなときのために、dry-runオプションを用意しておくとよいでしょう。事前に確認することで意図しないリソースに変更を加えてしまうというような事故を防ぐことができます。

--pathオプションで指定したファイルを削除するプログラムを例とすると、--dry-runオプションによって削除対象のファイルが正しいかを確認できるようにしておきます。

clickを利用する場合だと、Boolean flagsを利用してdry-runオプションを実装できます。

delete.py
#!/usr/bin/env python

import os
import sys

import click

@click.command()
@click.option('--path', help='Target path')
@click.option('--dry-run', is_flag=True, help='Dryrun')
def delete(path, dry_run):
    if dry_run:
        print("Dry-run: %s is deleted." % path)
        sys.exit(0)

    os.remove(path)
    print("%s is deleted." % path)

if __name__ == "__main__":
    delete()
$ python delete.py --path demo.txt --dry-run
Dry-run: demo.txt is deleted.

実動作には明示的なオプション指定を必須にする(デフォルト動作をdry-runする)

破壊的な操作を行うコマンドラインツールの場合は、意図しない操作によって実害が出る可能性を減らすために、デフォルトの動作をdry-run同様にしておき、明示的にオプション指定しない限り実際の操作が行われないようにします。

先程のファイル削除のプログラムを少し変えてみます。
--deleteオプションを明示的に指定しない場合はDry runが実行されるようにしています。

delete_2.py
#!/usr/bin/env python

import os
import sys

import click

@click.command()
@click.option('--path', help='Target path')
@click.option('--delete', is_flag=True, help='Actually delete file')
def delete(path, delete):
    if delete:
        os.remove(path)
        print("%s is deleted." % path)
    else:
        print("Dry-run: %s is deleted." % path)
        sys.exit(0)


if __name__ == "__main__":
    delete()
$ python delete_2.py --path demo.txt
Dry-run: demo.txt is deleted.
$ python delete_2.py --path demo.txt --delete
demo.txt is deleted.

まとめ

  • Clickはオススメ
  • 実用性より安全性を優先する