# Simplifying parsing command-line arguments

I'm currently working on a command-line "database migration" utility. Here are some of the requirements regarding reading the command-line arguments part:

• It should accept regular database connection parameters: host, database name, port, user and password (do not allow to specify password as a command-line argument, ask for password separately)
• It works in two modes - "validate" and "apply" - one of them has to be specified explicitly, both cannot be specified
• There should be an integer "batch size" argument specified
• There can be an optional range of integer ids specified via --id-begin and --id-end (the beginning of the range defaults to 1 if not specified)
• Instead of a range, there can also be a --name argument specified
• Both name and id range cannot be present
• There should be a "verbose" flag

My Solution

Here is what I came up with:

I'm using argparse module and extracted the parsing command-line arguments into a separate .parse_args() function which returns the parsed arguments (which is argparse.Namespace instance):

import argparse
import getpass

def parse_arguments(args):
"""Parses command-line arguments."""
parser = argparse.ArgumentParser()

# general database connection settings
parser.add_argument('-H', '--host', help="Database host IP address", required=True)
parser.add_argument('-D', '--database', help="Database name", required=True)
parser.add_argument('-P', '--port', help="Database port", required=True, type=int)

parser.add_argument('-u', '--user', help='Database user name', required=True)

# modes
parser.add_argument('--validate', help='Enables "validate only" mode', action='store_true')
parser.add_argument('--apply', help='Enables "apply" mode', action='store_true')

# extra settings
parser.add_argument('-b', '--batch-size', help='Batch size - determines how many cases are processed at a time',
type=int, default=5000)
parser.add_argument('--id-begin', help='ID for the processing to begin with', type=int, default=None)
parser.add_argument('--id-end', help='ID for the processing to end with', type=int, default=None)
parser.add_argument('--name', help='NAME value', type=str, default=None)

parser.add_argument('--verbose', help='Enables "verbose" reporting mode', action='store_true', default=False)

args = parser.parse_args(args)

# if both specified, id end cannot be smaller than id begin
if args.id_begin and args.id_end and args.id_end < args.id_begin:
raise parser.error("Invalid ID range.")

# at least one mode should be specified
if not args.validate and not args.apply:
raise parser.error('Please specify one of the "validate" or "apply" modes.')

# don't allow to use both "apply" and "validate" mode
if args.apply and args.validate:
raise parser.error('Cannot use both "validate" and "apply" modes.')

# don't allow to use both NAME and ID
if args.name and (args.id_begin or args.id_end):
raise parser.error("Cannot use both NAME and ID.")

# set the default id_begin value
if not args.name and not args.id_begin:
args.id_begin = 1

return args


The Questions

The code works and I've even covered it with tests (put them into a gist). But, I don't particularly like the way I check the requirements for name and id ranges, mutually exclusive "apply" and "validate" modes. It feels like I could have used more argparse specific features like custom types or actions. What do you think?

I would also appreciate other ideas about handling the password - using getpass() helps to not have the password shown on the terminal explicitly, but requires mocking it in tests.

Your function should have args=None as default. This way you can use it to parse the command-line arguments as well as pass in a list of arguments to parse.

For your mutual exclusive options you should use...mutual exclusive groups. However, this only works properly for your mode, because adding a group of not mutually exclusive options (your --id-begin and --id-end) to a mutually exclusive group seems to no be supported at the moment, even though there was some activity on that a few years back: http://bugs.python.org/issue10984

# modes

Also note that the default behavior of action='store_true' is to have default=False, so you can just do:
parser.add_argument('--verbose', help='Enables "verbose" reporting mode',