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

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

Pythonでコマンドラインアプリケーション（python製の[`httpie`](https://httpie.org/)など）を作るときは、
以下のように `setup.py` で **entry_point** を書くのが主流になっていますよね。

```py: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
```

このように引数をとります。（`PUT`、`http://httpbin.org/put`、`hello=world`すべてが引数です）
Pythonでコマンドライン引数を定義・取得するには**argparse**という標準ライブラリがあります。

```py
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で作ったパーサーを使ってコマンドライン引数を取得します。

```py:__main__.py

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

def main(argv=sys.argv):
    
    args = parser.parse_args()

```

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

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

```py:__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](https://ja.wikipedia.org/wiki/%E7%B5%82%E4%BA%86%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9)

終了ステータスについての詳細は各自で調べて下さい。
とにかく、アプリケーションが終了する際に、**そのアプリケーションが正しく終了したのか、エラーを吐いて異常な終了をしたのかを判別するための整数** ですね。これをmain関数に組み込みましょう。

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

```py:__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                          
```

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

```py:__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    
```

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

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

１. argparseで作ったパーサーに**--stack-trace**引数を追加
2. エラーが出たときに`--stack-trace`引数があった場合は**traceback**ライブラリを使用してスタックトレースを出力

```py:cli.py
parser.add_argument(
    "--stack-trace",
    dest="stacktrace",
    action="store_true",
    help="Display the stack trace when error occured.")
```
```py:__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関数の雛形は以下のようになります。

```py:__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 訂正

上記コードの

```py

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

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


```py

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