Skip to content

Instantly share code, notes, and snippets.

@podhmo
Last active August 13, 2017 14:43
Show Gist options
  • Save podhmo/f55ad792fb4aa8464042b878e918ec9e to your computer and use it in GitHub Desktop.
Save podhmo/f55ad792fb4aa8464042b878e918ec9e to your computer and use it in GitHub Desktop.

pythonでコマンドラインアプリケーション作る話

これの記事を見てなるほど~と思ったついでに自分ならどうするか考えてみた。

memo:

  • コマンドラインアプリケーションは、setup.py経由でインストールされるcliのコマンドのことを指しているっぽい
  • 主題はmainをどうやって書くかという話

提案していること

提案しているのはどういうものがあるかというと、以下のようなもの。

  • parserをfoo.cliに書いてimport
  • setup.pyのconsole_scriptsからは foo.__main__:main を呼び出す
  • 終了statusでexit
  • エラーメッセージを短くする(default)
  • 全部表示させたいときの --stack-trace オプションを追加

parserをfoo.cliに書いてimport

これは自分の場合はしないかも、理由はargparseのparesrは他で使うことがないので。こういう感じに書くことが多いかも(sys.exitはいつもはrunの方にかいていた)。

from foo import do_something

def run(x, y, z):  # 引数は適切なもの
    # いつもはここで直接sys.exitしていた
    return do_something(x, y, z)


def main():
    import argparse
    parser = argparse.ArgumentParser()

    parser.add_argument("-x")
    parser.add_argument("-y")
    parser.add_argument("-z")

    args = parser.parse_args()
    # 今までしていなかったけれど。ここでsys.exitするのは良いかも?
    sys.exit(run(args.x, args.y, args.z))

setup.pyのconsole_scriptsからは foo.__main__:main を呼び出す

これは純粋になるほどなーと思った。1パッケージ1コマンドにするならこれもありかも。というのも __main__ で書くということは python -m foo でも呼べるといことなので。

個人的には、いつも foo/cli.py, foo/cmd.py, foo/command.py, foo/__init__.py あたりに main() を書いていた

エラーメッセージを短くする(default)

これはいつもしないことが多かった。常にstack trace出た方が良いじゃんという立場だったりもしたけれど。エラー時にコンソールにいっぱい出力される系のコマンドやっぱりうざかったりする?のかもしれない。自分ならこういう感じに書くかも。エラーのクラス名は直接クラスから取ってくる。あと、エラーに __str__() が実装されている場合を尊重して直接printする。

# これを
def main():
    # .. snip
    sys.exit(run(args.x, args.y, args.z))

# こうする
def main():
    # .. snip
    try:
        sys.exit(run(args.x, args.y, args.z))
    except Exception as e:
        print("{e.__class__.__name__}: {e}".format(e=e, file=sys.stderr)
        sys.exit(1)

あと、これと一緒にtrace back表示するオプションをつけるとしたら。 --debug が付いていたらstack traceを表示にしちゃうかも。その場合は単にraiseしちゃう。

def main():
    # .. snip
    parser.add_argument("--debug", action="store_true", default=False)
    # .. snip
    try:
        sys.exit(run(args.x, args.y, args.z))
    except Exception as e:
        if args.debug:
            raise
        print("{e.__class__.__name__}: {e.args[0]}".format(e=e), file=sys.stderr)
        sys.exit(1)

実際の例

$ make
python cli.py  || echo "err"
10 / 2 = 5.0

python cli.py -x 10 -y 3  || echo "err"
10 / 3 = 3.3333333333333335

python cli.py -y 0  || echo "err" # error
ZeroDivisionError: division by zero

err

python cli.py -y 0  --debug  || echo "err"  # error(verbose)
Traceback (most recent call last):
  File "cli.py", line 31, in <module>
    main()
  File "cli.py", line 22, in main
    sys.exit(run(args.x, args.y))
  File "cli.py", line 9, in run
    div(x, y)
  File "cli.py", line 5, in div
    print("{} / {} = {}".format(x, y, x / y))
ZeroDivisionError: division by zero
err
import sys
def div(x, y):
print("{} / {} = {}".format(x, y, x / y))
def run(x, y):
div(x, y)
return 0
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-x", default=10, type=int)
parser.add_argument("-y", default=2, type=int)
parser.add_argument("--debug", action="store_true", default=False)
args = parser.parse_args()
try:
sys.exit(run(args.x, args.y))
except Exception as e:
if args.debug:
raise
print("{e.__class__.__name__}: {e}".format(e=e), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
default:
python cli.py || echo "err"
python cli.py -x 10 -y 3 || echo "err"
python cli.py -y 0 || echo "err" # error
python cli.py -y 0 --debug || echo "err" # error(verbose)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment