【问题标题】:Using argparse, how can I process a "chdir" argument before fromfile expansion?使用 argparse,如何在 fromfile 扩展之前处理“chdir”参数?
【发布时间】:2016-09-15 18:23:03
【问题描述】:

我想支持子命令 CLI 模型,就像 git 使用的那样,我遇到问题的特定位是“更改目录”选项。像git 一样,我想要一个-C DIR 选项,它会在执行子命令之前将程序更改为指定的目录。不是真正的问题,使用子解析器,但是我还想使用argparse.ArgumentParser(fromfile_prefix_chars='@') 机制在解析过程中应用-C DIR 参数之后。

问题来了:fromfile 参数扩展是由 argparse 在所有其他参数处理之前执行的。 因此,任何此类 fromfile 参数必须使用绝对路径或相对于调用解析器时的 CWD。我想要绝对路径;我“需要”使用与-C DIR 选项相关的fromfile 路径。我写了我自己的class ChdirAction(argparse.Action) 来做到这一点。它工作得很好,但由于 fromfile 参数已经扩展,它没有给我想要的东西。 (在发现这种 not-what-I-want 行为后,我查看了 python3.5/argparse.py 并发现同样的挫败感嵌入在冷酷、坚硬、无情的代码中。)

这是一个目录图,可能有助于解释我想要什么:

/ foo / aaa / iii / arg.txt
      |     |
      |     + jjj / arg.txt
      |     |
      |     + arg.txt
      |
      + bbb / iii / arg.txt
            |
            + jjj / arg.txt

在解析命令行参数时,考虑 CWD 是 aaa 还是 bbb。如果我运行类似prog -C ./iii @arg.txt 我希望解析器使用来自/foo/aaa/iii/arg.txt 的参数扩展@arg.txt。实际发生的是fromfile/foo/aaa/arg.txt 的内容扩展而来。当 CWD 为 /foo/aaa 时,这是“错误”文件;当/foo/bbb 它引发“错误:[Errno 2] 没有这样的文件或目录:'arg.txt'”

更一般地说,prog -C ./DIR @arg.txt 应该从 /foo/aaa/DIR/arg.txt 扩展,即使 fromfile 具有“向上目录”部分,例如prog -C ./iii @../arg.txt 应该从 /foo/aaa/arg.txt 扩展。

如果可以使这种行为发生,那么我可以 -C DIR{aaa,bbb}/{iii,jjj} 中的任何一个,并从通用命令行构造中获得一致的行为。

如上所述,我的问题不是什么大问题。如果能提供-C DIR,通过参数解析后的os.chdir(DIR)实现,那么我也可以构造合适的fromfile参数。它们可以是绝对的或相对于 CWD 解析时(在任何 -C DIR 生效之前)。这可能看起来像:

cd /foo/aaa; prog -C ./DIR @arg.txt @./DIR/arg.txt

我不喜欢它,但没关系。 REAL 问题是我使用的实际更改目录参数更像-C PATTERN。在我真正的问题案例中,PATTERN 可能是一个简单的路径(绝对或相对)。或者,它可能是一个 glob 模式,或者具有“非平凡”解析逻辑的部分名称,用于查找 os.chdir(DIR) 的实际目录。在这种情况下(我正在努力解决),我不能让程序的调用者解析 fromfile 路径的实际位置。

实际上,我可以, 但这会给调用者带来不适当的负担。而且,当那个调用程序是一个 Eclipse 启动程序时,我真的没有必要的控制流能力来做到这一点。所以,它又回到了让程序照顾自己的需求;一个更好的抽象,但是我该如何实现它?

