summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsteven.bethard <devnull@localhost>2009-07-12 22:20:19 +0000
committersteven.bethard <devnull@localhost>2009-07-12 22:20:19 +0000
commited87842cef7a4f1ec4719116aa008e9b3658413e (patch)
tree00a895ce9dd818a13bdd42c5963cfbe8fd94da63
parent8f00f550659f65d9d49cfa227f27091fe7b90490 (diff)
downloadargparse-ed87842cef7a4f1ec4719116aa008e9b3658413e.tar.gz
Add support for specifying additional command line arguments in a file using the syntax ``@file``.
-rw-r--r--argparse.py30
-rw-r--r--doc/source/ArgumentParser.rst21
-rw-r--r--test/test_argparse.py59
3 files changed, 97 insertions, 13 deletions
diff --git a/argparse.py b/argparse.py
index 2aaf86a..fd8e4d2 100644
--- a/argparse.py
+++ b/argparse.py
@@ -1396,6 +1396,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
parents=[],
formatter_class=HelpFormatter,
prefix_chars='-',
+ fromfile_prefix_chars=None,
argument_default=None,
conflict_handler='error',
add_help=True):
@@ -1415,6 +1416,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
self.epilog = epilog
self.version = version
self.formatter_class = formatter_class
+ self.fromfile_prefix_chars = fromfile_prefix_chars
self.add_help = add_help
self._has_subparsers = False
@@ -1551,6 +1553,10 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
self.error(str(err))
def _parse_known_args(self, arg_strings, namespace):
+ # replace arg strings that are file references
+ if self.fromfile_prefix_chars is not None:
+ arg_strings = self._read_args_from_files(arg_strings)
+
# map all mutually exclusive arguments to the other arguments
# they can't occur with
action_conflicts = {}
@@ -1781,6 +1787,30 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# return the updated namespace and the extra arguments
return namespace, extras
+ def _read_args_from_files(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 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:
+ args_file = open(arg_string[1:])
+ arg_strings = args_file.read().splitlines()
+ arg_strings = self._read_args_from_files(arg_strings)
+ new_arg_strings.extend(arg_strings)
+ args_file.close()
+ except IOError:
+ err = _sys.exc_info()[1]
+ self.error(str(err))
+
+ # return the modified argument list
+ return new_arg_strings
+
def _match_argument(self, action, arg_strings_pattern):
# match the pattern for this action to the arg strings
nargs_pattern = self._get_nargs_pattern(action)
diff --git a/doc/source/ArgumentParser.rst b/doc/source/ArgumentParser.rst
index 2820b82..ff62745 100644
--- a/doc/source/ArgumentParser.rst
+++ b/doc/source/ArgumentParser.rst
@@ -11,7 +11,8 @@ ArgumentParser objects
* add_help_ - Add a -h/--help option to the parser. (default: True)
* argument_default_ - Set the global default value for arguments. (default: None)
* parents_ - A list of :class:ArgumentParser objects whose arguments should also be included.
- * prefix_chars_ - The set of characters that indicate optional arguments. (default: '-')
+ * prefix_chars_ - The set of characters that prefix optional arguments. (default: '-')
+ * fromfile_prefix_chars_ - The set of characters that prefix files from which additional arguments should be read. (default: None)
* formatter_class_ - A class for customizing the help output.
* conflict_handler_ - Usually unnecessary, defines strategy for resolving conflicting optionals.
* prog_ - Usually unnecessary, the name of the program (default: ``sys.argv[0]``)
@@ -121,6 +122,24 @@ The ``prefix_chars=`` argument defaults to ``'-'``. Supplying a set of character
Note that most parent parsers will specify :meth:`add_help` ``=False``. Otherwise, the ArgumentParser will see two ``-h/--help`` options (one in the parent and one in the child) and raise an error.
+fromfile_prefix_chars
+---------------------
+
+Sometimes, e.g. for particularly long argument lists, it may make sense to keep the list of arguments in a file rather than typing it out at the command line.
+If the ``fromfile_prefix_chars=`` argument is given to the ArgumentParser constructor, then arguments that start with any of the specified characters will be treated as files, and will be replaced by the arguments they contain. For example::
+
+ >>> open('args.txt', 'w').write('-f\nbar')
+ >>> parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
+ >>> parser.add_argument('-f')
+ >>> parser.parse_args(['-f', 'foo', '@args.txt'])
+ Namespace(f='bar')
+
+Arguments read from a file must be one per line (with each whole line being considered a single argument) and are treated as if they were in the same place as the original file referencing argument on the command line.
+So in the example above, the expression ``['-f', 'foo', '@args.txt']`` is considered equivalent to the expression ``['-f', 'foo', '-f', 'bar']``.
+
+The ``fromfile_prefix_chars=`` argument defaults to ``None``, meaning that arguments will never be treated as file references.
+
+
argument_default
----------------
diff --git a/test/test_argparse.py b/test/test_argparse.py
index d5a706b..e7ee189 100644
--- a/test/test_argparse.py
+++ b/test/test_argparse.py
@@ -56,6 +56,20 @@ class TestCase(unittest.TestCase):
super(TestCase, self).assertEqual(obj1, obj2)
+class TempDirMixin(object):
+
+ def setUp(self):
+ self.temp_dir = tempfile.mkdtemp()
+ self.old_dir = os.getcwd()
+ os.chdir(self.temp_dir)
+
+ def tearDown(self):
+ os.chdir(self.old_dir)
+ for filename in os.listdir(self.temp_dir):
+ os.remove(os.path.join(self.temp_dir, filename))
+ os.rmdir(self.temp_dir)
+
+
class Sig(object):
def __init__(self, *args, **kwargs):
@@ -1212,24 +1226,45 @@ class TestParserDefault42(ParserTestCase):
('--baz a b', NS(foo='a', bar=['b'], baz=True)),
]
-# =====================
-# Type conversion tests
-# =====================
-class TempDirMixin(object):
+class TestArgumentsFromFile(ParserTestCase, TempDirMixin):
+ """Test reading arguments from a file"""
def setUp(self):
- self.temp_dir = tempfile.mkdtemp()
- self.old_dir = os.getcwd()
- os.chdir(self.temp_dir)
+ super(TestArgumentsFromFile, self).setUp()
+ file_texts = [
+ ('hello', 'hello world!\n'),
+ ('recursive', '-a\n'
+ 'A\n'
+ '@hello'),
+ ('invalid', '@no-such-path\n'),
+ ]
+ for path, text in file_texts:
+ file = open(path, 'w')
+ file.write(text)
+ file.close()
- def tearDown(self):
- os.chdir(self.old_dir)
- for filename in os.listdir(self.temp_dir):
- os.remove(os.path.join(self.temp_dir, filename))
- os.rmdir(self.temp_dir)
+ parser_signature = Sig(fromfile_prefix_chars='@')
+ argument_signatures = [
+ Sig('-a'),
+ Sig('x'),
+ Sig('y', nargs='+'),
+ ]
+ failures = ['', '-b', 'X', '@invalid', '@missing']
+ successes = [
+ ('X Y', NS(a=None, x='X', y=['Y'])),
+ ('X -a A Y Z', NS(a='A', x='X', y=['Y', 'Z'])),
+ ('@hello X', NS(a=None, x='hello world!', y=['X'])),
+ ('X @hello', NS(a=None, x='X', y=['hello world!'])),
+ ('-a B @recursive Y Z', NS(a='A', x='hello world!', y=['Y', 'Z'])),
+ ('X @recursive Z -a B', NS(a='B', x='X', y=['hello world!', 'Z'])),
+ ]
+# =====================
+# Type conversion tests
+# =====================
+
class TestFileTypeRepr(TestCase):
def test_r(self):