Generating better help from argparse when nargs='*'

Viewed 319

Like many command line tools, mine accepts optional filenames. Argparse seems to support this via nargs='*', which is working for me as expected:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument(
        'files',
        help='file(s) to parse instead of stdin',
        nargs='*')

parser.parse_args()

However, the help output is bizarre:

$ ./help.py -h
usage: help.py [-h] [files [files ...]]

How can I avoid the nested optional and repeated parameter name? The repetition adds no information beyond [files ...], which is the traditional way optional parameter lists are indicated on Unix:

$ grep --help
usage: grep [-abcDEFGHhIiJLlmnOoqRSsUVvwxZ] [-A num] [-B num] [-C[num]]
        [-e pattern] [-f file] [--binary-files=value] [--color=when]
        [--context[=num]] [--directories=action] [--label] [--line-buffered]
        [--null] [pattern] [file ...]

$ ls --help
Usage:
  exa [options] [files...]

$ vim --help
Usage:
  nvim [options] [file ...]      Edit file(s)

Any help is appreciated. I'm trying argparse because using it seems to be a Python best practice, but this help output is a dealbreaker for me.

2 Answers

This was fixed in Python 3.9, see https://bugs.python.org/issue38438 and commit a0ed99bc that fixed it.

Your code produces the usage message you expect if run on 3.9:

Python 3.9.0 (default, Oct 12 2020, 02:44:01) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('files', help='file(s) to parse instead of stdin', nargs='*')
_StoreAction(option_strings=[], dest='files', nargs='*', const=None, default=None, type=None, choices=None, help='file(s) to parse instead of stdin', metavar=None)
>>> parser.print_help()
usage: [-h] [files ...]

For all versions prior to Python 3.9:

You may override the usage line by passing a usage="%(prog)s [options]" string when instantiating ArgumentParser or by updating the usage property of the existing instance.

You can combine this with ArgumentParser.format_usage() if you want to to let it generate a usage string, but use regex or string replacement to replace only nargs options.

For example:

import argparse
import re

parser = argparse.ArgumentParser()
parser.add_argument(
    'files',
    help='file(s) to parse instead of stdin',
    nargs='*',
)

usage = parser.format_usage()[7:]  # remove "usage: " prefix
parser.usage = re.sub(r'\[(.+?) \[\1 ...\]\]', r'[\1 ...]', usage)

parser.parse_args()

Produces:

usage: test.py [-h] [files ...]

positional arguments:
  files       file(s) to parse instead of stdin

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