Edited at

Pythonでコマンドラインアプリケーションを作るときの雛形

More than 1 year has passed since last update.


Pythonでコマンドラインアプリケーションを作るには

Pythonでコマンドラインアプリケーション(python製のhttpieなど)を作るときは、

以下のように setup.pyentry_point を書くのが主流になっていますよね。


setup.py

from setuptools import setup

setup(
# ...中略...
entry_points={
"console_scripts": [
"my-console-app=my_console_app.__main__:main"
]
},
# ...中略...
)


上記の場合、my_console_appパッケージの __main__.py に書かれている main 関数を my-console-app というアプリケーションとして定義しますよ、という内容です。

そして、大切なのはこの main関数をどのように書くか です。今回の記事は自分がコンソールアプリケーションを作ってきた中で定着してきたmain関数の雛形を紹介しようと思います


main関数に何を書くべきか

main関数に必要なのは


  • argparseを使ったコマンドライン引数の取得

  • アプリケーション内部の実装

  • 終了ステータスの定義

  • エラーの出力

だと思います。


argparseを使ったコマンドライン引数の取得

コマンドラインアプリケーションは引数を取ることが普通です。たとえば例にあげたhttpieの場合ですと、

$ http PUT http://httpbin.org/put hello=world

このように引数をとります。(PUThttp://httpbin.org/puthello=worldすべてが引数です)

Pythonでコマンドライン引数を定義・取得するにはargparseという標準ライブラリがあります。

from argparse import ArgumentParser

parser = ArgumentParser(
prog="my-console-app",
description="This is my console application.")

parser.add_argument(
"message",
action="store",
help="The message string.")

こういう感じでコマンドライン引数を定義します。

argparseについて詳しく書くのは、記事が無駄に長くなりますのでやめておきます。各自で調べて下さい。

main関数ではこのargpaseで作ったパーサーを使ってコマンドライン引数を取得します。


__main__.py


from .cli import parser # パッケージ内部のcli.pyファイルにparserを定義しておく

def main(argv=sys.argv):

args = parser.parse_args()



アプリケーション内部の実装

次に、アプリケーション本体の実装が必要です。

このアプリケーションには上記で取得したコマンドライン引数を用いて、出力する内容を変えますので、


__main__.py


from .cli import parser
from .core import program # パッケージ内部のcore.pyにアプリケーションを実装しておく

def main(argv=sys.argv):

args = parser.parse_args()
program(args) # アプリケーションの実行にコマンドライン引数が必要なため、引数にargsをとる


このようにパース後のコマンドライン引数を、プログラムの引数にとってあげると良いです


終了ステータスの定義


終了ステータスとは、コンピュータプログラミングにおけるプロセスの終了ステータス(英: exit status)またはリターンコード(英: return code)とは、子プロセス(または呼び出された側)が具体的な手続きや委任されたタスクを実行完了した際、親プロセス(または呼び出した側)に渡す小さな数である。

終了ステータス - wikipedia


終了ステータスについての詳細は各自で調べて下さい。

とにかく、アプリケーションが終了する際に、そのアプリケーションが正しく終了したのか、エラーを吐いて異常な終了をしたのかを判別するための整数 ですね。これをmain関数に組み込みましょう。

この終了ステータスですが、main関数に定義するよりも、アプリケーション内部のプログラムが終了ステータスを返すように実装すると良いと思われます。どういうことかというと、


__main__.py


import sys
from .cli import parser
from .core import program

def main(argv=sys.argv):

args = parser.parse_args()
exit_status = program(args) # アプリケーション内部のプログラムが終了ステータスを返すように実装するといい

return sys.exit(exit_status)


こういうことです。


エラーの出力

コンソールで動くアプリケーションなのでエラーもちゃんと出力してあげたほうがユーザーに優しいですよね。

$ my-console-app -m 1111

Traceback (most recent call last):
File "/home/vagrant/.anyenv/envs/pyenv/versions/2.7.5/lib/python2.7/runpy.py", line 162, in _run_module_as_main
"__main__", fname, loader, pkg_name)
File "/home/vagrant/.anyenv/envs/pyenv/versions/2.7.5/lib/python2.7/runpy.py", line 72, in _run_code
exec code in run_globals
File "/home/vagrant/lab/Python/my-console-app/my_console_app/__main__.py", line 63, in <module>
main()
File "/home/vagrant/lab/Python/my-console-app/my_console_app/__main__.py", line 52, in main
exit_code = program(args)
File "/home/vagrant/lab/Python/my-console-app/my_console_app/__main__.py", line 34, in program
print prettyprint(result)
File "my_console_app/utils.py", line 50, in prettyprint
raise TypeError("Message must be string not integer")
TypeError: Message must be string not integer

伝えたいエラーは最後の一文だけ なのですが、こんなにいっぱい情報が出てくると何もしらないユーザーはビビりますよね。なのでこのエラー形式を、最後の一文だけ表示されるようにします。


__main__.py


import sys
from .cli import parser
from .core import program

def main(argv=sys.argv):

args = parser.parse_args()

try:
exit_status = program(args) # アプリケーション内部のプログラムが終了ステータスを返すように実装するといい
return sys.exit(exit_status)

except Exception as e:
error_type = type(e).__name__ # 29.6.2追記:これでエラー名を取得できる
sys.stderr.write("{0}: {1}\n".format(error_type, e.message))
sys.exit(1) # 終了ステータスは0以外の整数の場合、異常終了を指す


このようなコードを書くと、エラーが以下のように出力されます。

$ my-console-app -m 1111

TYpeError: Message must be string not integer

見やすくなりましたね!!

でもこれじゃあアプリケーション内部のエラーの詳細が見えないじゃないか、と思った方。

対処法はございます。

1. argparseで作ったパーサーに--stack-trace引数を追加

2. エラーが出たときに--stack-trace引数があった場合はtracebackライブラリを使用してスタックトレースを出力


cli.py

parser.add_argument(

"--stack-trace",
dest="stacktrace",
action="store_true",
help="Display the stack trace when error occured.")


__main__.py


import sys
import traceback
from .cli import parser
from .core import program

def main(argv=sys.argv):

if len(argv) == 1:
# 2017.04.29訂正
# len(argv) == 0ではなく1でした。失礼しました
parser.parse_args(["-h"])

args = parser.parse_args(argv)

try:
exit_code = program(args)
sys.exit(exit_code)

except Exception as e:
error_type = type(e).__name__ # 29.6.2追記:これでエラー名を取得できる
stack_trace = traceback.format_exc() # エラーが出たときにスタックトレースを保存しておき、

if args.stacktrace: # --stack-trace引数がある場合はスタックトレースを出力
print "{:=^30}".format(" STACK TRACE ")
print stack_trace.strip()

else:
sys.stderr.write(
"{0}: {1}\n".format(e_type, e.message))
sys.exit(1)



Pythonでコマンドラインアプリケーションを作るときの雛形

つまり、main関数の雛形は以下のようになります。


__main__.py


import sys
import traceback
from .cli import parser
from .core import program

def main(argv=sys.argv):

if len(argv) == 1:
# 2017.04.29訂正
# len(argv) == 0ではなく1でした。失礼しました

parser.parse_args(["-h"]) # コマンドライン引数が無かった場合はヘルプメッセージを出力
sys.exit(0)

args = parser.parse_args(argv)

try:
exit_code = program(args)
sys.exit(exit_code)

except Exception as e:
error_type = type(e).__name__ # 29.6.2追記:これでエラー名を取得できる
stack_trace = traceback.format_exc()

if args.stacktrace:
print "{:=^30}".format(" STACK TRACE ")
print stack_trace.strip()

else:
sys.stderr.write(
"{0}: {1}\n".format(e_type, e.message))
sys.exit(1)



2017.04.29 訂正

上記コードの


if len(sys.argv) == 0:
parser.parse_args(["-h"])

この部分に誤りがありました。正しくは


if len(sys.argv) == 1: # 0 -> 1
parser.parse_args(["-h"])

このコード(わざわざparserからヘルプメッセージを出す)の目的は、argparseの場合、何もコマンドライン引数を設定せずにアプリケーションを起動すると、

usage: setup-script [-h] [-l] [--stack-trace] formula project

setup-script: error: too few arguments

このようなエラーメッセージ(引数がないよという意味)がでてしまうので、このエラーをださない意図で sys.argv が1の場合はparser.parse_args(["-h"])としてヘルプメッセージを出力しています。


2017.6.2 追記

エラーオブジェクトからエラー名を取得するには、 type(e).__name__ でできると知りました