How can I pass command line arguments contained in a file and retain the name of that file?

Viewed 1106

I have a script that consumes command line arguments and I would like to implement two argument-passing schemes, namely:

  • Typing the arguments out at the command line.
  • Storing the argument list in a file, and passing the name of this file to the program via the command line.

To that end I am passing the argument fromfile_prefix_chars to the ArgumentParser constructor.

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

Sample use cases

$ 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')

I was expecting that args.filename would retain the name of the file, but surprinsingly enough it has the value None instead. I am aware that I could get the file name from sys.argv through a bit of processing. Is there a cleaner way (ideally an argparse-based approach) to elicit the name of the arguments file?

3 Answers

Your script.py, plus the file. I have added the file name to the file itself.

args.txt

args.txt
--foo
2
--bar
2

testing:

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')

From my testing, using fromfile_prefix_chars means that argparse will not actually pass the argument to your program. Instead, argparse sees the @args.txt, intercepts it, reads from it, and passes the arguments without @args.txt to your program. This is presumably because most people don't really need the filename, just need the arguments within, so argparse saves you the trouble of creating another argument to store something you don't need.

Unfortunately, all of the arguments are stored as local variables in argparse.py, so we cannot access them. I suppose that you could override some of argparse's functions. Keep in mind that this is a horrible, disgusting, hacky solution and I feel that parsing sys.argv is 100% better.

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)

Just for the record, here's a quick and dirty solution I came up with. It basically consists in creating a copy of parser and set its from_file_prefix_chars attribute to 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:]
Related