65

I have this code which I am generally pleased with:

import argparse

servers = [ "ApaServer", "BananServer", "GulServer", "SolServer", "RymdServer",
            "SkeppServer", "HavsServer", "PiratServer", "SvartServer", "NattServer", "SovServer" ]

parser = argparse.ArgumentParser(description="A program to update components on servers.")
group = parser.add_mutually_exclusive_group()
group.add_argument('-l', '--list', dest="update", action='store_false', default=False, help='list server components')
group.add_argument('-u', '--updatepom', dest="update", action='store_true', help='update server components')
parser.add_argument('-o', '--only', nargs='*', choices=servers, help='Space separated list of case sensitive server names to process')
parser.add_argument('-s', '--skip', nargs='*', choices=servers, help='Space separated list of case sensitive server names to exclude from processing')
args = parser.parse_args()

I like that the choice=servers validates the server names in the input for me, so that I won´t have to. However, having so many valid choices makes the help output look terrible:

usage: args.py [-h] [-l | -u]
               [-o [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]]]
               [-s [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]]]

A program to update components on servers.

optional arguments:
  -h, --help            show this help message and exit
  -l, --list            list server components
  -u, --updatepom       update server components
  -o [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]], --only [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]]
                        Space separated list of case sensitive server names to
                        process
  -s [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]], --skip [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} [{ApaServer,BananServer,GulServer,SolServer,RymdServer,SkeppServer,HavsServer,PiratServer,SvartServer,NattServer,SovServer} ...]]
                        Space separated list of case sensitive server names to
                        exclude from processing

Which way would you recommend if I want:

  • Nice (mostly) auto-generated help output
  • Validation that the entries given to the -o or the -s options are in servers.

Bonus:

  • Would it be possible to have case insensitive string matching for the server names?

Append

I tried using michaelfilms suggestion where the -o -s options are removed from the above output and this part is added:

server optional arguments:
  Valid server names are: ApaServer, BananServer, GulServer, SolServer,
  RymdServer, SkeppServer, HavsServer, PiratServer, SvartServer,
  NattServer, SovServer

I think it looks pretty good, but I really need to provide help for -o and -s options as the user wouldn´t know about them otherwise. So I am not all the way there yet using this approach.

normanius
  • 8,629
  • 7
  • 53
  • 83
Deleted
  • 1,351
  • 3
  • 14
  • 18

7 Answers7

65

I am basically repeating what Ernest said - to avoid the ugly long list of choices, set metavar='' for the choice-based arguments (though it won't get rid of the whitespace between the argument and comma (e.g. -o , instead of -o,).

You can then describe the available choices in detail in the general description (RawDescriptionHelpFormatter is useful here if you want them listed with obvious indentation).

This code:

import argparse

servers = [ "ApaServer", "BananServer", "GulServer", "SolServer", "RymdServer",
            "SkeppServer", "HavsServer", "PiratServer", "SvartServer", "NattServer", "SovServer" ]

parser = argparse.ArgumentParser(description="A program to update components on servers.")
group = parser.add_mutually_exclusive_group()
group.add_argument('-l', '--list', dest="update", action='store_false', default=False, help='list server components')
group.add_argument('-u', '--updatepom', dest="update", action='store_true', help='update server components')
parser.add_argument('-o', '--only', choices=servers, help='Space separated list of case sensitive server names to process.  Allowed values are '+', '.join(servers), metavar='')
parser.add_argument('-s', '--skip', choices=servers, help='Space separated list of case sensitive server names to exclude from processing.  Allowed values are '+', '.join(servers), metavar='')
args = parser.parse_args()

produces the following help output:

usage: run.py [-h] [-l | -u] [-o] [-s]

A program to update components on servers.

optional arguments:
  -h, --help       show this help message and exit
  -l, --list       list server components
  -u, --updatepom  update server components
  -o , --only      Space separated list of case sensitive server names to
                   process. Allowed values are ApaServer, BananServer,
                   GulServer, SolServer, RymdServer, SkeppServer, HavsServer,
                   PiratServer, SvartServer, NattServer, SovServer
  -s , --skip      Space separated list of case sensitive server names to
                   exclude from processing. Allowed values are ApaServer,
                   BananServer, GulServer, SolServer, RymdServer, SkeppServer,
                   HavsServer, PiratServer, SvartServer, NattServer, SovServer

This is hopefully what original post was looking for.

Olivia Stork
  • 4,660
  • 5
  • 27
  • 40
user2463717
  • 667
  • 5
  • 2
  • Setting metavar to '' or None led to displaying no help at all.move_parser.add_argument('old_host', action='store', choices=distinct_host, help="Old host"+ ','.join(distinct_host), metavar= None) – RamPrasadBismil May 05 '15 at 18:19
  • 3
    `metavar=''` seems to cause an error now. Setting that to anything other than a blank string (like the argument name) fixes it. –  Jun 29 '16 at 19:08
  • trying to use an invalid choice still results in printing the set of all choices: `invalid choice: 500 (choose from 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)` – Michael Anderson Aug 20 '22 at 01:18
30

There is no need to subclass anything. Simply pass a metavar argument with the string you want to appear in the help message.

See the argparse documentation for details.

Ernest A
  • 7,526
  • 8
  • 34
  • 40
12

I have this same problem, and as a workaround, I used the epilog to describe each of the option choices. I had to use argparse.RawTextHelpFormatter, which lets you specify that the epilog is pre-formatted.

def choicesDescriptions():
   return """
Choices supports the following: 
   choice1         - the FIRST option
   choice2         - the SECOND option
   ...
   choiceN         - the Nth option
"""

def getChoices():
   return ["choice1", "choice2", ..., "choiceN"]

parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, epilog=choicesDescriptions())
parser.add_argument(
   'choices', 
   choices=getChoices(),
   help='Arg choice.  See the choices options below'
   )

