Created
February 20, 2015 09:18
-
-
Save polyvertex/f87967100496038ed6ba to your computer and use it in GitHub Desktop.
A small command line arguments parser in Python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def getopts(args=None, opts=[]): | |
""" | |
A small command line arguments parser. | |
args is the list of the arguments to parse. sys.argv[1:] by default. | |
opts is a list of definitions of the options to interpret. | |
Each element in the list is a string defining one option and its properties. | |
Definition format is as follows: | |
{name}[,name2[,...]][(properties)][={value_type}] | |
Where {name} is the name of the option that will directly be used by the | |
caller of the script like in "-name" or "--name". You can optionally declare | |
a comma-separated list of one or several additional names as long as none of | |
them collide with any other. | |
(properties) is an optional field to specify some properties to your option. | |
It is a string of one or several letters in no particular order and enclosed | |
by parentheses. Each letter represents a property. | |
Properties: | |
* "r": means this option is required and a ValueError exception will be | |
raised if it is missing | |
* "m": indicates an option that can be specified several times | |
{value_type} is a single letter that indicates two things: | |
* this option requires parameter | |
* the expected type of this parameter: | |
"s" for a string, "u" for a digit-only string casted to an integer, | |
"i" for an integer and "f" for a float. | |
Example: | |
getopts(opts=[ | |
"help,h", | |
"dir,d(r)=s", | |
"verbose,v(rm)", | |
"count,loops,c=u", | |
"my-long-option-name"]) | |
""" | |
if args == None: | |
args = sys.argv[1:] | |
opts_dict = {} # name: def_dict | |
opts_names = {} # secondary name: name | |
opts_required = [] # names of every options marked as 'required' | |
opts_out = {} | |
# interpret options definitions | |
for opt_def in opts: | |
# "help,h(rm)[={s|i|f}]" | |
m = re.match(r"""^ | |
(?P<names>[0-9a-zA-Z][0-9a-zA-Z\,\-]*) | |
(?:\( (?P<props>[rm]+) \))? | |
(?:\= (?P<valtype>[suif]) )? | |
$""", opt_def, re.VERBOSE | re.A) | |
if not m: | |
raise ValueError( | |
"getopts: malformed definition \"" + opt_def + "\"") | |
mdict = m.groupdict("") | |
# expand properties | |
mdict['names'] = " ".join(mdict['names'].split(",")).split() | |
mdict['required'] = "r" in mdict['props'] | |
mdict['multi'] = "m" in mdict['props'] | |
del mdict['props'] | |
name = mdict['names'][0] | |
# 'names' contains the primary name and optional secondary names | |
# ensure none of them has been used by an other option already | |
if not set(mdict['names']).isdisjoint(opts_names.keys()): | |
raise ValueError("getopts: option defined twice") | |
# keep option's definition in opts_dict pointed by its primary name | |
opts_dict[name] = mdict | |
# also, ensure we can easily reference an option by any of its names | |
for n in mdict['names']: | |
opts_names[n] = name | |
# if this option is required, keep track of that somewhere to be able | |
# to easily check later | |
if mdict['required']: | |
opts_required.append(name) | |
# populate the output dictionary so the caller doesn't have to check | |
# first if a key exists | |
if mdict['valtype']: | |
opts_out[name] = None | |
else: | |
opts_out[name] = 0 if mdict['multi'] else False | |
# read args | |
idx = 0 | |
while idx < len(args): | |
m = re.match( | |
r"^\-\-?([0-9a-zA-Z][0-9a-zA-Z\-]*)(?:\=(.+))?$", | |
args[idx], re.A) | |
if not m or m.group(1) not in opts_names: | |
idx += 1 | |
else: | |
opt_info = opts_dict[opts_names[m.group(1)]] | |
opt_name = opt_info['names'][0] | |
try: | |
opt_value = m.group(2) | |
except IndexError: | |
opt_value = None | |
args.pop(idx) | |
if opt_name in opts_required: | |
opts_required.remove(opt_name) | |
if not opt_info['valtype']: | |
if opt_value is not None: | |
raise ValueError("Option " + opt_name + | |
" has an unexpected parameter: " + opt_value) | |
if not opt_info['multi']: | |
opts_out[opt_name] = True | |
else: | |
try: | |
opts_out[opt_name] += 1 | |
except KeyError: | |
opts_out[opt_name] = 1 | |
else: | |
if opt_value is None: | |
try: | |
opt_value = args.pop(idx) | |
except IndexError: | |
raise ValueError( | |
"Option " + opt_name + " requires a parameter") | |
if opt_info['valtype'] == "s": | |
pass | |
elif opt_info['valtype'] == "u": | |
m = re.match(r"^[0-9]+$", opt_value, re.A) | |
if not m: | |
raise ValueError( | |
"Not an unsigned integer: " + opt_value) | |
opt_value = int(opt_value) | |
elif opt_info['valtype'] == "i": | |
try: | |
opt_value = int(opt_value) | |
except ValueError: | |
raise ValueError("Not an integer: " + opt_value) | |
elif opt_info['valtype'] == "f": | |
try: | |
opt_value = float(opt_value) | |
except ValueError: | |
raise ValueError("Not an integer: " + opt_value) | |
else: | |
raise Exception("getopt: new valtype '" + \ | |
opt_info['valtype'] + "' not handled here!") | |
if not opt_info['multi']: | |
opts_out[opt_name] = opt_value | |
else: | |
try: | |
opts_out[opt_name].append(opt_value) | |
except KeyError: | |
opts_out[opt_name] = [opt_value] | |
# ensure we've got every required parameters | |
if len(opts_required) > 0: | |
raise ValueError("Missing parameter(s): " + ", ".join(opts_required)) | |
return (opts_out, args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment