【備忘録】PythonでCLIを作成

PythondでCLIを作成するときの備忘録です。


環境は以下の通りです。

### 環境
* Python 3.7

# argparseモジュール
https://docs.python.org/ja/3/library/argparse.html


## 通常

### コマンドライン引数のグループ化
`add_argument_group`で、コマンドライン引数をグループ化できます。
共通のオプションなどは、グループ化するとヘルプ表示が見やすくなります。

```python
    group = parser.add_argument_group("global optional arguments")
    group.add_argument('--yes', action="store_true", help="処理中に現れる問い合わせに対して、常に'yes'と回答します。")
    group.add_argument('--logdir', type=str, default=".log", help="ログファイルを保存するディレクトリを指定します。")

```

```
$ python test_command.py -h
optional arguments:
  -h, --help       show this help message and exit

global optional arguments:
  --yes            処理中に現れる問い合わせに対して、常に'yes'と回答します。
  --logdir LOGDIR  ログファイルを保存するディレクトリを指定します。
```

### 相互排他
オプション`--foo`と`--bar`を同時に指定することを禁止する場合、`add_mutually_exclusive_group`メソッドを使います。
https://docs.python.org/ja/3/library/argparse.html#mutual-exclusion

### 共通のオプション引数
各コマンドで共通の引数を指定したい場合、`ArgumentParser`の`parents`引数を利用します。
https://docs.python.org/ja/3/library/argparse.html#parents

### version情報の表示
`action="version"`を指定します。

```python
import argparse
parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('--version', action='version', version='%(prog)s 2.0')
parser.parse_args(['--version'])
```

https://docs.python.org/ja/3/library/argparse.html#action 引用


### デフォルトで、長いオプションは先頭の一文字に短縮可能
>parse_args() メソッドは、デフォルトで、長いオプションに曖昧さがない (先頭の文字が一意である) かぎり、先頭の一文字に短縮して指定できます

https://docs.python.org/ja/3/library/argparse.html#argument-abbreviations-prefix-matching

`allow_abbrev`を`False`をすることで、この機能は無効化されます。


# サブコマンド関係

### 1階層のサブコマンド
gitコマンドのように、`my_git push`、`my_git pull`のようなサブコマンドを作成します。

サブコマンドに対応する関数は、`set_defaults()`で紐づけられます。

```my_git.py
import argparse

def push(args):
    print("Execute push command")
    print(args)

def pull(args):
    print("Execute pull command")
    print(args)
    
def main():
    parser = argparse.ArgumentParser(prog='PROG')
    subparsers = parser.add_subparsers(help='sub-command help')

    # pull コマンドのパーサを作成
    parser_push = subparsers.add_parser('push', help='push help')
    parser_push.add_argument('--bar', type=str, help='bar help')
    parser_push.set_defaults(subcommand_func=push)

    # push コマンドのパーサを作成
    parser_pull = subparsers.add_parser('pull', help='pull help')
    parser_pull.add_argument('--foo', type=str, help='foo help')
    parser_pull.set_defaults(subcommand_func=pull)

    args = parser.parse_args()

    if hasattr(args, 'subcommand_func'):
        # set_defaultsで割り当てた関数を実行する
        args.subcommand_func(args)

    else:
        # pull or pushが指定されない場合
        parser.print_help()

if __name__ == "__main__":
    main()
```


```plain:console
$ python my_git.py push --bar hoge
Execute push command
Namespace(bar='hoge', subcommand_func=<function push at 0x0000027268F4C268>)

$ python my_git.py pull --foo hoge
Execute pull command
Namespace(foo='hoge', subcommand_func=<function pull at 0x000002134792CD90>)

$ python my_git.py
usage: my_git.py [-h] {push,pull} ...

positional arguments:
  {push,pull}  sub-command help
    push       push help
    pull       pull help

optional arguments:
  -h, --help   show this help message and exit

```

### 2階層のサブコマンド
[AWS CLI](https://aws.amazon.com/jp/cli/)のように、`my_aws ec2 wait`、`my_aws sns publish`のようなサブコマンドを作成します。

1つ目のサブコマンド、2つ目のサブコマンドが指定されない場合は、ヘルプを表示するようにしました。

```my_aws.py
import argparse

def wait(args):
    print("Execute ec2 wait command")
    print(args)

def publish(args):
    print("Execute sns publish command")
    print(args)
    
def main():
    parser = argparse.ArgumentParser()
    parser.set_defaults(command_help=parser.print_help)
    subparsers = parser.add_subparsers(help='sub-command help')

    # ec2 コマンドのパーサを作成
    parser_ec2 = subparsers.add_parser('ec2', help='ec2 help')
    parser_ec2.set_defaults(command_help=parser_ec2.print_help)
    subparser_ec2 = parser_ec2.add_subparsers()

    # ec2 wait コマンドのパーサ作成
    parser_ec2_wait = subparser_ec2.add_parser('wait', help='ec2 wait help')
    parser_ec2_wait.set_defaults(subcommand_func=wait)
    
    # sns コマンドのパーサを作成
    parser_sns = subparsers.add_parser('sns', help='sns help')
    parser_ec2.set_defaults(command_help=parser_ec2.print_help)
    subparser_sns = parser_sns.add_subparsers()

    # sns publish コマンドのパーサ作成
    parser_sns_publish = subparser_sns.add_parser('publish', help='sns publish help')
    parser_sns_publish.set_defaults(subcommand_func=publish)

    args = parser.parse_args()

    if hasattr(args, 'subcommand_func'):
        args.subcommand_func(args)

    else:
        # pull or pushが指定されない場合
        args.command_help()

if __name__ == "__main__":
    main()

```


```plain:Console
$ python my_aws.py
usage: my_aws.py [-h] {ec2,sns} ...

positional arguments:
  {ec2,sns}   sub-command help
    ec2       ec2 help
    sns       sns help

optional arguments:
  -h, --help  show this help message and exit


$ python my_aws.py ec2
usage: my_aws.py ec2 [-h] {wait} ...

positional arguments:
  {wait}
    wait      ec2 wait help

optional arguments:
  -h, --help  show this help message and exit


$ python my_aws.py sns publish
Execute sns publish command
Namespace(command_help=<bound method ArgumentParser.print_help of ArgumentParser(prog='my_aws.py', usage=None, description=None, formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)>, subcommand_func=<function publish at 0x00000286B96BCD90>)

```

# その他

### WIP コマンドライン引数 or ファイルで渡す場合