59

I have written the following sample code to demonstrate my issue.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-v', '--version', action='version',
                    version='%(prog)s 1.0')
parser.parse_args()

This produces the following help message.

$ python foo.py --help
usage: foo.py [-h] [-v]

optional arguments:
  -h, --help     show this help message and exit
  -v, --version  show program's version number and exit

I want to customize this help output such that it capitalizes all phrases and sentences, and puts period after sentences. In other words, I want the help message to be generated like this.

$ python foo.py --help
Usage: foo.py [-h] [-v]

Optional arguments:
  -h, --help     Show this help message and exit.
  -v, --version  Show program's version number and exit.

Is this something that I can control using the argparse API. If so, how? Could you please provide a small example that shows how this can be done?

Lone Learner
  • 18,088
  • 20
  • 102
  • 200
  • 1
    Have you tried setting [`help`](https://docs.python.org/3/library/argparse.html#help)? – jonrsharpe Mar 07 '16 at 15:11
  • 2
    Oh, I see - then you could set `add_help` to `False` and do it manually. But lowercase is the convention for these things. – jonrsharpe Mar 07 '16 at 15:14
  • 1
    @jonrsharpe, how do you know that? `git` uppercases. `curl` uppercases. `apt-get` uppercases. `gcc` uppercases. If there is a convention (and I doubt that there is), I'm not certain argparse is on the right side of it. – Paul Draper Mar 10 '21 at 15:53
  • @PaulDraper I should have been more specific, it's the convention *in `argparse`* (and in Python in general, `python3 --help` shows lowercase help for the flags). – jonrsharpe Mar 10 '21 at 16:00
  • Couldn't you just edit the text that `parser.format_help` produces? Something like `parser.usage = edit(parser.format_help())`, then just write a function `edit` to parse each line converting the help string to Title format. – SurpriseDog Apr 19 '21 at 18:22

4 Answers4

60

First of all: capitalising those phrases flies in the face of convention, and argparse isn't really tooled to help you change these strings easily. You have three different classes of strings here: boilerplate text from the help formatter, section titles, and help text per specific option. All these strings are localisable; you could just provide a 'capitalised' translation for all of these strings via the gettext() module support. That said, you can reach in and replace all these strings if you are determined enough and read the source code a little.

The version action includes a default help text, but you can supply your own by setting the help argument. The same applies to the help action; if you set the add_help argument to False you can add that action manually:

parser = argparse.ArgumentParser(add_help=False)

parser.add_argument('-v', '--version', action='version',
                    version='%(prog)s 1.0', help="Show program's version number and exit.")
parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS,
                    help='Show this help message and exit.')

Next, the optional arguments message is a group title; each parser has two default groups, one for positional arguments, the other for optional. You can reach these by the attributes _positionals and _optionals, both of which have a title attribute:

parser._positionals.title = 'Positional arguments'
parser._optionals.title = 'Optional arguments'

Be warned, by accessing names starting with an underscore you are venturing into the undocumented private API of the module, and your code may break in future updates.

Finally, to change the usage string, you'll have to subclass the help formatter; pass the subclass in as the formatter_class argument:

class CapitalisedHelpFormatter(argparse.HelpFormatter):
    def add_usage(self, usage, actions, groups, prefix=None):
        if prefix is None:
            prefix = 'Usage: '
        return super(CapitalisedHelpFormatter, self).add_usage(
            usage, actions, groups, prefix)

parser = argparse.ArgumentParser(formatter_class=CapitalisedHelpFormatter)

Demo, putting these all together:

>>> import argparse
>>> class CapitalisedHelpFormatter(argparse.HelpFormatter):
...     def add_usage(self, usage, actions, groups, prefix=None):
...         if prefix is None:
...             prefix = 'Usage: '
...         return super(CapitalisedHelpFormatter, self).add_usage(
...             usage, actions, groups, prefix)
...
>>> parser = argparse.ArgumentParser(add_help=False, formatter_class=CapitalisedHelpFormatter)
>>> parser._positionals.title = 'Positional arguments'
>>> parser._optionals.title = 'Optional arguments'
>>> parser.add_argument('-v', '--version', action='version',
...                     version='%(prog)s 1.0', help="Show program's version number and exit.")
_VersionAction(option_strings=['-v', '--version'], dest='version', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help="Show program's version number and exit.", metavar=None)
>>> parser.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS,
...                     help='Show this help message and exit.')
_HelpAction(option_strings=['-h', '--help'], dest='help', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help='Show this help message and exit.', metavar=None)
>>> print(parser.format_help())
Usage: [-v] [-h]