args = parser.parse_args()
print(args)
Mike
  • 1,041
  • 9
  • 17
7

This will not help in situations where the options list is extremely long, as in the original question, but for those people who, like me, came across this question looking for a way to break moderately long options strings into two lines, here is my solution:

import argparse

class CustomFormatter(argparse.HelpFormatter):
    """Custom formatter for setting argparse formatter_class. Identical to the
    default formatter, except that very long option strings are split into two
    lines.
    """

    def _format_action_invocation(self, action):
        if not action.option_strings:
            metavar, = self._metavar_formatter(action, action.dest)(1)
            return metavar
        else:
            parts = []
            # if the Optional doesn't take a value, format is:
            #    -s, --long
            if action.nargs == 0:
                parts.extend(action.option_strings)
            # if the Optional takes a value, format is:
            #    -s ARGS, --long ARGS
            else:
                default = action.dest.upper()
                args_string = self._format_args(action, default)
                for option_string in action.option_strings:
                    parts.append('%s %s' % (option_string, args_string))
            if sum(len(s) for s in parts) < self._width - (len(parts) - 1) * 2:
                return ', '.join(parts)
            else:
                return ',\n  '.join(parts)

This code overrides the default argparse.HelpFormatter method _format_action_invocation, and is identical to the default implementation except in the last four lines.

Default formatter behavior:

parser = argparse.ArgumentParser(description="Argparse default formatter.")
parser.add_argument('-a', '--argument', help='not too long')
parser.add_argument('-u', '--ugly', choices=range(20), help='looks messy')
parser.print_help()

outputs:

usage: test.py [-h] [-a ARGUMENT]
               [-u {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}]

Argparse default formatter.

optional arguments:
  -h, --help            show this help message and exit
  -a ARGUMENT, --argument ARGUMENT
                        not too long
  -u {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}, --ugly {0,1,2,3,4,5,6,
7,8,9,10,11,12,13,14,15,16,17,18,19}
                        looks messy

Custom formatter behavior:

parser = argparse.ArgumentParser(description="Argparse custom formatter.",
                                 formatter_class=CustomFormatter)
parser.add_argument('-a', '--argument', help='not too long')
parser.add_argument('-l', '--less-ugly', choices=range(20), help='less messy')

outputs:

usage: test.py [-h] [-a ARGUMENT]
               [-l {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}]

Argparse custom formatter.

optional arguments:
  -h, --help            show this help message and exit
  -a ARGUMENT, --argument ARGUMENT
                        not too long
  -l {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19},
  --less-ugly {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}
                        less messy
vamin
  • 2,178
  • 6
  • 26
  • 30
4

To get the expected output, you'll need to subclass argparse.HelpFormatter and implement the formatting that you need. In particular, you'll need to implement your own _metavar_formatter method, which is the one in charge of joining all the choices into a single string separated by commas.

jcollado
  • 39,419
  • 8
  • 102
  • 133
3

Why not used parser.add_argument_group to create a group for your server based options and give that a description arg displays the list of possible choices? Then pass argparse.SUPPRESS into the help for each of the individual options. I believe that will give you what you want.

michaelfilms
  • 704
  • 3
  • 5
  • I tried it and it looks pretty good. But I am not all the way there yet using this approach. I´ve got to help the user by letting him know about the `-o` and `-s` options. – Deleted Feb 21 '12 at 09:26
1

http://bugs.python.org/issue16468 argparse only supports iterable choices is the bugs issue discussing the formatting of choices. The list of choices can appear in 3 places: the usage line, the help lines, and error messages.

All that parser cares about is doing an in (__contains__) test. But for formatting, long lists, unbounded 'lists' (e.g. integers >100), and other objects that aren't iterable give problems. metavar is the way that current users can get around most formatting issues (it may not help with the error messages). Look at the issue to get ideas of how to change your own version of argparse.

hpaulj
  • 221,503
  • 14
  • 230
  • 353