横着な人のための極力労力を使わないargparseの使い方の紹介

## はじめに

何らかのコマンド(CLI)を作りたい場合に[argparse](https://docs.python.jp/3/library/argparse.html)を使ったりしますね。このargparseをなるべく労力を使わず楽して便利に使うためのtipsを紹介します。

## `sys.argv` を渡す必要なし

幾つかのargparseの使い方の紹介では、`sys.argv`をまじめに渡していますが。不要です。無引数で `parse_args()` を呼ぶとよしなにやってくれます。


```python
import argparse


parser = argparse.ArgumentParser()
parser.parse_args()  # これでOK
```

## ヘルプメッセージの表示もargparseに任せよう

`-h` または `--help` をつけるとヘルプメッセージが出る機能はargparseが自動で付けてくれます。そしてその上で、利用方法を間違えてしまった場合の挙動もargparseに委ねてしまうのが便利です。

例えば、何も引数を指定せず実行してしまった場合などのためにこのように親切なコードを書く必要はありません。

```python
import sys

# このコードは不要
if len(sys.argv) == 0:
    parser.parse_args(["-h"])
```

この記事の良いね数が多かったので不要だよということを伝えたかっただけです。

- [Pythonでコマンドラインアプリケーションを作るときの雛形 - Qiita](http://qiita.com/Alice1017/items/0464a38ab335ac3b9336#python%E3%81%A7%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E4%BD%9C%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AE%E9%9B%9B%E5%BD%A2)

追記:

このコードの意図は、エラー時のメッセージを短いusageではなく、長いhelpをすべて出したいというものだったそうです。少しhackっぽいですが以下の様に書けます(とは言え、処理内容が明示的にわかる元のコードの方が良いという人もいるかもしれません)。

```python
parser.print_usage = parser.print_help
```

## positional argumentの場合

positional argumentを１つでも指定した場合には自動的に引数の不足が調べられるので何もする必要がありません。


```python
import argparse

parser = argparse.ArgumentParser("<command name>")
parser.add_argument("file")  # positional argument
args = parser.parse_args()
```

```bash
$ python cmd.py
usage: <command name> [-h] file
<command name>: error: the following arguments are required: file
```

## optionの場合

全てがオプションだった場合には、必須そうなものに `required=True` を付けてください(とは言え必須ならpositional argumentするべきという話もあります)。

```python
import argparse

parser = argparse.ArgumentParser("<command name>")
parser.add_argument("--file", required=True)
args = parser.parse_args()
```

```bash
$ python cmd.py
usage: <command name> [-h] --file FILE
<command name>: error: the following arguments are required: file
```

## デフォルトの引数を指定して楽をしよう

何も引数を指定せずに実行してしまった場合の挙動としてもう１つ挙動が考えられます。デフォルト値を埋め込んで処理を行うことです。これは `default` で指定してしまえば良いです。


例えば、ファイルを指定しなかった場合には標準入力からデータを受け取るみたいなコマンドを作りたいという場合があるかもしれません。


### positional argument の場合
positional argument で指定していた場合には、`nargs="?"` を使ってオプショナルにします

```python
import sys
import argparse


parser = argparse.ArgumentParser("hai")
parser.add_argument("file", nargs="?", type=argparse.FileType("r"), default=sys.stdin)
args = parser.parse_args()

print(args.file.read())
```

こちらが動くようになります。


```bash
$ cat hello.txt | python echo.py
$ python echo.py hello.txt
```

## optionの場合

オプション引数の場合にはdefaultを指定するだけです。

```python
parser.add_argument("--file", type=argparse.FileType("r"), default=sys.stdin)
```


## actionを使って楽をしよう

parserのadd_argumentが受け取るactionを覚えておくと便利です。全部は覚える必要はありませんが幾つかのactionは覚えておくと楽ができます。

### option引数にはstore_true/store_false

例えば、`--with-<something>` みたいなフラグだったり、 `--debug` みたいな引数を取らないオプションを取るときにstore_trueを使いましょう。default値はFalseになります

```py
parser.add_argument("--debug", action="store_true")
```

逆に、`--without-<something>` というフラグだったり、defaultをTrueにしてオプションが指定された場合にはFalseになる場合にはstore_falseを使いましょう。

```py
parser.add_argument("--execute", action="store_false")  # defaultはdry-runみたいな状況
```

注意点として `with-foo` と `without-foo` というフラグの両方を提供したい場合には、set_defaults()でデフォルト値を指定しておいたほうが良いです。そうでないと書いた順序にデフォルトの値が依存してしまうので。

```python
# defaultはdebug=False
parser.add_argument("--without-debug", action="store_false", dest="debug")
parser.add_argument("--with-debug", action="store_true", dest="debug")
parser.set_defaults(debug=False)
```


### 同じオプションに複数の値を渡したい場合にはappend

複数の値を取りたい場合にはappendをactionに指定しましょう。

```python
import argparse


parser = argparse.ArgumentParser()
parser.add_argument("--target", action="append")
args = parser.parse_args()
print(args)
```

以下のように動きます。

```bash
$ python cmd.py --target x --target y --target z
Namespace(target=['x', 'y', 'z'])
```

## 取りうる値の範囲を制限したい場合にはchoicesを使おう

取りうる値の範囲を制限したい場合にはchoicesを使うと便利です。例えばloggingのレベルを指定する部分については以下の様に書くと良いです。

```python
import argparse
import logging

parser = argparse.ArgumentParser()
loglevels = list(logging._nameToLevel.keys())  # _　prefixのものなのでお行儀は良くない
parser.add_argument("--loglevel", choices=loglevels, default="INFO")

args = parser.parse_args()
logging.basicConfig(level=args.loglevel)
logger = logging.getLogger("log")

logger.debug("hmm..")
logger.info("hmm")
```

defaultはinfoが良い気がします。


```bash
$ python cmd.py
INFO:log:hmm
$ python cmd.py -h
usage: cmd.py [-h]
                    [--loglevel {WARN,INFO,NOTSET,DEBUG,CRITICAL,WARNING,ERROR}]

optional arguments:
  -h, --help            show this help message and exit
  --loglevel {WARN,INFO,NOTSET,DEBUG,CRITICAL,WARNING,ERROR}
$ python cmd.py --loglevel=DEBUG
DEBUG:log:hmm..
INFO:log:hmm
```

## argparseのドキュメントを読もう

公式ドキュメントの説明が詳しく役に立つのでこんな記事を読んでいるよりも公式ドキュメントを見た方が有益です。公式ドキュメントを見ましょう。

- https://docs.python.jp/3/library/argparse.html#module-argparse

## おまけ

### サブコマンド

サブコマンドを作りたい場合には早々にargparseを諦めて、[click](http://click.pocoo.org/)を使ってしまいましょう。

### 関数の定義からコマンドに変換する

後は関数の定義からコマンドとして実行可能にするパッケージを何か１つ覚えておくと便利です([自分は自作しました](https://github.com/podhmo/handofcats))。

例えば以下の様なものです。

```python
# greeting.py
from handofcats import as_command

@as_command
def greeting(message, is_surprised=False, name="foo"):
    suffix = "!" if is_surprised else ""
    print("{name}: {message}{suffix}".format(name=name, message=message, suffix=suffix))
```

```bash
$ pip install handofcats
$ python greeting.py -h
python greeting.py -h
usage: greeting.py [-h] [--is-surprised] [--name NAME] [-v] [-q] message

positional arguments:
  message

optional arguments:
  -h, --help      show this help message and exit
  --is-surprised
  --name NAME
  -v, --verbose   (default option: increment logging level(default is
                  WARNING))
  -q, --quiet     (default option: decrement logging level(default is
                  WARNING))
$ python greeting.py
foo: hello
$ python greeting.py --is-surprised --name me hello
me: hello!
```