Pythonでコマンドラインアプリケーションを作るには
Pythonでコマンドラインアプリケーション(python製のhttpie
など)を作るときは、
以下のように setup.py
で entry_point を書くのが主流になっていますよね。
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
このように引数をとります。(PUT
、http://httpbin.org/put
、hello=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で作ったパーサーを使ってコマンドライン引数を取得します。
from .cli import parser # パッケージ内部のcli.pyファイルにparserを定義しておく
def main(argv=sys.argv):
args = parser.parse_args()
アプリケーション内部の実装
次に、アプリケーション本体の実装が必要です。
このアプリケーションには上記で取得したコマンドライン引数を用いて、出力する内容を変えますので、
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関数に定義するよりも、アプリケーション内部のプログラムが終了ステータスを返すように実装すると良いと思われます。どういうことかというと、
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
伝えたいエラーは最後の一文だけ なのですが、こんなにいっぱい情報が出てくると何もしらないユーザーはビビりますよね。なのでこのエラー形式を、最後の一文だけ表示されるようにします。
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**ライブラリを使用してスタックトレースを出力
parser.add_argument(
"--stack-trace",
dest="stacktrace",
action="store_true",
help="Display the stack trace when error occured.")
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関数の雛形は以下のようになります。
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__
でできると知りました