Argparse - better help for positional arguments

I’m trying to write a script that handles a series of arguments. The first part of the argument list must be a series of strings of the form NAME=VALUE, followed by a further series of strings which don’t have that form. For comparison, think of the argument structure for the Unix env command: env [NAME=VALUE]... [COMMAND [ARG]...]

My actual script has additional options, so I want to use argparse, and that’s fine. Here’s a massively simplified example:

from argparse import ArgumentParser

p = ArgumentParser()
p.add_argument("arg", nargs="*")
args = p.parse_args()

for i in range(len(args.arg)):
    if "=" not in args.arg[i]:
        break
assignments, rest = args.arg[:i], args.arg[i:]
print(assignments)
print(rest)

The problem is that the command line help generated by argparse doesn’t explain any of this structure:

❯ py example.py --help
usage: example.py [-h] [arg ...]

positional arguments:
  arg

optional arguments:
  -h, --help  show this help message and exit

What I’d like is more like:

❯ py example.py --help
usage: example.py [-h] [NAME=VAL ...] [ARG...]

positional arguments:
  NAME=VAL      An assignment of value VAL to NAME
  ARG           Other stuff

optional arguments:
  -h, --help  show this help message and exit

I can do that by having two positional arguments,

p.add_argument("arg1", nargs="*", metavar="NAME=VAL", help="An assignment of value VAL to NAME")
p.add_argument("arg2", nargs="*", metavar="ARG", help="Other stuff")

but all of the arguments are picked up by arg1 and arg2 is empty, so the code looks rather clumsy, as the declared arguments don’t match the structure.

Is there a better way of doing this? Either a way to customise the help text (preferably without having to manually handle the help for all of the options I’ve omitted from this example), or even better a way of telling argparse how to detect when to end arg1 and move onto arg2?

(Worst case, I guess I could just name the two positional arguments “args” and “dummy”, and move on…)

What about this? The usage line is a bit messy, and you have to read the details for it to be clear. Or is this what you’re trying to avoid?

p.add_argument(
    "arg",
    nargs="*",
    metavar="NAME=VAL | ARGS",
    help="Assign VAL to NAME; these must come before ARGS",
)
argtest.py --help
usage: argtest.py [-h] [NAME=VAL | ARGS [NAME=VAL | ARGS ...]]

positional arguments:
  NAME=VAL | ARGS  Assign VAL to NAME; these must come before ARGS

optional arguments:
  -h, --help       show this help message and exit

I’m trying to avoid the help being horrible. It’s easy enough to get the behaviour I want, but the help is terrible. I could just write my own help and override the lot, but this is a little utility script, not a big application, so I really don’t want to invest a lot of time in this - but I also don’t want the help to be incomprehensible :slightly_frowning_face:

I’ve had this problem in the past, and the solution I went with was to put one of the positional arguments into a action="append" keyword argument:

parser.add_argument("arg", nargs="*", help="other stuff")
parser.add_argument(
    "-v", "--value",
    action="append",
    metavar="NAME=VAL",
    help="set value (can be specified multiple times)",
)

Yeah, if I were writing the interface from scratch I may well go with that - but I’m replacing existing functionality with a Python script, and I want to keep the interface as close to the original as possible.

I’m getting the impression that hacking around the help is my best option, so I’ll probably go with that.

(I originally posted because I thought I’d seen something that said new Action types could control what was consumed from the command line, and I thought that might work - but I couldn’t find any details, and having scanned the source, I think that’s wrong, and I’d just misread something).