6

I'm using Python3 argparse for complicated command-line interface. Lots of arguments, some of them are "verbose" to avoid misunderstandings.

parser = argparse.ArgumentParser(description='Command-line interface')
parser.add_argument('--long-param-one',
                    help='Long param one description',
                    dest='lond_param_one',
                    required=True)

parser.add_argument('--long-param-two',
                    help='Long param two description',
                    dest='lond_param_two',
                    required=True)

When param name is long enough and destination variable long as well, it causes ugly formatting when you calling script with --help

Command-line interface

optional arguments:
  -h, --help            show this help message and exit
  --long-param-one LONG_PARAM_ONE
                        Long param one description
  --long-param-two LONG_PARAM_TWO
                        Long param two description

I mean, parameter and value are on the one string, description is on other, even though there's plenty of space in console to the right so that put it in one line. Like the fisrt param --help does. When you have 30-40 params, command-line help readability is really worsen

  • Related: https://stackoverflow.com/questions/5462873/control-formatting-of-the-argparse-help-argument-list `argparse` by default assumes that the terminal is 80 chars wide (widely used convention) so even if you have a 40'' monitor and fullscreen terminal it will still only use 80 columns. There seem to be no public API to change that but the above question points out that there is a private API. Hopefully sometimes in the future the HelpFormatter will have a more customizable public API. – Giacomo Alzetta Oct 02 '18 at 09:05
  • 1
    BTW: one simple way to shorten the line is to use the `metavar` parameter so that instead of having `--long-param-one LONG_PARAM_ONE` you get `--long-param-one ` where `` is what you have chosen which might be way shorter (like `--this-is-a-nice-number N`). – Giacomo Alzetta Oct 02 '18 at 09:06
  • @GiacomoAlzetta I hoped since 2011 some enhancements happen :) I'll appreciate if you provide some code example with metavar –  Oct 02 '18 at 09:17
  • @GiacomoAlzetta I'm not aware of any proposals to enhance the API for the `HelpFormatter`. As you found with the `formatter_class` parameter you can do almost anything, either with the `lambda` or subclassing. So the power is there; but the question is, which features, if any, should be more accessible to beginning users? – hpaulj Oct 02 '18 at 23:38
  • @hpaulj No you can't. According to the documentation the *only* officially supported way to give a value to `formatter_class` is to provide one of the subclasses mentioned in the documentation. `max_help_position` is already an *implementation detail* which you shouldn't rely on. There is nothing bout how to customize or create a new `HelpFormatter` class. I believe they should really try to define an API for `HelpFormatter` so that people can actually rely on it. At least for simple stuff like `max_help_position`, maybe not all aspects immediately. – Giacomo Alzetta Oct 03 '18 at 07:13
  • The documentation for `argparse` isn't a formal API defining what's public and private (vague concepts in python), or what's fixed or subject to change. – hpaulj Oct 03 '18 at 14:28

2 Answers2

12

argparse by default limits the maximum space taken for the option+metavar and will write the help message on a separate line, even if the terminal would be big enough to accomodate both.

Consider this example script:

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--name', help='help help')
parser.add_argument('--parameter', help='help help')
parser.add_argument('--parameter-name', help='help help')
parser.add_argument('--this-parameter-name', help='help help')
parser.add_argument('--this-is-parameter-name', help='help help')
parser.add_argument('--this-is-a-parameter-name', help='help help')
parser.add_argument('--this-is-a-long-parameter-name', help='help help')
parser.add_argument('--this-is-a-very-long-parameter-name', help='help help')
parser.add_argument('--this-is-a-very-very-long-parameter-name', help='help help')
parser.add_argument('--this-is-a-very-very-very-long-parameter-name', help='help help')

parser.parse_args()

Results in the following output:

usage: a.py [-h] [--name NAME] [--parameter PARAMETER]
            [--parameter-name PARAMETER_NAME]
            [--this-parameter-name THIS_PARAMETER_NAME]
            [--this-is-parameter-name THIS_IS_PARAMETER_NAME]
            [--this-is-a-parameter-name THIS_IS_A_PARAMETER_NAME]
            [--this-is-a-long-parameter-name THIS_IS_A_LONG_PARAMETER_NAME]
            [--this-is-a-very-long-parameter-name THIS_IS_A_VERY_LONG_PARAMETER_NAME]
            [--this-is-a-very-very-long-parameter-name THIS_IS_A_VERY_VERY_LONG_PARAMETER_NAME]
            [--this-is-a-very-very-very-long-parameter-name THIS_IS_A_VERY_VERY_VERY_LONG_PARAMETER_NAME]