Optional arguments:
  -v, --version  Show program's version number and exit.
  -h, --help     Show this help message and exit.
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • I think that even the non-underscore methods of `argparse.HelpFormatter` are private API: https://github.com/python/cpython/blob/v3.6.5/Lib/argparse.py#L58 "Also note that HelpFormatter and RawDescriptionHelpFormatter are only considered public as object names -- the API of the formatter objects is still considered an implementation detail." – Ciro Santilli OurBigBook.com Aug 26 '18 at 10:47
  • 5
    As noted by Paul Draper in a separate comment, there isn't really a convention regarding capitalization for command-line programs, so your opening phrase overstates the case. Run `--help` for git, curl, vim, gcc, perl, ruby, diff, rsync to see various approaches. Even python itself takes a slightly different path than argparse: mostly all-lowercase, but with a handful of deviations (the "Options and arguments" heading and arguably the `-X dev` bullet points). – FMc Apr 27 '21 at 01:57
  • @FMc: I wasn't talking so much about what is common among popular command-line tools and more about the conventions of English grammar. The programmers behind popular OSS tools come from a very wide range of cultural backgrounds with varying levels of mastery of English and its (sometimes convoluted) capitalisation rules, so it is not surprising that there is a lot of variability there. – Martijn Pieters May 06 '21 at 07:42
7

Instead of relying on internal API (which is subject to change without notice), here is an alternative using public API only. It is arguably more complex but in turn gives you maximum control over what is printed:

class ArgumentParser(argparse.ArgumentParser):

    def __init__(self, *args, **kwargs):
        super(ArgumentParser, self).__init__(*args, **kwargs)
        self.program = { key: kwargs[key] for key in kwargs }
        self.options = []

    def add_argument(self, *args, **kwargs):
        super(ArgumentParser, self).add_argument(*args, **kwargs)
        option = {}
        option["flags"] = [ item for item in args ]
        for key in kwargs:
            option[key] = kwargs[key]
        self.options.append(option)

    def print_help(self):
        # Use data stored in self.program/self.options to produce
        # custom help text

How it works:

  • tap into constructor of argparse.ArgumentParser to capture and store program info (e.g. description, usage) in self.program
  • tap into argparse.ArgumentParser.add_argument() to capture and store added arguments (e.g. flags, help, defaults) in self.options
  • redefine argparse.ArgumentParser.print_help() and use previously stored program info / arguments to produce help text

Here is a full example covering some common use cases. Note that this is by no means complete (e.g. there is no support for positional arguments or options with more than one argument), but it should provide a good impression of what is possible:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import sys
import argparse
import textwrap

class ArgumentParser(argparse.ArgumentParser):

    def __init__(self, *args, **kwargs):
        super(ArgumentParser, self).__init__(*args, **kwargs)
        self.program = { key: kwargs[key] for key in kwargs }
        self.options = []

    def add_argument(self, *args, **kwargs):
        super(ArgumentParser, self).add_argument(*args, **kwargs)
        option = {}
        option["flags"] = [ item for item in args ]
        for key in kwargs:
            option[key] = kwargs[key]
        self.options.append(option)

    def print_help(self):
        wrapper = textwrap.TextWrapper(width=80)

        # Print usage
        if "usage" in self.program:
            print("Usage: %s" % self.program["usage"])
        else:
            usage = []
            for option in self.options:
                usage += [ "[%s %s]" % (item, option["metavar"]) if "metavar" in option else "[%s %s]" % (item, option["dest"].upper()) if "dest" in option else "[%s]" % item for item in option["flags"] ]
            wrapper.initial_indent = "Usage: %s " % os.path.basename(sys.argv[0])
            wrapper.subsequent_indent = len(wrapper.initial_indent) * " "
            output = str.join(" ", usage)
            output = wrapper.fill(output)
            print(output)
        print()

        # Print description
        if "description" in self.program:
            print(self.program["description"])
            print()

        # Print options
        print("Options:")
        maxlen = 0
        for option in self.options:
            option["flags2"] = str.join(", ", [ "%s %s" % (item, option["metavar"]) if "metavar" in option else "%s %s" % (item, option["dest"].upper()) if "dest" in option else item for item in option["flags"] ])
            if len(option["flags2"]) > maxlen:
                maxlen = len(option["flags2"])
        for option in self.options:
            template = "  %-" + str(maxlen) + "s  "
            wrapper.initial_indent = template % option["flags2"]
            wrapper.subsequent_indent = len(wrapper.initial_indent) * " "
            if "help" in option and "default" in option:
                output = option["help"]
                output += " (default: '%s')" % option["default"] if isinstance(option["default"], str) else " (default: %s)" % str(option["default"])
                output = wrapper.fill(output)
            elif "help" in option:
                output = option["help"]
                output = wrapper.fill(output)
            elif "default" in option:
                output = "Default: '%s'" % option["default"] if isinstance(option["default"], str) else "Default: %s" % str(option["default"])
                output = wrapper.fill(output)
            else:
                output = wrapper.initial_indent
            print(output)

