1

My Python program accepts command line arguments using the argparse module and it works as intended, however, the help text is a bit misleading and I would like to fix it for others using my program.

Currently, I have a positional argument which is a directory and an optional argument, -p, which accepts an arbitrary number of package names using the nargs=+ argument to the add_argument function. The DIR positional argument needs to be specified before the optional argument list, otherwise the directory will be wrongly added to the list of package names and it will error out saying that a positional argument was not specified. The help output currently looks like the following:

package_info.py --help
usage: package_info.py [-h] [-v] [--no-cache] [-g FILE] [-p [PKG [PKG ...]]]
                       DIR

Get information on packages in ros workspace.

positional arguments:
  DIR                   The directory containing rospackages.

optional arguments:
  -h, --help            show this help message and exit
  -v, --verbose         Enables verbose mode
  --no-cache            Do not cache the dependency graph
  -g FILE, --graph-file FILE
                        The graph file to load from or save to.
  -p [PKG [PKG ...]], --packages [PKG [PKG ...]]
                        The packages to provide information on.

I would like it to be formatted with DIR showing up before the -p flag to be more clear to users that this argument must be specified first, like the following:

package_info.py --help
usage: package_info.py DIR [-h] [-v] [--no-cache] [-g FILE] [-p [PKG [PKG ...]]]
            .
            .
            .

or

package_info.py --help
usage: package_info.py DIR
                       [-h] [-v] [--no-cache] [-g FILE] [-p [PKG [PKG ...]]]
            .
            .
            .

Is there a simple way to format the help message, or will I need to write a custom help message formatter?

Logan
  • 35
  • 10

2 Answers2

2

You are talking about the usage line.

The usage formatter does separate out the positionals and puts them last, possibly on their own line if long enough. And yes, the does conflict with the handling of that '+' flagged argument. It shouldn't, but the fix is too complicated to simply drop in.

I wouldn't recommend changing the usage formatter - that set of methods is too complicated (and fragile) to be an easy patch.

Providing a custom usage parameter when defining ArgumentParser would be the easiest fix. I don't recall how it interacts with the line wrapping.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
1

After looking at the answer provided by hpaulj, I was able to look through the source for python3.5, argparse version 1.1 which is what I have on my system, and found a couple lines I could modify to make it work. Granted, not an elegant solution, but flexible (I am constantly adding/removing/modifying the command line arguments as the program evolves) and quick to implement.

I created a custom help formatter class which inherited from the argparse.HelpFormatter class. In my custom class, I copied the _format_usage function and changed the following lines:

# build full usage string
format = self._format_actions_usage
action_usage = format(optionals + positionals, groups)
usage = ' '.join([s for s in [prog, action_usage] if s])

to

# build full usage string
format = self._format_actions_usage
action_usage = format(positionals + optionals, groups)
usage = ' '.join([s for s in [prog, action_usage] if s])

This gives me the formatting I need if no wrapping of the text is needed.

I also changed the following:

# if prog is short, follow it with optionals or positionals
if len(prefix) + len(prog) <= 0.75 * text_width:
    indent = ' ' * (len(prefix) + len(prog) + 1)
    if opt_parts:
        lines = get_lines([prog] + opt_parts, indent, prefix)
        lines.extend(get_lines(pos_parts, indent))
    elif pos_parts:
        lines = get_lines([prog] + pos_parts, indent, prefix)
    else:
        lines = [prog]

to

# if prog is short, follow it with optionals or positionals
if len(prefix) + len(prog) <= 0.75 * text_width:
    indent = ' ' * (len(prefix) + len(prog) + 1)
    if pos_parts:
        lines = get_lines([prog] + pos_parts, indent, prefix)
        lines.extend(get_lines(opt_parts, indent))
    elif opt_parts:
        lines = get_lines([prog] + opt_parts, indent, prefix)
    else:
        lines = [prog]

Which gives me the formatting I need when wrapping is needed.

Logan
  • 35
  • 10