【问题标题】:Best architecture for a Python command-line tool with multiple subcommands具有多个子命令的 Python 命令行工具的最佳架构
【发布时间】:2023-09-06 19:29:01
【问题描述】:

我正在为一个项目开发一个命令行工具集。最终的工具应该支持很多子命令,像这样

foo command1 [--option1 [value]?]*

所以可以有类似的子命令

foo create --option1 value --

foo make file1 --option2 --option3

该工具使用 argparse 库来处理命令行参数和帮助功能等。

一些额外的要求和限制:

  • 所有子命令的某些选项和功能都是相同的(例如解析 YAML 配置文件等)

  • 一些子命令的编码既快速又简单,因为它们例如只需调用外部 bash 脚本即可。

  • 一些子命令会很复杂,因此代码很长。

  • 应该提供基本工具的帮助以及单个子命令的帮助:

    帮助 可用的命令有:make、create、add、xyz

    foo 帮助制作 make 子命令的详细信息

  • 子命令的错误代码应该是统一的(就像“找不到文件”的错误代码一样)

出于调试目的和在最小可行版本的自包含功能方面取得进展,我想开发一些子命令作为自包含脚本和模块,例如

make.py

可以导入到主 foo.py 脚本中,然后作为两者调用

make.py --option1 value etc.

foo.py make --option1 value

现在,我的问题是:用最少的冗余来模块化这样一个复杂的 CLI 工具的最佳方法是什么(例如,参数定义和解析应该只编码在一个组件中)?

选项 1:将所有内容放在一个大脚本中,但这将变得难以管理。

选项 2: 在单个模块/文件中开发子命令的功能(如 make.pyadd.py);但必须保持可调用(通过if __name__ == '__main__' ...)。

然后可以将子命令模块中的函数导入到主脚本中,并将子命令中的解析器和参数添加为子解析器。

选项3:主脚本可以简单地重新格式化对子进程的子命令的调用,就像这样

subprocess.run('./make.py {arguments}', shell=True, check=True, text=True)

【问题讨论】:

  • python 库 'click' 提供了这个功能
  • Typer 也是如此,如果你习惯了 FastAPI
  • 我认为您的问题比重复链接要大得多。 parents 是一种懒人在多个子解析器中定义相同参数的方式。但懒惰的程序员也知道他们可以编写辅助函数来执行重复性任务。但我认为 SO 不是解决程序结构问题的好论坛。范围太大,太受制于意见。一旦您编写了基本的子解析器代码,您就已经用尽了argparse 提供的工具。在模块中运行子命令和分区不是argparse 问题。
  • 没错。最佳实践问题,如果它们是关于 Stack Exchange 的主题,属于 Software Engineering,而不是这里。 Stack Overflow 仅适用于有关在编程实践中遇到的狭窄、具体问题的问题,这些问题可以得到规范的答案。
  • Typer(以及,为了 API 目的,FastAPI)看起来很有前途!谢谢!

标签: python command-line-interface command-line-arguments argparse


【解决方案1】:

我更习惯于回答有关numpyargparse 的详细信息的问题,但这是我对大包的设想。

main.py:

import submod1
# ....
sublist = [submod1, ...]
def make_parser(sublist):
    parser = argparse.ArgumentParser()
    # parser.add_argument('-f','--foo')  # main specific
    # I'd avoid positionals
    sp = parser.add_subparsers(dest='cmd', etc)
    splist=[]
    for md in sublist:
         sp1 = sp.add_parser(help='', parents=[md.parser])
         sp1.set_default(func=md.func)  # subparser func as shown in docs
         splist.append(sp1)
    return parser

如果 name == 'ma​​in': parser = make_parser(子列表) args = parser.parse_args() # print(args) # 调试显示 args.func(args) # 再次是子解析器函数

submod1.py

导入参数解析 def make_parser(): parser = argparse.ArgumentParser(add_help=False) # 检查文档? parser.add_argument(...) # 可以在这里添加一个共同的父母 返回解析器

parser.make_parser()

def func(args):
    # module specific 'main'

我确信这在很多方面都是不完整的,因为我是在未经测试的情况下即时编写的。这是一个基本的子解析器定义,但使用parents 导入子模块中定义的子解析器。 parents 也可用于定义子解析器的公共参数;但是实用功能也可以。我认为parents 在使用您无法访问的解析器时最有用; IE。一个进口的。

parents 本质上是从一个解析器复制动作到新的解析器——通过引用复制(不是通过值或作为副本)。它不是一个高度发达的工具,并且有许多 SO 让人们遇到问题。所以不要试图过度扩展它。

【讨论】:

    【解决方案2】:

    考虑使用Command PatternFactory Method Pattern

    简而言之,创建一个名为 Command 的抽象类,并使每个命令都成为继承自 Command 的自己的类。

    例子:

    class Command():
    
        def execute(self):
            raise NotImplementedError()
    
    
    class Command1(Command):
    
        def __init__(self, *args):
            pass
    
        def execute(self):
            pass
    
    
    class Command2(Command):
    
        def __init__(self, *args):
            pass
    
        def execute(self):
            pass
    

    这将处理命令的执行。对于建筑,制作一个命令工厂。

    class CommandFactory():
    
        @staticmethod
        def create(command, *args):
            if command == 'command1':
                return Command1(args)
            elif command == 'command2':
                return Command2(args)
    

    那么你就可以用一行来执行命令了:

    CommandFactory.create(command, args).execute()
    

    【讨论】:

    • 嗯 - 感谢您的努力!但是,问题不在于添加子命令——argparse 很好地支持了这一点。主要挑战是模块化方法,它消除了有关 CLI 参数和文档/帮助的冗余,同时允许将子命令作为单独的 Python 脚本运行。
    【解决方案3】:

    感谢您的所有建议!

    我认为最优雅的方法是使用 Typer 并遵循以下配方:

    https://typer.tiangolo.com/tutorial/subcommands/add-typer/

    【讨论】:

      最近更新 更多