Optional Arguments

Feb 1, 2013 at 12:00

Example code can be found in this zip file

One thing that I always take issue with are optional arguments in scripts and programs:

optional   op·tion·al   [op-shuh-nl] adjective

  1. left to one’s choice; not required or mandatory: Formal dress is optional.
  2. leaving something to choice.

If you ask a developer in a conversation what does optional mean, they will give you the above definition.

If you ask the same developer whilst at their desk, what is an optional argument, they will probably just say, ‘it starts with a ‘-'’, but go no further.

Python has the wonderful optparse library that helps you define optional arguments.

The Rules

My rules for writing scripts and arguments usage are:

  1. If it is an optional argument it is optional. The script can be run without it.
  2. Define the usage, and description. Setting the script version is nice, but not necessary.
  3. Always provide a --debug option (or -x if you’re unix-inclined).
  4. Consider saving, and reloading the optional arguments.
  5. Consider saving, and reloading the non-optional arguments.

The only exception to optional arguments is where the script takes no arguments, but potentially does something destructive, and in which case, I add a --batch option, that is set to False by default. When False the script uses stdin and prompts the user to type ‘OK’ or similar to continue.

An example Python script that shows some of this:

#  Python main with args.

import sys
import os
import json
from optparse import OptionParser

script_root = None


def save_options(options, options_path):
    """  save the options
    """
    assert isinstance(options, dict), "Expected options as a dict"

    with open(options_path, 'w') as f:
        f.write(json.dumps(options, sort_keys=True, indent=2))


def load_options(options_path):
    """  load the options
    """
    assert os.path.exists(options_path), "Not found:  {0}".format(options_path)
    options = {}

    with open(options_path, 'r') as f:
        options = json.load(f)

    return options


def run(options, s):
    """  This method would normally be in its own file.
    The rest of this script is just boilerplate.
    """
    assert isinstance(options, dict), "Expected options as a dict"

    if options.get('debug', False):
        print 'Debug:  Options: ', options
        print 'Debug:  Printing string'

    #  print it!
    print s

###############################################################################
#  Main
###############################################################################


def main():
    """  Main!
    """

    global script_root, options_path
    script_root = os.path.dirname(os.path.abspath(sys.argv[0]))
    options_path = script_root + os.sep + 'options.json'

    usage = '%prog <string>'
    desc = 'echo a string to stdout, e.g. %prog hello'
    version = '1.0'

    parser = OptionParser(usage=usage, description=desc, version=version)

    parser.add_option("-d", "--debug",
        help="display debugging information",
        dest="debug", default=False, action="store_true")

    parser.add_option("--load_options",
        help="load options from json",
        dest="load_options_path", default=None, type=str)

    parser.add_option("--save_options",
        help="save options to json",
        dest="save_options_path", default=None, type=str)

    parser.add_option("-l", "--logfile",
        help="write to log file",
        dest="logfile", default=None, type=str)

    options, args = parser.parse_args()

    if len(args) != 1:
        parser.print_help()
        return 0

    #  Convert the options to a dictionary
    opts = options.__dict__

    #  Load options if asked.
    if options.load_options_path is not None:
        opts = load_options(options.load_options_path)

    #  Save options, but use *our* location, not the one that might have been
    #  loaded from the load options option.
    if options.save_options_path is not None:
        opts['save_options_path'] = options.save_options_path
        save_options(opts, options.save_options_path)

    #  Finally execute our function with our options
    run(opts, args[0])

    return 0

if __name__ == '__main__':
    sys.exit(main())

Further improvements are to serialize and deserialize from json and maintain the options as an object: using get on a dict and changing the argument names can lead to subtle, silent errors.

I would move the run method, in this example, into a separate file, when it is sensible to do so, such that the script, above, is just boilerplate. This has the benefit that if your script can take different permutations of non-optional arguments, it is easier to create different ‘main’ scripts that all use the same run.