Myrfy
Myrfy

Reputation: 675

How to handle command line args that have relations with argparse?

suppose I have a program called myprog that takes some filename as input, and I also want use command line args to set the open mode for each file. For example

myprog --input a.txt --mode r --input b.txt --input c.txt --mode a

Which means open file a.txt with mode r, file b.txt doesn't have a --mode arg, so open it with default mode r, and for the file c.txt, use the a mode to open it.

Upvotes: 1

Views: 740

Answers (2)

Ballack
Ballack

Reputation: 966

If the command line is like this:

myprog --input a.txt --mode r --input c.txt --mode a --input b.txt

It's ok to add some code like this:

import argparse

parser = argparser.ArgumentParser()
parser.add_argument('--input', action='append')
parser.add_argument('--mode', action='append')
args = parser.parse_args()
args_dict = vars(args)

Then you can parse the args object, args_dict variable. The value is like this:

$ python test.py --input test.txt --mode w --input test3.txt --input test2.txt --mode a
{'mode': ['w', 'a'], 'input': ['test.txt', 'test3.txt', 'test2.txt']}

You can iterate both 'input' key and 'mode' key in the args_dict variable, for the remain of input list(it's 'test2.txt' here), you can open it with 'r' mode.

But if your command line have to write something like:

myprog --input a.txt --mode r --input b.txt --input c.txt --mode a

I don't think it's easy to parse the b.txt with 'r' mode, because argparse don't know which mode is bind to the relative input...


Get inspiration from @mgilson 's comment and answer, I have found another way to define Action subclass, and make the 'mode' input useful.

class ExtendReadOnlyAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        inputs = namespace.input
        modes = getattr(namespace, self.dest)
        if modes is None:
            modes = []
        modes.extend(['r' for i in range(len(inputs) - len(modes))])
        modes[-1] = values
        setattr(namespace, self.dest, modes)

And the client code can be something like this:

import argparse

parser = argparser.ArgumentParser()
parser.add_argument('--input', action='append')
parser.add_argument('--mode', action=ExtendReadOnlyAction)
args = parser.parse_args()
args_dict = vars(args)

Then we can parse the args object, args_dict variable more easier. If the command line is like this:

$ python test.py --input test.txt --mode w --input test2.txt --input test3.txt --mode a

The result will be:

{'mode': ['w', 'r', 'a'], 'input': ['test.txt', 'test2.txt', 'test3.txt']}

In the other special way, if the command line is like this:

$ python test.py --input test.txt --mode w --input test2.txt --input test3.txt --input test4.txt

The result will be:

{'input': ['test.txt', 'test2.txt', 'test3.txt', 'test4.txt'], 'mode': ['w']}

And then you can parse the dict more easier, the 'test2.txt ~ test4.txt' in the input argument will have the default 'r' mode :)

Upvotes: 0

mgilson
mgilson

Reputation: 309929

This is a tricky problem because argparse doesn't give you any way to know which --input a particular --mode is associated with. You could change the structure of the command so that the filename and the mode are separated by a sentinel character:

myprog --input a.txt:r --input b.txt --input c.txt:a

Obviously this assumes you don't have files whose names end in :<mode> where <mode> is any acceptable file mode. If this is an OK structure, then this becomes as simple as writing a custom action or type to parse the string and return a suitable object. e.g.

def parse_fstr(s):
    filename, _, mode = s.rpartition(':')
    return (filename, mode or 'r')

Other solutions could involve using nargs='*' and then parsing out the list of arguments passed.


Finally, to implement what you've actually asked for without too much difficulty, we need to make an assumption. The assumption is that argparse will parse items from left to right. Given the functionality of the library, that is the only reasonable choice for implementation as far as I can tell...

Given that implementation, we can do this with a custom type and a custom Action. The type is simply a structure to keep a filename and a mode grouped together. We'll use argparse to construct a new instance of this type every time we hit an --input and append it to a list (This is supported out of the box by argparse). Next, we'll write a custom action to update the mode of the last "file struct" in the list every time we encouter a --mode argument.

import argparse


class FileInfo(object):
    def __init__(self, name, mode='r'):
        self.name = name
        self.mode = mode

    def __repr__(self):
        return 'FileInfo(name={!r}, mode={!r})'.format(self.name, self.mode)


class UpdateMode(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        try:
            last_file_info = namespace.input[-1]
        except IndexError:
            # No file-info added yet.  Error.
            parser.error('{} must come after an --input'.format(option_string or '--mode'))

        last_file_info.mode = values


parser = argparse.ArgumentParser()
parser.add_argument('--input', action='append', type=FileInfo)
parser.add_argument('--mode', action=UpdateMode)
print(parser.parse_args())

I've chosen to throw an error if --mode shows up before any --input, but if 2 --mode follow an --input, I'm just overwriting the previous value. If you wanted to more error checking, it'd be a simple matter of writing a little more code in the FileInfo class to make sure that no mode has already been set when you go to update the mode.

Upvotes: 4

Related Questions