optional arguments:
  -h, --help            show this help message and exit
  --name NAME           help help
  --parameter PARAMETER
                        help help
  --parameter-name PARAMETER_NAME
                        help help
  --this-parameter-name THIS_PARAMETER_NAME
                        help help
  --this-is-parameter-name THIS_IS_PARAMETER_NAME
                        help help
  --this-is-a-parameter-name THIS_IS_A_PARAMETER_NAME
                        help help
  --this-is-a-long-parameter-name THIS_IS_A_LONG_PARAMETER_NAME
                        help help
  --this-is-a-very-long-parameter-name THIS_IS_A_VERY_LONG_PARAMETER_NAME
                        help help
  --this-is-a-very-very-long-parameter-name THIS_IS_A_VERY_VERY_LONG_PARAMETER_NAME
                        help help
  --this-is-a-very-very-very-long-parameter-name THIS_IS_A_VERY_VERY_VERY_LONG_PARAMETER_NAME
                        help help

The simplest way to try to avoid this problem is specifying the metavar explicitly and use a short value, so instead ov THIS_IS_A_VERY_VERY_VERY_LONG_PARAMETER_NAME you can use, say, X. For example:

import argparse

parser = argparse.ArgumentParser()
m = 'X'
parser.add_argument('--name', help='help help', metavar=m)
parser.add_argument('--parameter', help='help help', metavar=m)
parser.add_argument('--parameter-name', help='help help', metavar=m)
parser.add_argument('--this-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-a-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-a-long-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-a-very-long-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-a-very-very-long-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-a-very-very-very-long-parameter-name', help='help help', metavar=m)

parser.parse_args()

Which results in:

usage: a.py [-h] [--name X] [--parameter X] [--parameter-name X]
            [--this-parameter-name X] [--this-is-parameter-name X]
            [--this-is-a-parameter-name X] [--this-is-a-long-parameter-name X]
            [--this-is-a-very-long-parameter-name X]
            [--this-is-a-very-very-long-parameter-name X]
            [--this-is-a-very-very-very-long-parameter-name X]

optional arguments:
  -h, --help            show this help message and exit
  --name X              help help
  --parameter X         help help
  --parameter-name X    help help
  --this-parameter-name X
                        help help
  --this-is-parameter-name X
                        help help
  --this-is-a-parameter-name X
                        help help
  --this-is-a-long-parameter-name X
                        help help
  --this-is-a-very-long-parameter-name X
                        help help
  --this-is-a-very-very-long-parameter-name X
                        help help
  --this-is-a-very-very-very-long-parameter-name X
                        help help

this is already way better, but as you can see with very long parameter names it still wont write all the text on one line.

The only way to achieve what you want is to specify a formatter_class and use the max_help_position as described in this question. This however is not part of the public API of the module. I have no idea when they did not add at least a couple of useful parameters to the public API.

You still probably want to specify a metavar:

import argparse

formatter = lambda prog: argparse.HelpFormatter(prog,max_help_position=52)
parser = argparse.ArgumentParser(formatter_class=formatter)
m = 'X'
parser.add_argument('--name', help='help help', metavar=m)
parser.add_argument('--parameter', help='help help', metavar=m)
parser.add_argument('--parameter-name', help='help help', metavar=m)
parser.add_argument('--this-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-a-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-a-long-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-a-very-long-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-a-very-very-long-parameter-name', help='help help', metavar=m)
parser.add_argument('--this-is-a-very-very-very-long-parameter-name', help='help help', metavar=m)

parser.parse_args()

The output will be:

usage: a.py [-h] [--name X] [--parameter X] [--parameter-name X]
            [--this-parameter-name X] [--this-is-parameter-name X]
            [--this-is-a-parameter-name X] [--this-is-a-long-parameter-name X]
            [--this-is-a-very-long-parameter-name X]
            [--this-is-a-very-very-long-parameter-name X]
            [--this-is-a-very-very-very-long-parameter-name X]

