1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 """Helper functions to support command line tools providing more than
19 one command.
20
21 e.g called as "tool command [options] args..." where <options> and <args> are
22 command'specific
23 """
24
25 __docformat__ = "restructuredtext en"
26
27 import sys
28 import logging
29 from os.path import basename
30
31 from logilab.common.configuration import Configuration
32 from logilab.common.logging_ext import init_log, get_threshold
33 from logilab.common.deprecation import deprecated
37 """Raised when an unknown command is used or when a command is not
38 correctly used (bad options, too much / missing arguments...).
39
40 Trigger display of command usage.
41 """
42
44 """Raised when a command can't be processed and we want to display it and
45 exit, without traceback nor usage displayed.
46 """
47
52 """Usage:
53
54 >>> LDI = cli.CommandLine('ldi', doc='Logilab debian installer',
55 version=version, rcfile=RCFILE)
56 >>> LDI.register(MyCommandClass)
57 >>> LDI.register(MyOtherCommandClass)
58 >>> LDI.run(sys.argv[1:])
59
60 Arguments:
61
62 * `pgm`, the program name, default to `basename(sys.argv[0])`
63
64 * `doc`, a short description of the command line tool
65
66 * `copyright`, additional doc string that will be appended to the generated
67 doc
68
69 * `version`, version number of string of the tool. If specified, global
70 --version option will be available.
71
72 * `rcfile`, path to a configuration file. If specified, global --C/--rc-file
73 option will be available? self.rcfile = rcfile
74
75 * `logger`, logger to propagate to commands, default to
76 `logging.getLogger(self.pgm))`
77 """
78 - def __init__(self, pgm=None, doc=None, copyright=None, version=None,
79 rcfile=None, logthreshold=logging.ERROR,
80 check_duplicated_command=True):
81 if pgm is None:
82 pgm = basename(sys.argv[0])
83 self.pgm = pgm
84 self.doc = doc
85 self.copyright = copyright
86 self.version = version
87 self.rcfile = rcfile
88 self.logger = None
89 self.logthreshold = logthreshold
90 self.check_duplicated_command = check_duplicated_command
91
93 """register the given :class:`Command` subclass"""
94 assert not self.check_duplicated_command or force or not cls.name in self, \
95 'a command %s is already defined' % cls.name
96 self[cls.name] = cls
97
98 - def run(self, args):
99 """main command line access point:
100 * init logging
101 * handle global options (-h/--help, --version, -C/--rc-file)
102 * check command
103 * run command
104
105 Terminate by :exc:`SystemExit`
106 """
107 init_log(debug=True,
108 logthreshold=self.logthreshold,
109 logformat='%(levelname)s: %(message)s')
110 try:
111 arg = args.pop(0)
112 except IndexError:
113 self.usage_and_exit(1)
114 if arg in ('-h', '--help'):
115 self.usage_and_exit(0)
116 if self.version is not None and arg in ('--version'):
117 print self.version
118 sys.exit(0)
119 rcfile = self.rcfile
120 if rcfile is not None and arg in ('-C', '--rc-file'):
121 try:
122 rcfile = args.pop(0)
123 except IndexError:
124 self.usage_and_exit(1)
125 try:
126 command = self.get_command(arg)
127 except KeyError:
128 print 'ERROR: no %s command' % arg
129 print
130 self.usage_and_exit(1)
131 try:
132 sys.exit(command.main_run(args, rcfile))
133 except KeyboardInterrupt, exc:
134 print 'Interrupted',
135 if str(exc):
136 print ': %s' % exc,
137 print
138 sys.exit(4)
139 except BadCommandUsage, err:
140 print 'ERROR:', err
141 print
142 print command.help()
143 sys.exit(1)
144
146 logger = logging.Logger(self.pgm)
147 logger.handlers = [handler]
148 if logthreshold is None:
149 logthreshold = get_threshold(self.logthreshold)
150 logger.setLevel(logthreshold)
151 return logger
152
154 if logger is None:
155 logger = self.logger
156 if logger is None:
157 logger = self.logger = logging.getLogger(self.pgm)
158 logger.setLevel(get_threshold(self.logthreshold))
159 return self[cmd](logger)
160
162 """display usage for the main program (i.e. when no command supplied)
163 and exit
164 """
165 print 'usage:', self.pgm,
166 if self.rcfile:
167 print '[--rc-file=<configuration file>]',
168 print '<command> [options] <command argument>...'
169 if self.doc:
170 print '\n%s' % self.doc
171 print '''
172 Type "%(pgm)s <command> --help" for more information about a specific
173 command. Available commands are :\n''' % self.__dict__
174 max_len = max([len(cmd) for cmd in self])
175 padding = ' ' * max_len
176 for cmdname, cmd in sorted(self.items()):
177 if not cmd.hidden:
178 print ' ', (cmdname + padding)[:max_len], cmd.short_description()
179 if self.rcfile:
180 print '''
181 Use --rc-file=<configuration file> / -C <configuration file> before the command
182 to specify a configuration file. Default to %s.
183 ''' % self.rcfile
184 print '''%(pgm)s -h/--help
185 display this usage information and exit''' % self.__dict__
186 if self.version:
187 print '''%(pgm)s -v/--version
188 display version configuration and exit''' % self.__dict__
189 if self.copyright:
190 print '\n', self.copyright
191
195
196
197
198
199 -class Command(Configuration):
200 """Base class for command line commands.
201
202 Class attributes:
203
204 * `name`, the name of the command
205
206 * `min_args`, minimum number of arguments, None if unspecified
207
208 * `max_args`, maximum number of arguments, None if unspecified
209
210 * `arguments`, string describing arguments, used in command usage
211
212 * `hidden`, boolean flag telling if the command should be hidden, e.g. does
213 not appear in help's commands list
214
215 * `options`, options list, as allowed by :mod:configuration
216 """
217
218 arguments = ''
219 name = ''
220
221 hidden = False
222
223 min_args = None
224 max_args = None
225
226 @classmethod
228 return cls.__doc__.replace(' ', '')
229
230 @classmethod
233
239
246
247 - def main_run(self, args, rcfile=None):
248 """Run the command and return status 0 if everything went fine.
249
250 If :exc:`CommandError` is raised by the underlying command, simply log
251 the error and return status 2.
252
253 Any other exceptions, including :exc:`BadCommandUsage` will be
254 propagated.
255 """
256 if rcfile:
257 self.load_file_configuration(rcfile)
258 args = self.load_command_line_configuration(args)
259 try:
260 self.check_args(args)
261 self.run(args)
262 except CommandError, err:
263 self.logger.error(err)
264 return 2
265 return 0
266
267 - def run(self, args):
268 """run the command with its specific arguments"""
269 raise NotImplementedError()
270
273 """list available commands, useful for bash completion."""
274 name = 'listcommands'
275 arguments = '[command]'
276 hidden = True
277
278 - def run(self, args):
279 """run the command with its specific arguments"""
280 if args:
281 command = args.pop()
282 cmd = _COMMANDS[command]
283 for optname, optdict in cmd.options:
284 print '--help'
285 print '--' + optname
286 else:
287 commands = sorted(_COMMANDS.keys())
288 for command in commands:
289 cmd = _COMMANDS[command]
290 if not cmd.hidden:
291 print command
292
293
294
295
296 _COMMANDS = CommandLine()
297
298 DEFAULT_COPYRIGHT = '''\
299 Copyright (c) 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
300 http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
304 """register existing commands"""
305 for command_klass in commands:
306 _COMMANDS.register(command_klass)
307
308 @deprecated('use args.pop(0)')
309 -def main_run(args, doc=None, copyright=None, version=None):
310 """command line tool: run command specified by argument list (without the
311 program name). Raise SystemExit with status 0 if everything went fine.
312
313 >>> main_run(sys.argv[1:])
314 """
315 _COMMANDS.doc = doc
316 _COMMANDS.copyright = copyright
317 _COMMANDS.version = version
318 _COMMANDS.run(args)
319
320 @deprecated('use args.pop(0)')
321 -def pop_arg(args_list, expected_size_after=None, msg="Missing argument"):
322 """helper function to get and check command line arguments"""
323 try:
324 value = args_list.pop(0)
325 except IndexError:
326 raise BadCommandUsage(msg)
327 if expected_size_after is not None and len(args_list) > expected_size_after:
328 raise BadCommandUsage('too many arguments')
329 return value
330