Skip to content

省略可能な CLI 引数

前に説明したとおり、デフォルトでは:

  • CLI オプション省略可能
  • CLI 引数必須

繰り返しになりますが、これは デフォルトでの 挙動であり、多くの CLI プログラムやシステムでの慣例でもあります。

ただし、これは変更できます。

実際には、省略可能CLI 引数 を持つほうが、必須CLI オプション を持つよりもずっと一般的です。

それがどのように役立つかの例として、ls という CLI プログラムの動きを見てみましょう。

// ただ入力すると
$ ls

// ls は現在のディレクトリ内のファイルとディレクトリを一覧表示します
typer  tests  README.md  LICENSE

// ただし、省略可能な CLI 引数も受け取れます
$ ls ./tests/

// すると ls は、その CLI 引数で指定されたディレクトリ内のファイルとディレクトリを一覧表示します
__init__.py  test_tutorial

CLI 引数 の別の宣言方法

First Steps では、CLI 引数 を追加する方法を見ました。

import typer


def main(name: str):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

ここで、同じ CLI 引数 を作る別の方法を見てみましょう。

from typing import Annotated

import typer


def main(name: Annotated[str, typer.Argument()]):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

import typer


def main(name: str = typer.Argument()):
    print(f"Hello {name}")


if __name__ == "__main__":
    typer.run(main)

あるいは、Typer() instance を明示的に作る形では次のようになります。

from typing import Annotated

import typer

app = typer.Typer()


@app.command()
def main(name: Annotated[str, typer.Argument()]):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

import typer

app = typer.Typer()


@app.command()
def main(name: str = typer.Argument()):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

Info

Typer はバージョン 0.9.0 で Annotated のサポートを追加し、以後それを推奨するようになりました。

それより古いバージョンを使っている場合、Annotated を使おうとするとエラーになります。

Annotated を使う前に、Typer のバージョンを少なくとも 0.9.0 へ更新してください。

以前は、次のような関数パラメータでした。

name: str

それを今度は Annotated で包みます。

name: Annotated[str]

この 2 つは同じ意味で、Annotated はそのために標準 Python に用意されています。

ただし Annotated を使う後者の書き方では、Typer が利用できる追加のメタデータを渡せます。

name: Annotated[str, typer.Argument()]

これで nameCLI 引数 であることを明示しています。型は依然として str のままで、デフォルト値を持たないので必須のままです。

ここでやっていることは、以前と同じ 必須CLI 引数 を作ることです。

$ python main.py

Usage: main.py [OPTIONS] NAME
Try "main.py --help" for help.

Error: Missing argument 'NAME'.

まだそれほど便利ではありませんが、正しく動作しています。

そして、

name: Annotated[str, typer.Argument()]

のように 必須CLI 引数 を宣言する方法は、

name: str

とまったく同じように動きます。

これはあとで役立ちます。

CLI 引数 を省略可能にする

さて、ここで本題の、省略可能な CLI 引数 を見ていきます。

CLI 引数 を省略可能にするには、typer.Argument() を使い、"default" 値を与えます。たとえば "World" のようにします。

from typing import Annotated

import typer

app = typer.Typer()


@app.command()
def main(name: Annotated[str, typer.Argument()] = "World"):
    print(f"Hello {name}!")


if __name__ == "__main__":
    app()
🤓 Other versions and variants

Tip

Prefer to use the Annotated version if possible.

import typer

app = typer.Typer()


@app.command()
def main(name: str = typer.Argument(default="World")):
    print(f"Hello {name}!")


if __name__ == "__main__":
    app()

これで次のようになります。

name: Annotated[str, typer.Argument()] = "World"

typer.Argument() を使っているので、Typer はこれが CLI 引数 だと分かります(必須省略可能 かに関係なく)。

help を確認してみましょう。

// まず help を確認します
$ python main.py --help

Usage: main.py [OPTIONS] [NAME]

Arguments:
  [NAME]

Options:
  --help                Show this message and exit.

Tip

NAME は依然として CLI 引数 であり、"Usage: main.py ..." の部分に表示されていることに注目してください。

また、以前は単に NAME だったのに対し、今は [NAME] のように角括弧で囲まれていて、これが 必須 ではなく 省略可能 であることを表しています。

では、実行して試してみましょう。

// CLI 引数なし
$ python main.py

Hello World!

// 省略可能な CLI 引数を 1 つ渡す
$ python main.py Camila

Hello Camila

Tip

ここでの "Camila" は、省略可能な CLI 引数 であって CLI オプション ではありません。"--name Camila" のような形ではなく、単に "Camila" をプログラムへ直接渡しているからです。

古い別方式: typer.Argument() をデフォルト値として使う

Typer は、追加のメタデータ付きで CLI 引数 を宣言する、もう 1 つの古い書き方もサポートしています。

Annotated を使う代わりに、typer.Argument() をデフォルト値として使えます。

import typer

app = typer.Typer()


@app.command()
def main(name: str = typer.Argument()):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()
🤓 Other versions and variants
from typing import Annotated

import typer

app = typer.Typer()


@app.command()
def main(name: Annotated[str, typer.Argument()]):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

Tip

可能なら Annotated 版を使うほうがよいです。

以前は、name にデフォルト値がなかったので、Python の意味では 必須パラメータ でした。

デフォルト値として typer.Argument() を使うと、Typer も同様にこれを 必須CLI 引数 として扱います。

次のように変更しました。

name: str = typer.Argument()

しかし、今度は typer.Argument() が関数パラメータの「デフォルト値」になるため、Python の意味では「もはや必須ではない」ことになります。

関数パラメータに Python 側のデフォルト値があるかどうかで必須性やデフォルト値を判断できなくなるため、typer.Argument() は最初の引数として default パラメータを受け取り、それで同じ目的を果たします。

default 引数に何も渡さないのは、必須として扱うのと同じです。ただし typer.Argument(default=...) のように ... を渡して、明示的に required とすることもできます。

name: str = typer.Argument(default=...)

Info

もしこの ... を見たことがなければ、それは特殊な単一の値で、Python に含まれる "Ellipsis" です。

import typer

app = typer.Typer()


@app.command()
def main(name: str = typer.Argument(default=...)):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

同様に、たとえば "World" のような別の default 値を渡せば、省略可能にできます。

import typer

app = typer.Typer()


@app.command()
def main(name: str = typer.Argument(default="World")):
    print(f"Hello {name}!")


if __name__ == "__main__":
    app()
🤓 Other versions and variants
from typing import Annotated

import typer

app = typer.Typer()


@app.command()
def main(name: Annotated[str, typer.Argument()] = "World"):
    print(f"Hello {name}!")


if __name__ == "__main__":
    app()

typer.Argument(default="World") に渡した最初のパラメータ(新しい "default" 値)が "World" なので、Typer はこれを 省略可能CLI 引数 だと判断します。コマンドラインで呼び出すときに値が与えられなければ、その "World" がデフォルト値として使われます。

default 引数は最初のものなので、default= を明示せずに値を渡しているコードを見ることもあるでしょう。たとえば次のような形です。

name: str = typer.Argument(...)

あるいは次のような形です。

name: str = typer.Argument("World")

ただし繰り返しになりますが、可能なら Annotated を使うようにしてください。そのほうが Python の意味としても Typer の意味としても同じになり、こうした細部を覚えなくて済みます。