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-beginand--id-end(the beginning of the range defaults to1if not specified) - Instead of a range, there can also be a
--nameargument 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
args.password = getpass.getpass(prompt='Enter password: ')
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.