optional arguments:
  -h, --help                                        show this help message and
                                                    exit
  --name X                                          help help
  --parameter X                                     help help
  --parameter-name X                                help help
  --this-parameter-name X                           help help
  --this-is-parameter-name X                        help help
  --this-is-a-parameter-name X                      help help
  --this-is-a-long-parameter-name X                 help help
  --this-is-a-very-long-parameter-name X            help help
  --this-is-a-very-very-long-parameter-name X       help help
  --this-is-a-very-very-very-long-parameter-name X  help help

You could probably try to determine the size of the terminal (most terminals provide a WIDTH or COLUMNS env variable that may be useful for that) to decide the value of max_help_position that would be best in that situation.


To have all the parameters help on one line (assuming big enough terminal) you want:

max_help_position >= max(len(param.name)+len(param.metavar) for param in params)
Giacomo Alzetta
  • 2,431
  • 6
  • 17
1

The current HelpFormatter does check os.environ['COLUMNS'] for terminal width. But that does not get updated dynamically, and might not even be set.

There is a patch

https://bugs.python.org/file24602/issue13041.patch argparse: terminal width is not detected properly

that apparently was recently put into 3.8, that looks at shutil.get_terminal_size().columns instead.


As to why argparse doesn't provide more direct control of this width - the design philosophy has been allow a custom formatter_class specification, rather than a (potentially) large set of formatting parameters. Most of the parameters to ArgumentParser have to do with parsing, not help formatting. The goal is to allow complete customization without cluttering the inputs with a lot of rarely used parameters.

The HelpFormatter class does take several keyword parameters:

HelpFormatter.__init__(self, prog, indent_increment=2, max_help_position=24, width=None)

But the current method of creating a formatter just passes the prog parameter.

Giacomo's answer shows how specify these other parameters:

formatter = lambda prog: argparse.HelpFormatter(prog,max_help_position=52)

You could also subclass HelpFormatter to customize the formatting. That's what the alternatives like RawTextHelpFormatter do.

More on customizing the formatter

From the argparse documentation:

formatter_class

ArgumentParser objects allow the help formatting to be customized by specifying an alternate formatting class. Currently, there are four such classes:

Providing and listing these 4 classes is not meant to be restrictive. Other customization is allowed, even encouraged.

In https://bugs.python.org/issue13023, Steven Bethard, the original author of argparse, advocates writing your own formatter class:

Your solution is actually the current recommended solution - mix together both classes that you want to combine and pass your subclass as the parameter. This should probably be documented somewhere (and tested more).

The mixing he's talking about is:

class myFormatter(argparse.RawDescriptionHelpFormatter,
                  argparse.ArgumentDefaultsHelpFormatter):
    pass

I addressed the use of max_help_position 3 years ago:

https://bugs.python.org/issue25297 max_help_position is not works in argparse library

and a SO question:

max_help_position is not works in python argparse library

Other examples in argparse where you are allowed to provide custom classes or functions include:

https://docs.python.org/3/library/argparse.html#action-classes https://docs.python.org/3/library/argparse.html#the-namespace-object https://docs.python.org/3/library/argparse.html#customizing-file-parsing https://docs.python.org/3/library/argparse.html#type

I wouldn't worry about the max_help_position parameter disappearing or being disabled. If I have any say in the matter, any proposed change like that will be rejected on the grounds that it could have backward compatibility issues.

In practice it is easiest to change the documentation to match code, or to better illustrate vague points. In this case the lambda call to HelpFormatter could be documented. I can also imagine defining a small function that does the same thing. Adding features is easiest when there's no chance of harming existing users.

Community
  • 1
  • 1
hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • From what you say and what I understand even in python3.8 there would still be problems because `max_help_position` is static. The width of the terminal is taken into account only for priting the help messages and usage part but not for the alignment of options & corresponding help text. – Giacomo Alzetta Oct 03 '18 at 07:16
  • Digging in the bug/issues I found that we did look at `max_help_position` some 3 years ago. – hpaulj Oct 03 '18 at 16:10