Optional Arguments
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
- left to one’s choice; not required or mandatory: Formal dress is optional.
- 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:
- If it is an optional argument it is optional. The script can be run without it.
- Define the usage, and description. Setting the script version is nice, but not necessary.
- Always provide a
--debug
option (or-x
if you’re unix-inclined). - Consider saving, and reloading the optional arguments.
- 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
.