ArgumentParserを使ってpythonのコマンドライン引数をとことん使ってみた

# はじめに

pythonを使っていると、当然コマンドライン引数を使いたくなるプログラムがあります。
コマンドライン引数は拘りだすと色々と大変ですが、pythonには「ArgumentParser」が用意されています。

* ArgumentParserのマニュアル<br>
https://docs.python.org/ja/3/library/argparse.html

これはコマンドライン引数を使う際の、かゆいところまで網羅されたモジュールです。
今回はArgumentParserの使い方を学んだ過程で、こんなことやあんなことをどうすればできるのか、という情報を自分なりにまとめてました。

実行したpython環境は3.7.2です。

# 単純な使い方

とりあえず、引数の指定と表示をするだけの簡単なサンプルをまずは作ってみました。

```python:argparse_sample1.py
# coding: utf-8
import argparse

##########################################
# 単純なパーサの使い方
##########################################

# ------------------------------
# 1.パーサの作成
# ------------------------------
# コマンドライン引数をpythonオブジェクトへ変換するパーサ
parser = argparse.ArgumentParser(description='本プログラムのヘルプ文([-h]または[--help]時に表示)')

# ------------------------------
# 2.引数の追加
# ------------------------------
# 「parser.add_argument('引数名')」で作成

# 位置引数の追加
# 「python sample.py 10」のように引数のみを指定(複数ある場合)
# 位置引数が複数作成された場合、作成順の位置となる。
# 「python sample.py 10 20」→「INT1=10, INT2=20」
parser.add_argument('INT1')
parser.add_argument('INT2')

# オプション引数の追加(引数名に「-」を含ませる)
# 「python sample.py -d 10」のように「-d 10」で引数を指定
# add_argument()内に複数文字列がある場合、いずれも同じオプション引数として使用可能
parser.add_argument('-f', '--foo')

# ------------------------------
# 3.引数の文字列をpythonオブジェクトへ変換
# ------------------------------
# 引数の変換結果を取得
# (引数に「-h/--help」が指定された場合、ヘルプ文を表示し処理を終了)
args = parser.parse_args()

# ------------------------------
# 4.結果表示
# ------------------------------
# 引数へ代入された値を全体表示
print(args)

# 個別に表示する場合、「args.引数名」と指定
print('INT1:', args.INT1)
print('INT2:', args.INT1)

# オプション引数が複数の場合、次の優先度となる
# 「--オプション引数」 > 「-オプション引数」
# ※「--」や「-」のオプション引数が複数の場合、先に記述されたものが優先される。
print('foo:', args.foo)
```

まずは「-h(または--help)」によりヘルプ文を表示してみます。

```:ヘルプの実行結果
python argparse_sample1.py -h
--------------------(コマンド結果)--------------------
usage: argparse_sample1.py [-h] [-f FOO] INT1 INT2

本プログラムのヘルプ文([-h]または[--help]時に表示)

positional arguments:
  INT1
  INT2

optional arguments:
  -h, --help         show this help message and exit
  -f FOO, --foo FOO
```