【问题讨论】:

    标签: python argparse fromfile


    【解决方案1】:

    即使我正在充实这个问题,我也想出了一个主意。所以我试了一下,它有点,有点,好吧(ish)。我可以得到我真正想要的受限版本,但这对我来说已经足够了(目前),所以我想我不妨分享一下。它也可能对你来说已经足够好了。更好的是,它可能会从某个地方引出真正的解决方案,也许是 S.Bethard?

    我的技巧是分两个阶段进行解析:第一个阶段,在不启用 fromfile 机制的情况下通过 ArgumentParser.parse_known_args(...) 获取 -C PATTERN 参数就足够了。如果第一次(最小)解析的结果产生一个目录更改参数,那么我处理它。如果指定了多个 -C PATTERN,或无法明确解析 PATTERN,程序将中止。

    然后,我使用一个完全独立的 ArgumentParser 对象,配置了我真正想要的完整参数规范集并在启用 fromfile 机制的情况下解析它。

    要让--help 参数起作用(设置适当的冲突解决策略,然后仅在第一个解析器中接受 arg 只是为了传递给第二个解析器,它实际上具有所有“真实”参数规格)。此外,第一个解析器应该支持与第二个解析器相同的详细/安静选项,尊重它们的设置并从第一个解析器传递到第二个解析器。

    这是我的应用程序级 arg 解析器方法的简化版本。它在第一个解析器阶段不支持详细/安静选项。我省略了如何将-C PATTERN 解析为实际目录的复杂性。此外,我删除了第二个解析器的大部分参数规范,只留下了第二个解析器的 -C PATTERN 参数(--help 输出所需的参数)。

    注意:两个解析器都有一个-C PATTERN 参数。在chdirParser 中是有意义的;在argParser 中,它只存在于帮助输出中。应该为详细/安静选项做类似的事情 - 可能不是那么棘手,但这对我来说(还)不重要,所以我不介意总是报告目录更改,即使在安静模式下也是如此。

    def cli_args_from_argv():
    
        import argparse
        import glob
        import os
        import sys
    
        chdirParser = argparse.ArgumentParser(conflict_handler='resolve')
        chdirParser.add_argument("-C",           dest="chdir_pattern", action="append"    , default=None)
        chdirParser.add_argument("--help", "-h", dest="help",          action="store_true", default=False)
    
        (partial, remainder) = chdirParser.parse_known_args()
    
        if partial.help:
            remainder = ['--help']
    
        elif partial.chdir_pattern:
            if len(partial.chdir_pattern) > 1:
                print(r'Too many -C options - at most one may be given, but received: {!r}'.format(partial.chdir_pattern), file=sys.stderr)
                sys.exit(1)
    
            pattern  = partial.chdir_pattern[0]
            resolved_dir = pattern
            if os.path.exists(resolved_dir):
                resolved_dir = pattern
    
            else:
    
                ### ELIDED: resolution of pattern into an unambiguous and existing directory
    
                if not resolved_dir:
                    print("Failed to resolve -C {!r}".format(pattern), file=sys.stderr)
                    sys.exit(1)
    
            print("Changing to directory: {!r}".format(resolved_dir))
            print("");
    
            os.chdir(target_dir)
    
    
        argParser = argparse.ArgumentParser(usage="usage: PROG [common-args] SUBCMD [subcmd-args]", fromfile_prefix_chars=':')
    
        ### ELIDED: a bunches of add_argument(...)
    
        argParser.add_argument("-C", dest="chdir_spec", action="store", default=None, help="Before anything else, chdir to SPEC", metavar="SPEC")
    
        return argParser.parse_args(args=remainder)
    

    我觉得可能有更好的方法...你知道吗?

    【讨论】:

      【解决方案2】:

      我认为resolve 位可以替换为

      chdirParser = argparse.ArgumentParser(add_help=False)
      

      并省略-h 定义并保存。让第二个解析器处理 sys.argv 不变(因为您包含(但忽略)-C 参数)。

      如果您希望用户使用多个-C dir1 ... -C dir2... 命令,那么appendlen(partial.chdir_pattern) > 1 测试应该可以工作。使用默认 store 操作的替代方法,最终保存最后一次重复。为什么用户可能会重复-C,您为什么要关心?通常我们只是忽略重复。

      你可以替换

      print("Failed to resolve -C {!r}".format(pattern), file=sys.stderr)
      sys.exit(1)
      

      parser.error("Failed to resolve -C {!r}".format(pattern)')
      

      它打印使用情况(只有-C) and does ansys.exit(2)`。不太一样,但可能足够接近。

      对于第二个解析器,-C 可能会被简化(使用默认值):

      argParser.add_argument("-C", "--chdir-spec", help="Before anything else, chdir to SPEC", metavar="SPEC")
      

      并使用完整的sys.argv

      return argParser.parse_args()
      

      否则,使用 2 个解析器是有意义的,因为 fromfile 存在于更改后的目录中(并且您希望忽略初始目录中的任何此类文件)。

      我认为命令行中的:arg.txt 字符串可能会给第一个解析器带来问题。但是对于parse_known_args,它只会将其视为未知位置。但证据还在测试中。

      【讨论】:

      • 我喜欢您的改进,并将它们添加到我自己的程序代码中 - 尤其是。只是让第二个解析器使用sys.argv。这简化了第二个解析器的调用,并允许我通过详细/安静的处理来增强第一个解析器,而无需做任何特殊的事情来将它们“注入”回给第二个解析器的 remaining 参数
      • 您问“为什么有人会使用多个 -C SPEC 选项?”我不知道,但得到多个表示对 CLI 用法的误解或构造错误。在任何一种情况下,我都想抓住它并强制调用者清理他们的行为。忽略多次出现的“标准”对我来说很少感觉正确,这也不例外。
      • 至于第一个处理:arg.txt 事件的解析器:没有打开fromfile 机制(我没有),这些参数确实被认为是“忽略的未知位置参数"由第一个解析器。它已经为我工作了。从测试中得出了证据......