Package lamson :: Module args
[hide private]
[frames] | no frames]

Source Code for Module lamson.args

  1  """ 
  2  Implements Lamson's command line argument parsing system.  It is honestly 
  3  infinitely better than optparse or argparse so it will be released later as a 
  4  separate library under the BSD license. 
  5   
  6  It's used very easily.  First, you write a module that is like lamson.commands. 
  7  Each function name BLAH_command implements a sub-command.  Then you use 
  8  lamson.args.parse_and_run_command to parse the command line and run the function 
  9  that matches. 
 10   
 11  Note that the _command suffix is optional and configurable, but it is there 
 12  to disambiguate your commands so you can use Python reserved words and base 
 13  types as your command names.  Without it, you can do a list_command or a 
 14  for_command. 
 15   
 16  You command then specifies its keyword arguments to indicate what has 
 17  reasonable defaults and what is required.  Give a value to the option 
 18  to indicate its default, and give a None setting to indicate it is required. 
 19  A good way to read this is it is your commands "default settings" and None 
 20  says "this option has no default setting". 
 21   
 22  Here's an example from lamson: 
 23   
 24   
 25     def send_command(port=8825, host='127.0.0.1', debug=1, sender=None, to=None, 
 26                   subject=None, body=None, file=False): 
 27   
 28  You can see this has subject, body, sender, and to as required options (they  
 29  are None), and the rest have some default value. 
 30   
 31  With this the argument parser will parse the users given arguments, and then 
 32  call your command function with those as keyword arguments, but after it has 
 33  fixed them up with the defaults you gave.  In the event that a user does 
 34  not give a required option, lamson.args will abort with an error telling them. 
 35   
 36  Lamson's argument parser also accurately detects and parses integers, boolean 
 37  values, strings, emails, single word values, and can handle trailing arguments 
 38  after a -- argument.  This means you don't have to do conversion, it should be 
 39  the right type for what you expect. 
 40   
 41  Lamson.args does not care if you use one dash (-help), two dashes 
 42  (--help), three dashes (---help) or a billion.  In all honesty, who gives a 
 43  rat's ass, just get the user to type something like a dash followed by a word and 
 44  that's good enough. 
 45   
 46  If you just need argument parsing and no commands then you can just use 
 47  lamson.args.parse directly. 
 48   
 49  Finally, the help documentation for your commands is just the __doc__ 
 50  string of the function. 
 51  """ 
 52   
 53  import re 
 54  import sys 
 55  import inspect 
 56   
 57   
 58  S_IP_ADDRESS = lambda x, token: ['ip_address', token] 
 59  S_WORD = lambda x, token:  ['word', token] 
 60  S_EMAIL_ADDR = lambda x, token:  ['email', token] 
 61  S_OPTION = lambda x, token:  ['option', token.split("-")[-1]] 
 62  S_INT = lambda x, token:  ['int', int(token) ] 
 63  S_BOOL = lambda x, token:  ['bool', bool(token) ] 
 64  S_EMPTY = lambda x, token:  ['empty', ''] 
 65  S_STRING = lambda x, token:  ['string', token] 
 66  S_TRAILING = lambda x, token:  ['trailing', None] 
 67   
