如何使用argparse创建像git这样的命令组?

BPL*_*BPL 6 python argparse python-3.x

我试图弄清楚如何正确使用内置的argparse模块来获得与 git 等工具类似的输出,在其中我可以显示所有“根命令”的良好帮助,这些“根命令”很好地分组,即:

$ git --help
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           [--super-prefix=<path>] [--config-env=<name>=<envvar>]
           <command> [<args>]

These are common Git commands used in various situations:

start a working area (see also: git help tutorial)
   clone     Clone a repository into a new directory
   init      Create an empty Git repository or reinitialize an existing one

work on the current change (see also: git help everyday)
   add       Add file contents to the index
   mv        Move or rename a file, a directory, or a symlink
   restore   Restore working tree files
   rm        Remove files from the working tree and from the index

examine the history and state (see also: git help revisions)
   bisect    Use binary search to find the commit that introduced a bug
   diff      Show changes between commits, commit and working tree, etc
   grep      Print lines matching a pattern
   log       Show commit logs
   show      Show various types of objects
   status    Show the working tree status

grow, mark and tweak your common history
   branch    List, create, or delete branches
   commit    Record changes to the repository
   merge     Join two or more development histories together
   rebase    Reapply commits on top of another base tip
   reset     Reset current HEAD to the specified state
   switch    Switch branches
   tag       Create, list, delete or verify a tag object signed with GPG

collaborate (see also: git help workflows)
   fetch     Download objects and refs from another repository
   pull      Fetch from and integrate with another repository or a local branch
   push      Update remote refs along with associated objects

'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help <command>' or 'git help <concept>'
to read about a specific subcommand or concept.
See 'git help git' for an overview of the system.
Run Code Online (Sandbox Code Playgroud)

这是我的尝试:

from argparse import ArgumentParser


class FooCommand:
    def __init__(self, subparser):
        self.name = "Foo"
        self.help = "Foo help"
        subparser.add_parser(self.name, help=self.help)


class BarCommand:
    def __init__(self, subparser):
        self.name = "Bar"
        self.help = "Bar help"
        subparser.add_parser(self.name, help=self.help)


class BazCommand:
    def __init__(self, subparser):
        self.name = "Baz"
        self.help = "Baz help"
        subparser.add_parser(self.name, help=self.help)


def test1():
    parser = ArgumentParser(description="Test1 ArgumentParser")
    root = parser.add_subparsers(dest="command", description="All Commands:")

    # Group1
    FooCommand(root)
    BarCommand(root)

    # Group2
    BazCommand(root)

    args = parser.parse_args()
    print(args)


def test2():
    parser = ArgumentParser(description="Test2 ArgumentParser")

    # Group1
    cat1 = parser.add_subparsers(dest="command", description="Category1 Commands:")
    FooCommand(cat1)
    BarCommand(cat1)

    # Group2
    cat2 = parser.add_subparsers(dest="command", description="Category2 Commands:")
    BazCommand(cat2)

    args = parser.parse_args()
    print(args)
Run Code Online (Sandbox Code Playgroud)

如果你运行test1你会得到:

$ python mcve.py --help
usage: mcve.py [-h] {Foo,Bar,Baz} ...

Test1 ArgumentParser

options:
  -h, --help     show this help message and exit

subcommands:
  All Commands:

  {Foo,Bar,Baz}
    Foo          Foo help
    Bar          Bar help
    Baz          Baz help
Run Code Online (Sandbox Code Playgroud)

显然这不是我想要的,在那里我只看到平面列表中的所有命令,没有组或任何内容......所以下一个逻辑尝试将尝试对它们进行分组。但如果我跑test2我会得到:

$ python mcve.py --help
usage: mcve.py [-h] {Foo,Bar} ...
mcve.py: error: cannot have multiple subparser arguments
Run Code Online (Sandbox Code Playgroud)

这显然意味着我没有正确使用 argparse 来完成手头的任务。那么,是否可以使用 argparse 来实现与 git 类似的行为?过去我依赖“黑客”,所以我认为这里的最佳实践是使用“黑客”的概念add_subparsers,但似乎我没有正确理解这个概念。

lar*_*sks 8

这本身不支持argparse——你不能嵌套子解析器,所以如果你想要这种使用 argparse 的 cli,你将需要在argparse. 您可以设置nargs=argparse.REMAINDER收集子命令和参数,而不用 argparse 解析它们,这意味着我们可以构建如下内容:

import argparse
import copy


class Command:
    def __init__(self):
        self.subcommands = {}
        self.parser = argparse.ArgumentParser()

    def add_subcommand(self, name, sub):
        self.subcommands[name] = sub

    def add_argument(self, *args, **kwargs):
        return self.parser.add_argument(*args, **kwargs)

    def parse_args(self, args=None):
        if not self.subcommands:
            args = self.parser.parse_args(args)
            return args

        p = copy.deepcopy(self.parser)
        p.add_argument("subcommand")
        p.add_argument("args", nargs=argparse.REMAINDER)
        args = p.parse_args(args)

        try:
            sub = self.subcommands[args.subcommand]
        except KeyError:
            return self.parser.parse_args(args)

        sub_args = sub.parse_args(args.args)

        for attr in dir(sub_args):
            if attr.startswith("_"):
                continue
            setattr(args, attr, getattr(sub_args, attr))

        return args


def main():
    root = Command()
    root.add_argument("-v", "--verbose", action="count")

    cmd1 = Command()
    cmd1_foo = Command()
    cmd1_foo.add_argument("-n", "--name")
    cmd1.add_subcommand("foo", cmd1_foo)
    root.add_subcommand("cmd1", cmd1)

    cmd2 = Command()
    cmd2_bar = Command()
    cmd2_bar.add_argument("-s", "--size", type=int)
    cmd2.add_subcommand("bar", cmd2_bar)
    root.add_subcommand("cmd2", cmd2)

    print(root.parse_args())


if __name__ == "__main__":
    main()
Run Code Online (Sandbox Code Playgroud)

这是可怕、丑陋且结构不良的,但这意味着我们可以这样做:

$ python argtest.py --verbose cmd1 foo --name lars
Namespace(verbose=1, subcommand='foo', args=['--name', 'lars'], name='lars')
Run Code Online (Sandbox Code Playgroud)

或这个:

$ python argtest.py --verbose cmd2 bar --size 10
Namespace(verbose=1, subcommand='bar', args=['--size', '10'], size=10)
Run Code Online (Sandbox Code Playgroud)

如果您愿意超越argparse,像ClickTyper这样的库会让事情变得更容易。例如,上面的命令可以使用 Click 来实现,如下所示:

import click

@click.group()
def main():
    pass

@main.group()
def cmd1():
    pass

@cmd1.command()
@click.option('-n', '--name')
def foo(name):
    pass

@main.group()
def cmd2():
    pass


@cmd2.command()
@click.option('-s', '--size', type=int)
def bar():
    pass

if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)

好多了!