127
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

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

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

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
127
Help us understand the problem. What are the problem?