68 -class ArgumentError(Exception):
69 """Thrown when lamson.args encounters a command line format error.""" 70 pass 71 72 73 SCANNER = re.Scanner([ 74 (r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}", S_EMAIL_ADDR), 75 (r"[0-9]+\.[0-9]+\.[0-9]+\.[0-9]", S_IP_ADDRESS), 76 (r"-+[a-zA-Z0-9]+", S_OPTION), 77 (r"True", S_BOOL), 78 (r"[0-9]+", S_INT), 79 (r"--", S_TRAILING), 80 (r"[a-z\-]+", S_WORD), 81 (r"\s", S_EMPTY), 82 (r".+", S_STRING), 83 ]) 84 85
86 -def match(tokens, of_type = None):
87 """ 88 Responsible for taking a token off and processing it, ensuring it is 89 of the correct type. If of_type is None (the default) then you are 90 asking for anything. 91 """ 92 # check the type (first element) 93 if of_type: 94 if not peek(tokens, of_type): 95 raise ArgumentError("Expecting '%s' type of argument not %s in tokens: %r. Read the lamson help." % 96 (of_type, tokens[0][0], tokens)) 97 98 # take the token off the front 99 tok = tokens.pop(0) 100 101 # return the value (second element) 102 return tok[1]
103 104
105 -def peek(tokens, of_type):
106 """Returns true if the next token is of the type, false if not. It does not 107 modify the token stream the way match does.""" 108 if len(tokens) == 0: 109 raise ArgumentError("This command expected more on the command line. Not sure how you did that.") 110 111 return tokens[0][0] == of_type
112 113
114 -def trailing_production(data, tokens):
115 """Parsing production that handles trailing arguments after a -- is given.""" 116 data['TRAILING'] = [x[1] for x in tokens] 117 del tokens[:]
118
119 -def option_production(data, tokens):
120 """The Option production, used for -- or - options. The number of - aren't 121 important. It will handle either individual options, or paired options.""" 122 if peek(tokens, 'trailing'): 123 # this means the rest are trailing arguments, collect them up 124 match(tokens, 'trailing') 125 trailing_production(data, tokens) 126 else: 127 opt = match(tokens, 'option') 128 if not tokens: 129 # last one, it's just true 130 data[opt] = True 131 elif peek(tokens, 'option') or peek(tokens, 'trailing'): 132 # the next one is an option so just set this to true 133 data[opt] = True 134 else: 135 # this option is set to something else, so we'll grab that 136 data[opt] = match(tokens)
137 138
139 -def options_production(tokens):
140 """List of options, optionally after the command has already been taken off.""" 141 data = {} 142 while tokens: 143 option_production(data, tokens) 144 return data
145 146
147 -def command_production(tokens):
148 """The command production, just pulls off a word really.""" 149 return match(tokens, 'word')
150 151
152 -def tokenize(argv):
153 """Goes through the command line args and tokenizes each one, trying to match 154 something in the scanner. If any argument doesn't completely parse then it 155 is considered a 'string' and returned raw.""" 156 157 tokens = [] 158 for arg in argv: 159 toks, remainder = SCANNER.scan(arg) 160 if remainder or len(toks) > 1: 161 tokens.append(['string', arg]) 162 else: 163 tokens += toks 164 return tokens
165 166
167 -def parse(argv):
168 """ 169 Tokenizes and then parses the command line as wither a command style or 170 plain options style argument list. It determines this by simply if the 171 first argument is a 'word' then it's a command. If not then it still 172 returns the first element of the tuple as None. This means you can do: 173 174 command, options = args.parse(sys.argv[1:]) 175 176 and if command==None then it was an option style, if not then it's a command 177 to deal with. 178 """ 179 tokens = tokenize(argv) 180 if not tokens: 181 return None, {} 182 elif peek(tokens, "word"): 183 # this is a command style argument 184 return command_production(tokens), options_production(tokens) 185 else: 186 # options only style 187 return None, options_production(tokens)
188 189
190 -def determine_kwargs(function):
191 """ 192 Uses the inspect module to figure out what the keyword arguments 193 are and what they're defaults should be, then creates a dict with 194 that setup. The results of determine_kwargs() is typically handed 195 to ensure_defaults(). 196 """ 197 spec = inspect.getargspec(function) 198 keys = spec[0] 199 values = spec[-1] 200 result = {} 201 for i in range(0, len(keys)): 202 result[keys[i]] = values[i] 203 return result
204
205 -def ensure_defaults(options, reqs):
206 """ 207 Goes through the given options and the required ones and does the 208 work of making sure they match. It will raise an ArgumentError 209 if any option is required. It will also detect that required TRAILING 210 arguments were not given and raise a separate error for that. 211 """ 212 for key in reqs: 213 if reqs[key] == None: 214 # explicitly set to required 215 if key not in options: 216 if key == "TRAILING": 217 raise ArgumentError("Additional arguments required after a -- on the command line.") 218 else: 219 raise ArgumentError("Option -%s is required by this command." % key) 220 else: 221 if key not in options: 222 options[key] = reqs[key]
223
224 -def command_module(mod, command, options, ending="_command"):
225 """Takes a module, uses the command to run that function.""" 226 function = mod.__dict__[command+ending] 227 kwargs = determine_kwargs(function) 228 ensure_defaults(options, kwargs) 229 try: 230 return function(**options) 231 except TypeError, exc: 232 print "ERROR: ", exc
233 234
235 -def available_help(mod, ending="_command"):
236 """Returns the dochelp from all functions in this module that have _command 237 at the end.""" 238 help_text = [] 239 for key in mod.__dict__: 240 if key.endswith(ending): 241 name = key.split(ending)[0] 242 help_text.append(name + ":\n" + mod.__dict__[key].__doc__) 243 244 return help_text
245 246
247 -def help_for_command(mod, command, ending="_command"):
248 """ 249 Returns the help string for just this one command in the module. 250 If that command doesn't exist then it will return None so you can 251 print an error message. 252 """ 253 254 if command in available_commands(mod): 255 return mod.__dict__[command + ending].__doc__ 256 else: 257 return None
258 259
260 -def available_commands(mod, ending="_command"):
261 """Just returns the available commands, rather than the whole long list.""" 262 commands = [] 263 for key in mod.__dict__: 264 if key.endswith(ending): 265 commands.append(key.split(ending)[0]) 266 267 commands.sort() 268 return commands
269 270
271 -def invalid_command_message(mod, exit_on_error):
272 """Called when you give an invalid command to print what you can use.""" 273 print "You must specify a valid command. Try these: " 274 print ", ".join(available_commands(mod)) 275 276 if exit_on_error: 277 sys.exit(1) 278 else: 279 return False
280 281
282 -def parse_and_run_command(argv, mod, default_command=None, exit_on_error=True):
283 """ 284 A one-shot function that parses the args, and then runs the command 285 that the user specifies. If you set a default_command, and they don't 286 give one then it runs that command. If you don't specify a command, 287 and they fail to give one then it prints an error. 288 289 On this error (failure to give a command) it will call sys.exit(1). 290 Set exit_on_error=False if you don't want this behavior, like if 291 you're doing a unit test. 292 """ 293 try: 294 command, options = parse(argv) 295 296 if not command and default_command: 297 command = default_command 298 elif not command and not default_command: 299 return invalid_command_message(mod, exit_on_error) 300 301 if command not in available_commands(mod): 302 return invalid_command_message(mod, exit_on_error) 303 304 command_module(mod, command, options) 305 except ArgumentError, exc: 306 print "ERROR: ", exc 307 if exit_on_error: 308 sys.exit(1) 309 310 return True
311