spagh-eddie
spagh-eddie

Reputation: 162

Variable-length argparse argument lists

I want to have a set of arguments be passed into a script with an equal amount of inputs and outputs arguments. I know that I can parse along the lines of

inputs, outputs = sys.argv[:halfway], sys.argv[halfway:]

taking into account sys.argv[0] being the name, but I want the helpful features of argparse.

I also know that I can change the code to parser.add_argument('-i', 'inputs', nargs='+') so that I can specify my arguments as python testarg.py -i 1 2 -o 3 4, but I do not want to use that syntax as there is already a precedent of one-input, one-output python testarg.py input output which I would like to keep by making the syntax python testarg.py inputs[...] outputs[...]

This is the closest I get

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('inputs', nargs='+')
parser.add_argument('outputs', nargs='+')
print(parser.parse_args())
$ python testarg.py 1
usage: testarg.py [-h] input [input ...] output [output ...]
testarg.py: error: the following arguments are required: output

$ python testarg.py 1 2
Namespace(inputs=['1'], outputs=['2'])

$ python testarg.py 1 2 3 4
Namespace(inputs=['1', '2', '3'], outputs=['4'])

I want

Namespace(inputs=['1', '2'], outputs=['3', '4'])

Upvotes: 0

Views: 663

Answers (2)

hpaulj
hpaulj

Reputation: 231355

The nargs are modelled on (and even use) the regex wildcard quantifiers

In this case:

Namespace(inputs=['1', '2', '3'], outputs=['4'])

one value has been allocated to outputs (because it is "one-or-more"), and the rest, the "more" goes to inputs.

Now if you could accept

python prog.py --inputs 1 2 --outputs 3 4

the '+' would work as expected.

But with variable length positionals (or optional followed by positional), there's no way to tell it where the first list ends and second starts.

Of course if you like the argparse help, you could adjust the lists balance after parsing - e.g move the '3' to the other list. Nothing wrong with tweaking the parsed values. You won't get extra "good boy" points for doing everything in the parser itself.

Upvotes: 1

mattficke
mattficke

Reputation: 797

The click library can do this, it supports callback functions that can modify argument values.

import click


def split_args(context, param, value):
    num_args = len(value)
    if num_args % 2:
        raise click.BadParameter(
            f"Must provide an even number of arguments, got {num_args} arguments"
        )
    midpoint = num_args // 2
    return value[:midpoint], value[midpoint:]


@click.command()
@click.argument("args", callback=split_args, nargs=-1)
def io(args):
    inputs, outputs = args
    print("inputs: ", inputs)
    print("outputs: ", outputs)


if __name__ == "__main__":
    io()
$ python3 testarg.py 1 2 3 4
inputs:  ('1', '2')
outputs:  ('3', '4')

Upvotes: 2

Related Questions