alice1017
alice1989.qrunch.io

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

2018/10/16に投稿

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

Pythonでコマンドラインアプリケーション(python製のhttpieなど)を作るときは、 以下のように setup.pyentry_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

このように引数をとります。(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で作ったパーサーを使ってコマンドライン引数を取得します。


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__
        sys.stderr.write("{0}: {1}\n".format(error_type, e.message)) # 最初の1行だけ出力
        sys.exit(1) # 終了ステータスは0以外の整数の場合、異常終了を指す

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

$ my-console-app -m 1111
TypeError: Message must be string not integer    

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

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

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

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

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


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

def main(argv=sys.argv):

    args = parser.parse_args(argv)

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

    except Exception as e:
        error_type = type(e).__name__
        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)

ヘルプメッセージの出力

argparseの場合、何もコマンドライン引数を設定せずにアプリケーションを起動すると、

usage: setup-script [-h] [-l] [--stack-trace] formula project
setup-script: error: too few arguments

このようなエラーメッセージ(引数がないよという意味)がでてしまうので、この「引数がないよ」というエラーメッセージを出さず、ヘルプを表示してあげましょう。(「引数がない」と ただ言うのではなく、ヘルプを表示して「どのように使うのか」を明示してあげよう、という意味です。)

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

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

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


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

def main(argv=sys.argv):

    if len(argv) == 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__
         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)

関連記事

コメントはありません。
alice1017
alice1989.qrunch.io
フォロー
フォロワー
ブログを開設

クランチで技術ブログを
始めてみませんか?

この先は、クランチへのアカウント登録、及びログインが必要なページになります。

Markdownの書き方
見出し # 見出し(h1)
## 見出し(h2) , ### 見出し(h3) ...
リスト - 箇条書き
   - タブでインデント
番号付きリスト 1. テキスト
2. テキスト
改行 行末に半角スペース2つ
リンクの挿入 [タイトル](https://xxx.com)
引用 > テキスト
コード挿入 ```cpp:title
code
```
画像の挿入 ![代替テキスト](URL "タイトル")
太字 **テキスト**
斜体 *テキスト*
打消し線 ~~テキスト~~
水平線 ***
技術ブログを開設
ログイン