ArgumentParserで作成したため、最初から整形されたヘルプ文が表示されます。
最初の「usage」行に指定可能な引数情報が表示されます。
続いて、パーサ作成時に記述したヘルプ文が表示されます。
また「positional arguments」が位置引数、「optional arguments」がオプション引数の説明です。
「-f FOO」となっているのは、「-f」オプションに対して「FOO」という引数を指定する必要がある、ということです。
このままでは分かりにくいですが、勿論説明文の追加等が可能です。
詳しくは[後述](#コマンドライン引数の説明文の記述)します。

次に引数を指定した実行結果を見てみます。

```:引数指定の実行結果
python argparse_sample1.py -f 100 10 20
--------------------(コマンド結果)--------------------
Namespace(INT1='10', INT2='20', foo='100')
INT1:10
INT2:10
foo:100
```

「-f 100」はどの位置に配置しても結果は同じです。
上述の通り「10」「20」は順番通り、「INT1」「」INT2」へ代入されました。
また「-f」で指定した値「100」も「foo(-f)」に代入されています。
しかし最初の行のNamespace内を見れば分かる通り、引数は数値型ですが、文字列として代入されています。
これを数値型にする方法は[後述](#typeオプションの使い方)していきます。

# 複雑な使い方

とりあえずヘルプ表示、引数の指定、引数の取得はできました。
しかし実際に使用する際は、今のままでは色々と不足しています。
そのためより複雑な使い方を少しずつ紹介します。

## 説明文を充実させてみる

「-h(または--help)」で表示させるヘルプ文を充実させていきます。

### コマンドライン引数の説明文の記述

```python:argparse_sample2.py
# coding: utf-8
import argparse

##########################################
# ヘルプ文を充実させてみる
# (コマンドライン引数の説明文の充実)
##########################################
help_desc_msg = '本プログラムのヘルプ文([-h]または[--help]時に表示)'
parser = argparse.ArgumentParser(description=help_desc_msg)

# 「help=」を追加し引数の説明を記述
parser.add_argument('hoge', help='引数の説明文')
args = parser.parse_args()

print(args)
```

```:ヘルプの実行結果
python argparse_sample2.py -h
--------------------(コマンド結果)--------------------
usage: argparse_sample2.py [-h] hoge

本プログラムのヘルプ文([-h]または[--help]時に表示)

positional arguments:
  hoge        引数の説明文

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


### プログラムの説明文に冒頭と文末を記述

プログラムの説明文は「description=」で冒頭、「epilog=」に文末を表示します。

```python:argparse_sample2.py
# coding: utf-8
import argparse

##########################################
# ヘルプ文を充実させてみる
# (プログラムの説明文の充実)
##########################################
# 冒頭と文末の説明文追加
help_desc_msg = '==== ヘルプ文開始 ===='
help_epi_msg = '==== ヘルプ文終了 ===='
parser = argparse.ArgumentParser(description=help_desc_msg, epilog=help_epi_msg)

parser.add_argument('hoge')
args = parser.parse_args()

print(args)
```

```:ヘルプの実行結果
python argparse_sample2.py -h
--------------------(コマンド結果)--------------------
usage: argparse_sample2.py [-h] hoge

==== ヘルプ文開始 ====

positional arguments:
  hoge

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

==== ヘルプ文終了 ====
```


### 説明文のフォーマット

|フォーマットクラス|説明|
|---|---|
|デフォルト(HelpFormatter)|改行を無視し折返し|
|RawDescriptionHelpFormatter|プログラムの説明文の冒頭/文末を元の文章に準じ改行|
|RawTextHelpFormatter|元の説明文通りに改行|
|ArgumentDefaultsHelpFormatter|引数のデフォルト値を表示。改行は無視し折返し表示|
|MetavarTypeHelpFormatter|引数の説明文の値表示に「データ型」を表示。改行は無視し折返し表示|

プログラムの説明文の整形は「formatter_class=」により指定可能です。
目的に併せて適切なフォーマットを選択します。

* formatter_classのマニュアル<br>
https://docs.python.org/ja/3/library/argparse.html#formatter-class

#### デフォルト設定(HelpFormatter)の場合

改行は無視され、折返し表示となります。
そのため実行環境により改行位置は異なります。

```python:argparse_sample2.py
# coding: utf-8
import argparse

##########################################
# ヘルプ文を充実させてみる
# (プログラムの説明文の充実)
##########################################
# 説明文の整形(デフォルト)
help_desc_msg ="""寿限無、寿限無
五劫の擦り切れ
海砂利水魚の
水行末 雲来末 風来末
食う寝る処に住む処
藪ら柑子の藪柑子
パイポ　パイポ　パイポのシューリンガン
シューリンガンのグーリンダイ
グーリンダイのポンポコピーのポンポコナーの
長久命の長助
"""
parser = argparse.ArgumentParser(description=help_desc_msg)

parser.add_argument('hoge')
args = parser.parse_args()

print(args)
```

```:ヘルプの実行結果
python argparse_sample2.py -h
--------------------(コマンド結果)--------------------
usage: argparse_sample2.py [-h] hoge

寿限無、寿限無 五劫の擦り切れ 海砂利水魚の 水行末 雲来末 風来末 食う寝る処に住む処 藪ら柑子の藪柑子 パイポ　パイポ　パイポのシュー リンガン
シューリンガンのグーリンダイ グーリンダイのポンポコピーのポンポコナーの 長久命の長助

positional arguments:
  hoge

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


#### RawDescriptionHelpFormatterの場合

プログラムの説明文の冒頭/文末を元の文章に準じた改行をします。
ただし引数の説明文におけるヘルプ文の改行は無視されます。
(正確には自動折返しになります)

```python:argparse_sample2.py
# coding: utf-8
import argparse

##########################################
# ヘルプ文を充実させてみる
# (プログラムの説明文の充実)
##########################################
# 説明文の整形(RawDescriptionHelpFormatter)

# プログラムの説明の冒頭と文末部分
help_desc_msg ="""プログラムの説明文
(ここから読んでね)
"""
help_epi_msg = """説明完了
(終わりだよ)
"""

# RawDescriptionHelpFormatterの場合
parser = argparse.ArgumentParser(description=help_desc_msg,
                                 epilog=help_epi_msg,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)

# 引数のヘルプ文を追加
parser.add_argument('hoge', help='説明\nするよ')
args = parser.parse_args()

print(args)
```

```:ヘルプの実行結果
python argparse_sample2.py -h
--------------------(コマンド結果)--------------------
usage: argparse_sample2.py [-h] hoge

プログラムの説明文
(ここから読んでね)

positional arguments:
  hoge        説明 するよ

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

説明完了
(終わりだよ)
```

argparse_sample2.py内では'説明\nするよ'と改行を挟んでいますが、上述の通り空白になっています。


#### RawTextHelpFormatterの場合

ヘルプ文全てを元の文章に準じた改行をします。
引数の説明文の改行も反映されます。

```python:argparse_sample2.py
# coding: utf-8
import argparse

##########################################
# ヘルプ文を充実させてみる
# (プログラムの説明文の充実)
##########################################
# 説明文の整形(RawTextHelpFormatter)

# プログラムの説明の冒頭と文末部分
help_desc_msg ="""プログラムの説明文
(ここから読んでね)
"""
help_epi_msg = """説明完了
(終わりだよ)
"""

# RawTextHelpFormatterの場合
parser = argparse.ArgumentParser(description=help_desc_msg,
                                 epilog=help_epi_msg,
                                 formatter_class=argparse.RawTextHelpFormatter)

# 引数のヘルプ文を追加
parser.add_argument('hoge', help='説明\nするよ')
args = parser.parse_args()

print(args)
```

```:ヘルプの実行結果
python argparse_sample2.py -h
--------------------(コマンド結果)--------------------
usage: argparse_sample2.py [-h] hoge

プログラムの説明文
(ここから読んでね)

positional arguments:
  hoge        説明
              するよ

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

説明完了
(終わりだよ)
```

argparse_sample2.py内で記述した通り'説明\nするよ'に改行が挟まれました。


#### ArgumentDefaultsHelpFormatterの場合

引数のデフォルト値を表示します。
詳細はこちらで説明しますが、引数にはデフォルト値が設定可能です。
通常のヘルプ文章では表示されませんが、ArgumentDefaultsHelpFormatterにより表示可能です。


```python:argparse_sample2.py
# coding: utf-8
import argparse

##########################################
# ヘルプ文を充実させてみる
# (プログラムの説明文の充実)
##########################################
# 説明文の整形(ArgumentDefaultsHelpFormatter)

# プログラムの説明の冒頭と文末部分
help_desc_msg ="""プログラムの説明文
(ここから読んでね)
"""
help_epi_msg = """説明完了
(終わりだよ)
"""

# ArgumentDefaultsHelpFormatterの場合
parser = argparse.ArgumentParser(description=help_desc_msg,
                                 epilog=help_epi_msg,
                                 formatter_class=argparse.ArgumentDefaultsHelpFormatter)

# 引数の引数の数とデフォルト値を追加
parser.add_argument('hoge', nargs='?', default='ABC',  help='説明\nするよ')
args = parser.parse_args()

print(args)
```

位置引数にデフォルト値を追加するために、nargsの設定も行っています。
この件に関する詳細は後術します。

```:ヘルプの実行結果
python argparse_sample2.py -h
--------------------(コマンド結果)--------------------
usage: argparse_sample2.py [-h] [hoge]

プログラムの説明文 (ここから読んでね)

positional arguments:
  hoge        説明 するよ (default: ABC)

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

説明完了 (終わりだよ)
```

上述の通り、デフォル値「(default: ABC)」が説明文に追加されました。
ただしその他の整形はデフォルト設定のため、改行が無視されている点に注意してください。

#### MetavarTypeHelpFormatterの場合

引数の説明文の値表示に「データ型」を表示します。
何を言っているか分かりにくいので実行結果を見たほうが早いです。

```python:argparse_sample2.py
# coding: utf-8
import argparse

##########################################
# ヘルプ文を充実させてみる
# (プログラムの説明文の充実)
##########################################
# 説明文の整形(MetavarTypeHelpFormatter)

# プログラムの説明の冒頭と文末部分
help_desc_msg ="""プログラムの説明文
(ここから読んでね)
"""
help_epi_msg = """説明完了
(終わりだよ)
"""

# ArgumentDefaultsHelpFormatterの場合
parser = argparse.ArgumentParser(description=help_desc_msg,
                                 epilog=help_epi_msg,
                                 formatter_class=argparse.MetavarTypeHelpFormatter)

# データ型を追加
parser.add_argument('hoge', type=str, nargs='?', default='ABC',  help='オプション引数だよ\n文字列型だよ')
# オプション引数を追加
parser.add_argument('--foo', type=int, help='オプション引数だよ\nint型だよ')
args = parser.parse_args()

print(args)
```

```:ヘルプの実行結果
python argparse_sample2.py -h
--------------------(コマンド結果)--------------------
usage: argparse_sample2.py [-h] [--foo int] [str]

プログラムの説明文 (ここから読んでね)

positional arguments:
  str         オプション引数だよ 文字列型だよ

optional arguments:
  -h, --help  show this help message and exit
  --foo int   オプション引数だよ int型だよ

説明完了 (終わりだよ)
```

今まで「hoge」と表示されていた位置引数が「str」とデータ型が表示されています。
(「usage:」行と「positional arguments:」内の両方で)
ただしその他の整形はデフォルト設定のため、やはり改行が無視されている点に注意してください。

##### (補足)複数改行の場合

ヘルプ文に記述した改行は「RawDescriptionHelpFormatter」や「RawTextHelpFormatter」において反映されます。
ただし空行は複数あっても1つにまとめられます。
複数の空行を維持したい場合、空行にスペースを含めるなどの工夫が必要です。


```python:argparse_sample2.py
# coding: utf-8
import argparse

##########################################
# 複数の空行の扱い
##########################################

# 複数の空行を含むプログラムの説明
help_desc_msg = 'プログラムの説明文\n' \
                '\n' \
                '\n' \
                '\n' \
                '上3つの空行は1つにまとめられる。\n' \
                '下3つの空行はスペースを含むため3行分になる。\n' \
                ' \n' \
                ' \n' \
                ' \n' \
                'ほらね。'

parser = argparse.ArgumentParser(description=help_desc_msg,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)

parser.add_argument('hoge')
args = parser.parse_args()

print(args)
```

```:ヘルプの実行結果
python argparse_sample2.py -h
--------------------(コマンド結果)--------------------
usage: argparse_sample2.py [-h] hoge

プログラムの説明文

上3つの空行は1つにまとめられる。
下3つの空行はスペースを含むため3行分になる。



ほらね。

positional arguments:
  hoge

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


##### (補足)複数の説明文のフォーマットの複合

前述した通り、「ArgumentDefaultsHelpFormatter」や「MetavarTypeHelpFormatter」を使った場合、意図的な改行は反映されません。
しかしちょっとした工夫をすることで、複数の説明文フォーマットを組み合わせることが可能です。
今回はデフォルト値を表示する「ArgumentDefaultsHelpFormatter」とプログラムの説明文の冒頭/文末を改行する「RawDescriptionHelpFormatter」を組み合わせます。

```python:argparse_sample2.py
# coding: utf-8
import argparse

##########################################
# 複数のフォーマットを組み合わせ
##########################################


# ------------------------------
# 複数フォーマットを組み合わせた独自HelpFormatterクラス
# ------------------------------
class MyHelpFormatter(argparse.ArgumentDefaultsHelpFormatter,
                      argparse.RawDescriptionHelpFormatter):
    pass


# ------------------------------
# 説明文の整形
# (ArgumentDefaultsHelpFormatterとRawDescriptionHelpFormatterの組み合わせ)
# ------------------------------
# プログラムの説明の冒頭と文末部分
help_desc_msg ="""プログラムの説明文
(ここから読んでね)
"""
help_epi_msg = """説明完了
(終わりだよ)
"""

parser = argparse.ArgumentParser(description=help_desc_msg,
                                 epilog=help_epi_msg,
                                 formatter_class=MyHelpFormatter)

# 引数の引数の数とデフォルト値を追加
parser.add_argument('hoge', nargs='?', default='ABC',  help='説明\nするよ')
args = parser.parse_args()

print(args)
```

「ArgumentDefaultsHelpFormatter」と「RawDescriptionHelpFormatter」を継承した独自のMyHelpFormatterクラスを作成します。
これをformatter_classに指定することで、両者の特性を組み合わせることが可能です。

```:ヘルプの実行結果
python argparse_sample2.py -h
--------------------(コマンド結果)--------------------
usage: argparse_sample2.py [-h] [hoge]

プログラムの説明文
(ここから読んでね)

positional arguments:
  hoge        説明 するよ (default: ABC)

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

説明完了
(終わりだよ)
```
上記の通り、

* ArgumentDefaultsHelpFormatter:説明文の冒頭/文末のみ改行は反映
* RawDescriptionHelpFormatter:引数のデフォルト値を表示

という両者のフォーマットを取り入れた結果となりました。


##### (補足)自作の説明文フォーマット

[先程の内容](#補足複数の説明文のフォーマットの複合)を応用することで、自作の説明文フォーマットが作成可能です。
例として、プログラムの冒頭/文末の説明文に「。」があった場合、その位置で改行する、という独自のフォーマットを作成します。
作成する際は、ArgumentParserモジュールの「argparse.py」の各クラスのコードを実際に見て参考にしてください。

* argparse.pyのソースコード<br>
https://github.com/python/cpython/blob/3.7/Lib/argparse.py


```python:argparse_sample2.py
# coding: utf-8
import argparse

##########################################
# 自作の説明文フォーマット
##########################################


# ------------------------------
# 説明文の冒頭/文末に句点(。)がある場合、改行するフォーマット
# RawDescriptionHelpFormatterを参考に作成
# ------------------------------
class MyHelpFormatter(argparse.HelpFormatter):
    def _fill_text(self, text, width, indent):
        return text.replace('。', '。\n')


# ------------------------------
# 説明文の整形
# ------------------------------
# プログラムの説明の冒頭と文末部分
help_desc_msg = '説明の冒頭。句点があると。改行だよ。'
help_epi_msg = '説明文の文末。冒頭と同じ仕様だよ。'

parser = argparse.ArgumentParser(description=help_desc_msg,
                                 epilog=help_epi_msg,
                                 formatter_class=MyHelpFormatter)

# 引数の引数の数とデフォルト値を追加
parser.add_argument('hoge', help='引数の説明文。句点があっても。改行しないよ。')
args = parser.parse_args()

print(args)
```

```:ヘルプの実行結果
python argparse_sample2.py -h
--------------------(コマンド結果)--------------------
usage: argparse_sample2.py [-h] hoge

説明の冒頭。
句点があると。
改行だよ。

positional arguments:
  hoge        引数の説明文。句点があっても。改行しないよ。

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

説明文の文末。
冒頭と同じ仕様だよ。
```

上述の結果通り、元の文章に改行がなくとも、「。」があれば説明文の冒頭/文末は改行します。


## コマンドライン引数の様々な使い方

コマンドライン引数は様々なオプションを含め、色々な使い方があります。
それらの複雑な使い方を見ていきます。

### バージョン表示

コマンドライン引数でよくあるバージョン表示ですが、ArgumentParserには最初から用意されています。
注意点としてはヘルプ(-h/--help)と同様に、使用すると指定された文章の表示後、処理が終了します。

```python:argparse_sample3.py
# coding: utf-8
import argparse

##########################################
# コマンドライン引数の様々な使い方
##########################################

# パーサの作成
parser = argparse.ArgumentParser(description='本プログラムのヘルプ文')

# バージョン情報コマンドライン引数の追加
# '-V', '--version' : バージョン情報引数の指定
# action='version'  : バージョン情報を表示する引数であることを指定
# version='<msg>'   : 実行時に<msg>を表示
parser.add_argument('-V', '--version',
                    action='version',
                    version='バージョン 12.34')

# 引数の変換結果を取得
# (バージョン「-V/--version」が指定された場合、version=で指定した文章を表示し処理を終了)
args = parser.parse_args()

print(args)
```

```:実行結果
python argparse_sample3.py -V
--------------------(コマンド結果)--------------------
バージョン 12.34
```

またバージョン引数の場合、ヘルプ文も最初から用意されています。

```:実行結果
python argparse_sample3.py
--------------------(コマンド結果)--------------------
usage: argparse_sample3.py [-h] [-V]

本プログラムのヘルプ文

optional arguments:
  -h, --help     show this help message and exit
  -V, --version  show program's version number and exit
```

### コマンドライン引数のオプション

コマンドライン引数作成に使われるadd_argument()には様々なオプションが用意されています。

|主なオプション|説明|
|---|---|
|action|コマンドライン引数が特別な行動をする|
|nargs|コマンドライン引数の個数を指定|
|default|コマンドライン引数がない場合のデフォルト値|
|type|コマンドライン引数のデータ型|
|help|コマンドライン引数の説明文|
|dest|コマンドライン引数の変換結果の属性名|

* add_argument()のマニュアル<br>
https://docs.python.org/ja/3/library/argparse.html#the-add-argument-method


#### actionオプションの使い方

[前述](#バージョン表示)したバージョン表示のように、コマンドライン引数が特別な行動をします。
version以外のactionについてはマニュアルを参照してください。

* actionオプションのマニュアル<br>
https://docs.python.org/ja/3/library/argparse.html#action


#### nargsオプションの使い方

1つのコマンドライン引数で受け取る引数の個数を指定します。
例えば2つの引数を受け取る場合、次のようになります。

```python:argparse_sample3.py
# coding: utf-8
import argparse

##########################################
# コマンドライン引数の様々な使い方
##########################################

# パーサの作成
parser = argparse.ArgumentParser(description='本プログラムのヘルプ文')

# コマンドライン引数の追加
# 2つの引数を受け取る
parser.add_argument('-f', '--foo',
                    nargs=2)

# 引数の変換結果を取得
args = parser.parse_args()

# 受け取った引数の値を表示
print(args.foo)
```

```:実行結果
python argparse_sample3.py -f ABC DEF
--------------------(コマンド結果)--------------------
['ABC', 'DEF']
```

注意としては「nargs=1」の場合、1個のリストとなることです。

```python:argparse_sample3.py
# coding: utf-8
import argparse

##########################################
# コマンドライン引数の様々な使い方
##########################################

# パーサの作成
parser = argparse.ArgumentParser(description='本プログラムのヘルプ文')

# コマンドライン引数の追加
# 1つの引数を受け取る(list型として)
parser.add_argument('-f', '--foo',
                    nargs=1)

# 引数の変換結果を取得
args = parser.parse_args()

# 受け取った引数の値とデータ型を表示
print('val:', args.foo)
print('type:', type(args.foo))
```

```:実行結果
python argparse_sample3.py -f ABC
--------------------(コマンド結果)--------------------
val: ['ABC']
type: <class 'list'>
```

上記の通り、受け取った'ABC'はリスト型となっています。
よって明確に受け取る引数の個数を1つにしたい場合、「nargs='?'」と指定します。

```python:argparse_sample3.py
# coding: utf-8
import argparse

##########################################
# コマンドライン引数の様々な使い方
##########################################

# パーサの作成
parser = argparse.ArgumentParser(description='本プログラムのヘルプ文')

# コマンドライン引数の追加
# 1つの引数を受け取る
parser.add_argument('-f', '--foo',
                    nargs='?')

# 引数の変換結果を取得
args = parser.parse_args()

# 受け取った引数の値とデータ型を表示
print('val:', args.foo)
print('type:', type(args.foo))
```

```:実行結果
python argparse_sample3.py -f ABC
--------------------(コマンド結果)--------------------
val: ABC
type: <class 'str'>
```

見ての通り、'ABC'が文字列型となりリストではなくなっています。

個数を指定せず任意の複数の引数を受け取る場合、「nargs=*」と指定します。

```python:argparse_sample3.py
# coding: utf-8
import argparse

##########################################
# コマンドライン引数の様々な使い方
##########################################

# パーサの作成
parser = argparse.ArgumentParser(description='本プログラムのヘルプ文')

# コマンドライン引数の追加
# 任意の個数の引数を受け取る
parser.add_argument('-f', '--foo',
                    nargs='*')

# 引数の変換結果を取得
args = parser.parse_args()

# 受け取った引数の値とデータ型を表示
print('val:', args.foo)
print('type:', type(args.foo))
```

```:実行結果
python argparse_sample3.py -f AB CD EF GH IJ
--------------------(コマンド結果)--------------------
val: ['AB', 'CD', 'EF', 'GH', 'IJ']
type: <class 'list'>
```

#### defaultオプションの使い方

defaultオプションは引数が指定されなかった場合の値を予め決めておく機能です。
位置引数で使う場合はnargsが'*'(任意の個数)または'?'(1個)である必要があります。
オプション引数では特に制約はありません。

```python:argparse_sample3.py
# coding: utf-8
import argparse

##########################################
# コマンドライン引数の様々な使い方
##########################################

# パーサの作成
parser = argparse.ArgumentParser(description='本プログラムのヘルプ文')

# 位置引数の追加(デフォルト値あり)
parser.add_argument('var1', default='Hello', nargs='?')
parser.add_argument('var2', default='World!', nargs='?')

# オプション引数の追加(デフォルト値あり)
parser.add_argument('-f', '--foo',
                    default='デフォルト値だよ！')

# 引数の変換結果を取得
args = parser.parse_args()

# 受け取った引数の値とデータ型を表示
print('var1:', args.var1)
print('var2:', args.var2)
print('foo:', args.foo)
```

```:実行結果
python argparse_sample3.py
--------------------(コマンド結果)--------------------
python argparse_sample3.py
var1: Hello
var2: World!
foo: デフォルト値だよ！
```

余談ですが、位置引数を1つだけ指定した場合、先に宣言した位置引数に値が代入されます。

```:実行結果
python argparse_sample3.py Hi
--------------------(コマンド結果)--------------------
python argparse_sample3.py
var1: Hi
var2: World!
foo: デフォルト値だよ！
```

上記の通り、先に宣言したvar1へ引数「Hi」が代入され、var2はデフォルト値の「World!」です。

#### typeオプションの使い方

引数のデータ型を指定します。
今までは全て文字列型として受け取っていましたが、typeオプションにより任意のデータ型へ変換できます。

```python:argparse_sample3.py
# coding: utf-8
import argparse

##########################################
# コマンドライン引数の様々な使い方
##########################################

# パーサの作成
parser = argparse.ArgumentParser(description='本プログラムのヘルプ文')

# オプション引数の追加(int型を受け取る)
parser.add_argument('-f', '--foo',
                    type=int)

# 引数の変換結果を取得
args = parser.parse_args()

# 受け取った引数の値とデータ型を表示
print('foo:', args.foo)
print('type:', type(args.foo))
```

```:実行結果
python argparse_sample3.py -f 1234
--------------------(コマンド結果)--------------------
foo: 1234
type: <class 'int'>
```

上記の通り「1234」を数値型として受け取っています。
また「-f」は数値型しか受け取れないため、文字列を指定するとエラーになります。

```:実行結果
python argparse_sample3.py -f ABC
--------------------(コマンド結果)--------------------
usage: argparse_sample3.py [-h] [-f FOO]
argparse_sample3.py: error: argument -f/--foo: invalid int value: 'ABC'
```

#### helpオプションの使い方

コマンドライン引数のヘルプ文を追加できます。
詳細は[前述](#コマンドライン引数の説明文の記述)した内容を参照ください。

#### destオプションの使い方

コマンドライン引数の変換結果の属性名を決められます。
今まで引数の変換結果を受け取る時、「args.<引数名>」としていました。
destを使うことで任意の属性名を使用可能です。
※destを使った場合、従来どおりの「args.<引数名>」は使えません

```python:argparse_sample3.py
# coding: utf-8
import argparse

##########################################
# コマンドライン引数の様々な使い方
##########################################

# パーサの作成
parser = argparse.ArgumentParser(description='本プログラムのヘルプ文')

# オプション引数の追加(destを指定する)
parser.add_argument('-f', '--foo',
                    dest='argument_val')

# 引数の変換結果を取得
args = parser.parse_args()

# 受け取った引数の値とデータ型を表示
# (destで指定したargument_valを使う)
print(args.argument_val)

# 従来通りのargs.fooだとエラー発生
try:
    print(args.foo)
except AttributeError as e:
    print(e)
    print("'foo'だとエラーになります。")
```

```:実行結果
python argparse_sample3.py -f ABC
--------------------(コマンド結果)--------------------
ABC
'Namespace' object has no attribute 'foo'
'foo'だとエラーになります。
```

# テストの仕方

コマンドライン引数を使う場合、テストの仕方に工夫が必要です。
毎回、コマンドライン引数を変えながらコードを打つのは面倒です。
そもそもunittestを使う場合、コマンドライン引数をテスト対象に直接コマンド入力できません。
そのためpythonコード上で引数を直接入力する工夫が必要です。

## コマンドライン引数をコード上で指定

今回はテスト用に以下のコマンドライン引数を持つpythonコードを用意しました。

```python:argparse_sample4.py
# coding: utf-8
import argparse


##########################################
# テスト用コマンドライン引数関数の実行
##########################################

def argparse_setup():
    """
    コマンドライン引数を宣言

    :return argparse.ArgumentParser parser: コマンドラインパーサ
    """
    # パーサの作成
    parser = argparse.ArgumentParser(description='本プログラムのヘルプ文')

    # バージョン情報コマンドライン引数の追加
    parser.add_argument('-V', '--version',
                        action='version',
                        version='バージョン 12.34')

    # 位置引数の追加(数値型)
    parser.add_argument('var1', type=int)
    parser.add_argument('var2', type=int)

    # オプション引数の追加(文字列型)
    parser.add_argument('-f', '--foo', type=str)

    return parser


def main():
    # 関数の実行と結果表示
    get_parser = argparse_setup()
    arg = get_parser.parse_args()
    print(arg)


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

コマンドライン引数は計3つあり、コマンド上で実行すると次のような結果が得られます。

```:実行結果
python argparse_sample4.py -f Hello 10 20
--------------------(コマンド結果)--------------------
Namespace(foo='Hello', var1=10, var2=20)
```

上記argparse_sample4.pyのテストをするために次のunittestコードを用意しました。

```python:argparse_unittest.py
# coding: utf-8
import unittest
from argparse_sample4 import argparse_setup


class ArgparseUnitTest(unittest.TestCase):

    def test(self):
        """
        コマンドライン引数のテスト
        :return: 
        """
        # コマンドライン引数をコード上で指定
        test_parser = argparse_setup()
        get_arg = test_parser.parse_args(['-f', 'Hello', '10', '20'])

        # 比較テスト
        self.assertEqual(get_arg.var1, 10)
        self.assertEqual(get_arg.var2, 20)
        self.assertEqual(get_arg.foo, 'Hello')
```

ポイントはコマンドライン引数を宣言したargparse_setup()関数に対して、「parse_args()」へ引数として文字列リストを渡すことです。
例えば「-f Hello 10 20」というコマンドライン引数は['-f', 'Hello', '10', '20']というリストとして渡します。
注意点としてvar1、var2はint型ですが、コマンドライン引数として渡す際は文字列にすることです。
その結果以下のようにテスト結果が得られます。

```:実行結果
python -m unittest argparse_unittest.ArgparseUnitTest.test01
--------------------(コマンド結果)--------------------
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
```
※「python -m unittest ファイル名.クラス名.関数名」で特定関数のテストを実行可能です。

このようにparse_args()の引数へ与えたいコマンドライン引数の文字列リストを渡すことでテストが可能です。

## 「--help」や「--version」の場合

コマンドライン引数において「--help」や「--version」もテストしたいケースが多々あります。
しかしこれらのをテストする際は以下の2点が問題となります。

* 実行結果は標準出力されるため、情報取得が難しい。
* 「--help」や「--version」は実行された後、その場で処理が終了してしまう。

そのため以下のような工夫が必要です。

```python:argparse_sample4.py
# coding: utf-8
import unittest
import sys
from io import StringIO
from argparse_sample4 import argparse_setup


class ArgparseUnitTest(unittest.TestCase):

    def test02(self):
        """
        バージョンコマンドのテスト
        :return:
        """
        # ------------------------------
        # 1.標準出力対策
        # ------------------------------
        # 標準出力を取得できるように変更
        # 現在の出力設定を保存(後で戻すため)後、StringIOに置き換える
        org_stdout, sys.stdout = sys.stdout, StringIO()

        # ------------------------------
        # 2.「--version」実行後、処理終了への対策
        # ------------------------------
        with self.assertRaises(SystemExit):
            # コマンドライン引数をコード上で指定
            test_parser = argparse_setup()
            get_arg = test_parser.parse_args(['--version'])

        # 「--version」コマンドライン引数の結果得た標準出力された文字列取得
        # (文末の改行を無視するためstrip()を用いる)
        std_out = sys.stdout.getvalue().strip()

        # 比較テスト
        self.assertEqual(std_out, 'バージョン 12.34')

        # 標準出力の設定を元に戻す
        sys.stdout = org_stdout
```

```:実行結果
python -m unittest argparse_unittest.ArgparseUnitTest.test02
--------------------(コマンド結果)--------------------
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
```

このように終了条件と標準出力の問題どちらもクリアしました。

余談ですがコマンドライン引数を用いるプログラムは標準出力をよく使います。
そのため毎回標準出力の設定を変更するのは手間がかかります。
よって以下のようにテスト関数の最初/最後に実行されるsetUp()/tearDown()関数をオーバーライドすることを推奨します。

```python:argparse_sample4.py
# coding: utf-8
import unittest
import sys
from io import StringIO
from argparse_sample4 import argparse_setup


class ArgparseUnitTest(unittest.TestCase):

    def setUp(self):
        # 標準出力を取得できるように変更
        self.org_stdout, sys.stdout = sys.stdout, StringIO()

    def tearDown(self):
        # 標準出力の設定を元に戻す
        sys.stdout = self.org_stdout

～(以下略)～
```

# 参考リンク

* argparseのマニュアル
https://docs.python.org/ja/3/library/argparse.html
基本的にマニュアルを読み、分かりにくい点は他の方の使い方を参考にしました。
<br>

* Stack Overflow ～ Python argparse: Show Defaults and keep newlines? ～
https://stackoverflow.com/questions/50700186/python-argparse-show-defaults-and-keep-newlines
ヘルプ文のformatter_classを複数組み合わせ方はこちらを参考にしました。
<br>

* Stack Overflow ～ Python argparse: How to insert newline in the help text? ～
https://stackoverflow.com/questions/3853722/python-argparse-how-to-insert-newline-in-the-help-text
ヘルプ文の独自formatter_classの作り方はこちらを参考にしました。

* ArgumentParserの使い方を簡単にまとめた
https://qiita.com/kzkadc/items/e4fc7bc9c003de1eb6d0
ArgumentParserの使い方が丁寧に整理されています。
とりあえず使ってみたい、という方はこちらの記事のほうが分かりやすいと思います。
本記事の執筆 & ArgumentParserを使いこなす上で大変参考にさせていただきました。

* [Dd]enzow(ill)? with DB and Python ～ Pythonのunittestで標準出力のテストを作る ～
http://www.denzow.me/entry/2017/10/05/212612
unittestで標準出力の結果を取得する方法はこちらを参照しました。
標準出力とunittestについて詳しく知りたい方は本リンク先を読むことを推奨します。