带有子命令的Python脚本模板(和类型提示)

319 阅读2分钟

这里有一个支持子命令的扩展版本,以及一个示例脚本。

模板

好了

#!/usr/bin/env python
from __future__ import annotations

import argparse
from collections.abc import Sequence


def main(argv: Sequence[str] | None = None) -> int:
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="command", required=True)

    # Add sub-commands
    subcommand1_parser = subparsers.add_parser("subcommand1", help="...")
    subcommand1_parser.add_argument("string")

    args = parser.parse_args(argv)

    if args.command == "subcommand1":
        return subcommand1(args.string)
    else:
        # Unreachable
        raise NotImplementedError(
            f"Command {args.command} does not exist.",
        )


def subcommand1(string: str) -> int:
    # Implement behaviour

    return 0


if __name__ == "__main__":
    raise SystemExit(main())

这可以在Python 3.7以上版本中使用:

以下是与vanilla模板的不同之处。

  • add_subparsers() 我们告诉它存储命令的名字以便以后进行比较,并使命名一个子命令成为必要。

  • 我们用subparsers.add_parser() ,为每个子命令添加解析器,它返回一个ArgumentParser 。我们可以用它以正常的方式添加参数。(...甚至可以通过调用它的 add_subparsers() ,配置子子命令。

  • 在参数解析之后,我们使用if 来打开args.command 中的子命令名称,并调用相应的子命令函数。我们在调用时解构args ,这样我们的子命令函数就可以接受精确的类型。

    if 进行切换,比argparse文档中首先提到的方法要好。 set_defaults(func=...)argparse文档中首先提到的方法。使用if ,可以保持控制流的清晰,而且这意味着我们的子命令函数可以接受带有类型的单个参数,而不是args

  • 我们的else 块对未识别的子命令提出了一个错误。 这在通常情况下是达不到的--argparse已经退出了,对未知的子命令发出了信息。

    $ ./example.py subcommand2
    usage: example.py [-h] {subcommand1} ...
    example.py: error: argument command: invalid choice: 'subcommand2'(choose from 'subcommand1') 
    

    但如果我们添加了一个子命令解析器,并错过了其对应的if 块,这个错误就会成为可能。 所以最好是保留它。

    为了支持没有子命令的使用,从add_subparsers() 调用中删除required=True ,并在这个else 块中添加行为。

举例来说

这里我们有两个子命令,debughello

#!/usr/bin/env python
from __future__ import annotations

import argparse
import sys
from collections.abc import Sequence


def main(argv: Sequence[str] | None = None) -> int:
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="command", required=True)

    subparsers.add_parser("debug", help="Show debug information.")

    hello_parser = subparsers.add_parser("hello", help="Say hello.")
    hello_parser.add_argument("name", help="Who to greet.")

    args = parser.parse_args(argv)

    if args.command == "debug":
        return debug()
    elif args.command == "hello":
        return hello(name=args.name)
    else:
        raise NotImplementedError(
            f"Command {args.command} does not exist.",
        )


def debug() -> int:
    print(f"Python version {sys.version}")
    return 0


def hello(name: str) -> int:
    print(f"Hello {name}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

酷......让我们试试吧。

我们可以请求帮助,而argparse也会答应。

$ ./example.py -h
usage: example.py [-h] {debug,hello} ...

positional arguments:
  {debug,hello}
    debug        Show debug information.
    hello        Say hello.

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

我们也可以得到子命令的帮助。

$ ./example.py hello --help
usage: example.py hello [-h] name

positional arguments:
  name        Who to greet.

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

debug 这个子命令很简单。

$ ./example.py debug
Python version 3.9.7 (default, Sep  1 2021, 11:36:38)
[Clang 12.0.5 (clang-1205.0.22.11)]

hello ,如预期的那样工作。

$ ./example.py hello reader
Hello reader

非常酷