如何传递存储在文件中的命令行参数并保留该文件的名称?

7

我有一个脚本,它接受命令行参数,并且我想要实现两种参数传递方案,分别是:

  • 在命令行输入参数。
  • 将参数列表存储在文件中,并通过命令行传递该文件的名称。

为此,我将参数fromfile_prefix_chars传递给ArgumentParser构造函数。

script.py

from argparse import ArgumentParser
parser = ArgumentParser(fromfile_prefix_chars='@')
parser.add_argument('filename', nargs='?')
parser.add_argument('--foo', nargs='?', default=1)
parser.add_argument('--bar', nargs='?', default=1)
args = parser.parse_args()
print(args)

args.txt

--foo
2
--bar
2

样例用例

$ python script.py --foo 3
Namespace(bar=1, filename=None, foo='3')
$ python script.py @args.txt --foo 3
Namespace(bar='2', filename=None, foo='3')

我本以为args.filename会保留文件名,但出乎意料的是它的值是None。我知道可以通过一些处理从sys.argv获取文件名。有没有更简便(最好基于argparse方法)的方式来获取参数文件的名称?


3
确实如此,但这样一来,参数就无法传递给程序。 - 101arrowz
1
哇,我完全不知道@filename的功能。但是,你能调用两次解析吗?一次带有@,一次不带?您可以从修改后的sys.argv提供自己的参数列表。然后从另一个命名空间调整一个名称,它相当宽容。 - JL Peyret
2
你的 args.txt 文件中没有为 filename 提供任何值,也没有在命令行中提供。你为什么期望它有一个不同于 None 的值呢?fromfile_prefix_chars 做了一件非常具体的事情:当它看到 @something 时,用从文件解析出来的参数替换 @something,就这样。它被设计成对参数是不可见的,因为它是在实际命令行解析之前处理的。 - Bakuriu
3
根据文档,“以特定字符开头的参数将被视为文件,并将被其所包含的参数替换。” - Bakuriu
@Bakuriu 说得有道理。我不知道你为什么需要访问文件名,但我的答案提供了一种可能的(虽然不符合 Python 风格)解决方案。请确保你确实需要文件名,因为我建议的做法正是 fromfile_prefix_chars 参数旨在防止的。 - 101arrowz
显示剩余5条评论
3个回答

2

你的 script.py,加上文件。我已经将文件名添加到文件本身。

args.txt

args.txt
--foo
2
--bar
2

测试:

1803:~/mypy$ python3 stack56811067.py --foo 3
Namespace(bar=1, filename=None, foo='3')
1553:~/mypy$ python3 stack56811067.py @args.txt --foo 3
Namespace(bar='2', filename='args.txt', foo='3')

整洁而聪明!但是你的方法存在问题,如果文件名更改了,你需要记得相应地更改第一行。 - Tonechas

1
从我的测试结果来看,使用fromfile_prefix_chars意味着argparse实际上不会将参数传递给您的程序。相反,argparse看到@args.txt,截取它,从中读取,并将没有@args.txt的参数传递给您的程序。这可能是因为大多数人实际上并不需要文件名,只需要其中的参数,因此argparse省去了创建另一个用于存储不需要的内容的参数的麻烦。

不幸的是,所有参数都作为局部变量存储在argparse.py中,因此我们无法访问它们。我想你可以重写一些argparse的函数。请记住,这是一个可怕、令人恶心的hacky解决方案,我认为解析sys.argv要好100%。

from argparse import ArgumentParser

# Most of the following is copied from argparse.py
def customReadArgs(self, arg_strings):
    # expand arguments referencing files
    new_arg_strings = []
    for arg_string in arg_strings:

        # for regular arguments, just add them back into the list
        if not arg_string or arg_string[0] not in self.fromfile_prefix_chars:
            new_arg_strings.append(arg_string)

        # replace arguments referencing files with the file content
        else:
            try:
                fn = arg_string[1:]
                with open(fn) as args_file:

                    # What was changed: before was []
                    arg_strings = [fn]

                    for arg_line in args_file.read().splitlines():
                        for arg in self.convert_arg_line_to_args(arg_line):
                            arg_strings.append(arg)
                    arg_strings = self._read_args_from_files(arg_strings)
                    new_arg_strings.extend(arg_strings)
            except OSError:
                err = _sys.exc_info()[1]
                self.error(str(err))

    # return the modified argument list
    return new_arg_strings
ArgumentParser._read_args_from_files = customReadArgs
parser = ArgumentParser(fromfile_prefix_chars='@')
parser.add_argument('filename', nargs='?')
parser.add_argument('--foo', nargs='?', default=1)
parser.add_argument('--bar', nargs='?', default=1)
args = parser.parse_args()
print(args)

确实,你的解决方案有些取巧...但它运行得很好!我终于创建了argparse.ArgumentParser的子类,并在子类MyArgumentParser中重写了_read_args_from_files方法。 - Tonechas

0
只是为了记录,这是我想出的一个快速而简单的解决方案。它基本上包括创建 parser 的副本并将其 from_file_prefix_chars 属性设置为 None
import copy

parser_dupe = copy.copy(parser)
parser_dupe.fromfile_prefix_chars = None
args_raw = parser_dupe.parse_args()
if args_raw.filename:
    args.filename = args_raw.filename[1:]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接