# Main
if (__name__ == "__main__"):
    #parser = argparse.ArgumentParser(description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)
    #parser = argparse.ArgumentParser(usage="%s [OPTION]..." % os.path.basename(sys.argv[0]), description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)
    #parser = ArgumentParser(usage="%s [OPTION]..." % os.path.basename(sys.argv[0]), description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)
    parser = ArgumentParser(description="Download program based on some library.", argument_default=argparse.SUPPRESS, allow_abbrev=False, add_help=False)

    parser.add_argument("-c", "--config-file", action="store", dest="config_file", metavar="file", type=str, default="config.ini")
    parser.add_argument("-d", "--database-file", action="store", dest="database_file", metavar="file", type=str, help="SQLite3 database file to read/write", default="database.db")
    parser.add_argument("-l", "--log-file", action="store", dest="log_file", metavar="file", type=str, help="File to write log to", default="debug.log")
    parser.add_argument("-f", "--data-file", action="store", dest="data_file", metavar="file", type=str, help="Data file to read", default="data.bin")
    parser.add_argument("-t", "--threads", action="store", dest="threads", type=int, help="Number of threads to spawn", default=3)
    parser.add_argument("-p", "--port", action="store", dest="port", type=int, help="TCP port to listen on for access to the web interface", default="12345")
    parser.add_argument("--max-downloads", action="store", dest="max_downloads", metavar="value", type=int, help="Maximum number of concurrent downloads", default=5)
    parser.add_argument("--download-timeout", action="store", dest="download_timeout", metavar="value", type=int, help="Download timeout in seconds", default=120)
    parser.add_argument("--max-requests", action="store", dest="max_requests", metavar="value", type=int, help="Maximum number of concurrent requests", default=10)
    parser.add_argument("--request-timeout", action="store", dest="request_timeout", metavar="value", type=int, help="Request timeout in seconds", default=60)
    parser.add_argument("--main-interval", action="store", dest="main_interval", metavar="value", type=int, help="Main loop interval in seconds", default=60)
    parser.add_argument("--thread-interval", action="store", dest="thread_interval", metavar="value", type=int, help="Thread loop interval in milliseconds", default=500)
    parser.add_argument("--console-output", action="store", dest="console_output", metavar="value", type=str.lower, choices=["stdout", "stderr"], help="Output to use for console", default="stdout")
    parser.add_argument("--console-level", action="store", dest="console_level", metavar="value", type=str.lower, choices=["debug", "info", "warning", "error", "critical"], help="Log level to use for console", default="info")
    parser.add_argument("--logfile-level", action="store", dest="logfile_level", metavar="value", type=str.lower, choices=["debug", "info", "warning", "error", "critical"], help="Log level to use for log file", default="info")
    parser.add_argument("--console-color", action="store", dest="console_color", metavar="value", type=bool, help="Colorized console output", default=True)
    parser.add_argument("--logfile-color", action="store", dest="logfile_color", metavar="value", type=bool, help="Colorized log file output", default=False)
    parser.add_argument("--log-template", action="store", dest="log_template", metavar="value", type=str, help="Template to use for log lines", default="[%(created)d] [%(threadName)s] [%(levelname)s] %(message)s")
    parser.add_argument("-h", "--help", action="help", help="Display this message")

    args = parser.parse_args(["-h"])

Produced output:

Usage: argparse_custom_usage.py [-c file] [--config-file file] [-d file]
                                [--database-file file] [-l file] [--log-file
                                file] [-f file] [--data-file file] [-t THREADS]
                                [--threads THREADS] [-p PORT] [--port PORT]
                                [--max-downloads value] [--download-timeout
                                value] [--max-requests value] [--request-timeout
                                value] [--main-interval value] [--thread-
                                interval value] [--console-output value]
                                [--console-level value] [--logfile-level value]
                                [--console-color value] [--logfile-color value]
                                [--log-template value] [-h] [--help]

Download program based on some library.

