Package logilab :: Package common :: Module clcommands

[frames] | no frames]

# Source Code for Module logilab.common.clcommands

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