clcommands.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  2. # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
  3. #
  4. # This file is part of logilab-common.
  5. #
  6. # logilab-common is free software: you can redistribute it and/or modify it under
  7. # the terms of the GNU Lesser General Public License as published by the Free
  8. # Software Foundation, either version 2.1 of the License, or (at your option) any
  9. # later version.
  10. #
  11. # logilab-common is distributed in the hope that it will be useful, but WITHOUT
  12. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  13. # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  14. # details.
  15. #
  16. # You should have received a copy of the GNU Lesser General Public License along
  17. # with logilab-common. If not, see <http://www.gnu.org/licenses/>.
  18. """Helper functions to support command line tools providing more than
  19. one command.
  20. e.g called as "tool command [options] args..." where <options> and <args> are
  21. command'specific
  22. """
  23. __docformat__ = "restructuredtext en"
  24. import sys
  25. import logging
  26. from os.path import basename
  27. from logilab.common.configuration import Configuration
  28. from logilab.common.logging_ext import init_log, get_threshold
  29. from logilab.common.deprecation import deprecated
  30. class BadCommandUsage(Exception):
  31. """Raised when an unknown command is used or when a command is not
  32. correctly used (bad options, too much / missing arguments...).
  33. Trigger display of command usage.
  34. """
  35. class CommandError(Exception):
  36. """Raised when a command can't be processed and we want to display it and
  37. exit, without traceback nor usage displayed.
  38. """
  39. # command line access point ####################################################
  40. class CommandLine(dict):
  41. """Usage:
  42. >>> LDI = cli.CommandLine('ldi', doc='Logilab debian installer',
  43. version=version, rcfile=RCFILE)
  44. >>> LDI.register(MyCommandClass)
  45. >>> LDI.register(MyOtherCommandClass)
  46. >>> LDI.run(sys.argv[1:])
  47. Arguments:
  48. * `pgm`, the program name, default to `basename(sys.argv[0])`
  49. * `doc`, a short description of the command line tool
  50. * `copyright`, additional doc string that will be appended to the generated
  51. doc
  52. * `version`, version number of string of the tool. If specified, global
  53. --version option will be available.
  54. * `rcfile`, path to a configuration file. If specified, global --C/--rc-file
  55. option will be available? self.rcfile = rcfile
  56. * `logger`, logger to propagate to commands, default to
  57. `logging.getLogger(self.pgm))`
  58. """
  59. def __init__(self, pgm=None, doc=None, copyright=None, version=None,
  60. rcfile=None, logthreshold=logging.ERROR,
  61. check_duplicated_command=True):
  62. if pgm is None:
  63. pgm = basename(sys.argv[0])
  64. self.pgm = pgm
  65. self.doc = doc
  66. self.copyright = copyright
  67. self.version = version
  68. self.rcfile = rcfile
  69. self.logger = None
  70. self.logthreshold = logthreshold
  71. self.check_duplicated_command = check_duplicated_command
  72. def register(self, cls, force=False):
  73. """register the given :class:`Command` subclass"""
  74. assert not self.check_duplicated_command or force or not cls.name in self, \
  75. 'a command %s is already defined' % cls.name
  76. self[cls.name] = cls
  77. return cls
  78. def run(self, args):
  79. """main command line access point:
  80. * init logging
  81. * handle global options (-h/--help, --version, -C/--rc-file)
  82. * check command
  83. * run command
  84. Terminate by :exc:`SystemExit`
  85. """
  86. init_log(debug=True, # so that we use StreamHandler
  87. logthreshold=self.logthreshold,
  88. logformat='%(levelname)s: %(message)s')
  89. try:
  90. arg = args.pop(0)
  91. except IndexError:
  92. self.usage_and_exit(1)
  93. if arg in ('-h', '--help'):
  94. self.usage_and_exit(0)
  95. if self.version is not None and arg in ('--version'):
  96. print self.version
  97. sys.exit(0)
  98. rcfile = self.rcfile
  99. if rcfile is not None and arg in ('-C', '--rc-file'):
  100. try:
  101. rcfile = args.pop(0)
  102. arg = args.pop(0)
  103. except IndexError:
  104. self.usage_and_exit(1)
  105. try:
  106. command = self.get_command(arg)
  107. except KeyError:
  108. print 'ERROR: no %s command' % arg
  109. print
  110. self.usage_and_exit(1)
  111. try:
  112. sys.exit(command.main_run(args, rcfile))
  113. except KeyboardInterrupt, exc:
  114. print 'Interrupted',
  115. if str(exc):
  116. print ': %s' % exc,
  117. print
  118. sys.exit(4)
  119. except BadCommandUsage, err:
  120. print 'ERROR:', err
  121. print
  122. print command.help()
  123. sys.exit(1)
  124. def create_logger(self, handler, logthreshold=None):
  125. logger = logging.Logger(self.pgm)
  126. logger.handlers = [handler]
  127. if logthreshold is None:
  128. logthreshold = get_threshold(self.logthreshold)
  129. logger.setLevel(logthreshold)
  130. return logger
  131. def get_command(self, cmd, logger=None):
  132. if logger is None:
  133. logger = self.logger
  134. if logger is None:
  135. logger = self.logger = logging.getLogger(self.pgm)
  136. logger.setLevel(get_threshold(self.logthreshold))
  137. return self[cmd](logger)
  138. def usage(self):
  139. """display usage for the main program (i.e. when no command supplied)
  140. and exit
  141. """
  142. print 'usage:', self.pgm,
  143. if self.rcfile:
  144. print '[--rc-file=<configuration file>]',
  145. print '<command> [options] <command argument>...'
  146. if self.doc:
  147. print '\n%s' % self.doc
  148. print '''
  149. Type "%(pgm)s <command> --help" for more information about a specific
  150. command. Available commands are :\n''' % self.__dict__
  151. max_len = max([len(cmd) for cmd in self])
  152. padding = ' ' * max_len
  153. for cmdname, cmd in sorted(self.items()):
  154. if not cmd.hidden:
  155. print ' ', (cmdname + padding)[:max_len], cmd.short_description()
  156. if self.rcfile:
  157. print '''
  158. Use --rc-file=<configuration file> / -C <configuration file> before the command
  159. to specify a configuration file. Default to %s.
  160. ''' % self.rcfile
  161. print '''%(pgm)s -h/--help
  162. display this usage information and exit''' % self.__dict__
  163. if self.version:
  164. print '''%(pgm)s -v/--version
  165. display version configuration and exit''' % self.__dict__
  166. if self.copyright:
  167. print '\n', self.copyright
  168. def usage_and_exit(self, status):
  169. self.usage()
  170. sys.exit(status)
  171. # base command classes #########################################################
  172. class Command(Configuration):
  173. """Base class for command line commands.
  174. Class attributes:
  175. * `name`, the name of the command
  176. * `min_args`, minimum number of arguments, None if unspecified
  177. * `max_args`, maximum number of arguments, None if unspecified
  178. * `arguments`, string describing arguments, used in command usage
  179. * `hidden`, boolean flag telling if the command should be hidden, e.g. does
  180. not appear in help's commands list
  181. * `options`, options list, as allowed by :mod:configuration
  182. """
  183. arguments = ''
  184. name = ''
  185. # hidden from help ?
  186. hidden = False
  187. # max/min args, None meaning unspecified
  188. min_args = None
  189. max_args = None
  190. @classmethod
  191. def description(cls):
  192. return cls.__doc__.replace(' ', '')
  193. @classmethod
  194. def short_description(cls):
  195. return cls.description().split('.')[0]
  196. def __init__(self, logger):
  197. usage = '%%prog %s %s\n\n%s' % (self.name, self.arguments,
  198. self.description())
  199. Configuration.__init__(self, usage=usage)
  200. self.logger = logger
  201. def check_args(self, args):
  202. """check command's arguments are provided"""
  203. if self.min_args is not None and len(args) < self.min_args:
  204. raise BadCommandUsage('missing argument')
  205. if self.max_args is not None and len(args) > self.max_args:
  206. raise BadCommandUsage('too many arguments')
  207. def main_run(self, args, rcfile=None):
  208. """Run the command and return status 0 if everything went fine.
  209. If :exc:`CommandError` is raised by the underlying command, simply log
  210. the error and return status 2.
  211. Any other exceptions, including :exc:`BadCommandUsage` will be
  212. propagated.
  213. """
  214. if rcfile:
  215. self.load_file_configuration(rcfile)
  216. args = self.load_command_line_configuration(args)
  217. try:
  218. self.check_args(args)
  219. self.run(args)
  220. except CommandError, err:
  221. self.logger.error(err)
  222. return 2
  223. return 0
  224. def run(self, args):
  225. """run the command with its specific arguments"""
  226. raise NotImplementedError()
  227. class ListCommandsCommand(Command):
  228. """list available commands, useful for bash completion."""
  229. name = 'listcommands'
  230. arguments = '[command]'
  231. hidden = True
  232. def run(self, args):
  233. """run the command with its specific arguments"""
  234. if args:
  235. command = args.pop()
  236. cmd = _COMMANDS[command]
  237. for optname, optdict in cmd.options:
  238. print '--help'
  239. print '--' + optname
  240. else:
  241. commands = sorted(_COMMANDS.keys())
  242. for command in commands:
  243. cmd = _COMMANDS[command]
  244. if not cmd.hidden:
  245. print command
  246. # deprecated stuff #############################################################
  247. _COMMANDS = CommandLine()
  248. DEFAULT_COPYRIGHT = '''\
  249. Copyright (c) 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  250. http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
  251. @deprecated('use cls.register(cli)')
  252. def register_commands(commands):
  253. """register existing commands"""
  254. for command_klass in commands:
  255. _COMMANDS.register(command_klass)
  256. @deprecated('use args.pop(0)')
  257. def main_run(args, doc=None, copyright=None, version=None):
  258. """command line tool: run command specified by argument list (without the
  259. program name). Raise SystemExit with status 0 if everything went fine.
  260. >>> main_run(sys.argv[1:])
  261. """
  262. _COMMANDS.doc = doc
  263. _COMMANDS.copyright = copyright
  264. _COMMANDS.version = version
  265. _COMMANDS.run(args)
  266. @deprecated('use args.pop(0)')
  267. def pop_arg(args_list, expected_size_after=None, msg="Missing argument"):
  268. """helper function to get and check command line arguments"""
  269. try:
  270. value = args_list.pop(0)
  271. except IndexError:
  272. raise BadCommandUsage(msg)
  273. if expected_size_after is not None and len(args_list) > expected_size_after:
  274. raise BadCommandUsage('too many arguments')
  275. return value