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
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
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
99 tok = tokens.pop(0)
100
101
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
115 """Parsing production that handles trailing arguments after a -- is given."""
116 data['TRAILING'] = [x[1] for x in tokens]
117 del tokens[:]
118
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
124 match(tokens, 'trailing')
125 trailing_production(data, tokens)
126 else:
127 opt = match(tokens, 'option')
128 if not tokens:
129
130 data[opt] = True
131 elif peek(tokens, 'option') or peek(tokens, 'trailing'):
132
133 data[opt] = True
134 else:
135
136 data[opt] = match(tokens)
137
138
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
148 """The command production, just pulls off a word really."""
149 return match(tokens, 'word')
150
151
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
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
184 return command_production(tokens), options_production(tokens)
185 else:
186
187 return None, options_production(tokens)
188
189
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
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
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
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
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
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
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
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
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