Python
commandline
command-line
Python2

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__ でできると知りました