Options:
  -c file, --config-file file    Default: 'config.ini'
  -d file, --database-file file  SQLite3 database file to read/write (default:
                                 'database.db')
  -l file, --log-file file       File to write log to (default: 'debug.log')
  -f file, --data-file file      Data file to read (default: 'data.bin')
  -t THREADS, --threads THREADS  Number of threads to spawn (default: 3)
  -p PORT, --port PORT           TCP port to listen on for access to the web
                                 interface (default: '12345')
  --max-downloads value          Maximum number of concurrent downloads
                                 (default: 5)
  --download-timeout value       Download timeout in seconds (default: 120)
  --max-requests value           Maximum number of concurrent requests (default:
                                 10)
  --request-timeout value        Request timeout in seconds (default: 60)
  --main-interval value          Main loop interval in seconds (default: 60)
  --thread-interval value        Thread loop interval in milliseconds (default:
                                 500)
  --console-output value         Output to use for console (default: 'stdout')
  --console-level value          Log level to use for console (default: 'info')
  --logfile-level value          Log level to use for log file (default: 'info')
  --console-color value          Colorized console output (default: True)
  --logfile-color value          Colorized log file output (default: False)
  --log-template value           Template to use for log lines (default:
                                 '[%(created)d] [%(threadName)s] [%(levelname)s]
                                 %(message)s')
  -h, --help                     Display this message

EDIT:

If have since extended the example significantly and will continue to do so on GitHub.

Fonic
  • 2,625
  • 23
  • 20
6

Martijn has give a couple of the fixes that came to mind - the providing the help parameter, and a custom Formatter class.

One other partial fix is to modify the help string after the argument is created. add_argument creates and returns an Action object that contains the parameters and defaults. You can save a link to this, and modify the Action. You can also get a list of those actions, and act on that.

Let me illustrate, for a simple parser with the default help and one other argument, the action list is:

In [1064]: parser._actions
Out[1064]: 
[_HelpAction(option_strings=['-h', '--help'], dest='help', nargs=0, const=None, default='==SUPPRESS==', type=None, choices=None, help='show this help message and exit', metavar=None),
 _StoreAction(option_strings=['-f', '--foo'], dest='foo', nargs=None, const=None, default=None, type=None, choices=None, help=None, metavar=None)]

I can view and modify the help attribute of any of these:

In [1065]: parser._actions[0].help
Out[1065]: 'show this help message and exit'
In [1066]: parser._actions[0].help='Show this help message and exit.'

producing this help:

In [1067]: parser.parse_args(['-h'])
usage: ipython3 [-h] [-f FOO]    
optional arguments:
  -h, --help         Show this help message and exit.
  -f FOO, --foo FOO

Using the parser._actions list uses a 'private' attribute, which some people consider unwise. But in Python that public/private distinction is not tight, and can be broken with care. Martijn is doing that by accessing the parser._positionals.title.

Another way to change that group title is with custom argument groups

ogroup=parser.add_argument_group('Correct Optionals Title')
ogroup.add_argument('-v',...)
ogroup.add_argument('-h',...)
hpaulj
  • 221,503
  • 14
  • 230
  • 353
0

This custom argparse.ArgumentParser can be used to capitalize the output message which also works with subparsers created with add_subparsers().

Works with Python 3.11, but don't know how long it will be true.

import argparse


class ArgumentParser(argparse.ArgumentParser):
    """Custom `argparse.ArgumentParser` which capitalizes the output message."""

    class _ArgumentGroup(argparse._ArgumentGroup):
        def __init__(self, *args, **kwargs) -> None:
            super().__init__(*args, **kwargs)
            self.title = self.title and self.title.title()

    class _HelpFormatter(argparse.RawDescriptionHelpFormatter):
        def _format_usage(self, *args, **kwargs) -> str:
            return super()._format_usage(*args, **kwargs).replace("usage:", "Usage:", 1)

        def _format_action_invocation(self, action: argparse.Action) -> str:
            action.help = action.help and (action.help[0].upper() + action.help[1:])
            return super()._format_action_invocation(action)

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs, formatter_class=self._HelpFormatter)

    def add_argument_group(self, *args, **kwargs) -> _ArgumentGroup:
        group = self._ArgumentGroup(self, *args, **kwargs)
        self._action_groups.append(group)
        return group

Example output:

app@01d3adfb794b:/usr/local/src/app$ app --help
Usage: app [-h] [-v] [--debug] [command] ...

Options:
  -h, --help          Show this help message and exit
  -v, --version       Show program's version number and exit
  --debug, --verbose  Show more log

Commands:
    database          Manage the database
    user              Manage the users
    plan              Create a plan for the user
    info              Get users' info

Run 'app COMMAND --help' for more information on a command
app@01d3adfb794b:/usr/local/src/app$ app user --help
Usage: app user [-h] [-a] [-d] username [username ...]

Positional Arguments:
  username              The user's username. Multiple usernames could be specified

Options:
  -h, --help            Show this help message and exit
  -a, --add             Add a user
  -d, --delete          Delete a user
Saber Hayati
  • 688
  • 12
  • 14