Artifact [e639432390]
Not logged in

Artifact e63943239013094f323e1ccd064bd17058770b93:


#
# @file  command.py
# @brief Morg's command execution infrastructure.
#

#
# Copyright (C) 2014 The Morg Project Developers
#
# See wiki/copyright.wiki in the top-level directory of the Morg source
# distribution for the full list of authors who have contributed to this
# project.
#

#
# This file is part of Morg.
#
# Morg is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# Morg is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License
# along with Morg. If not, see <http://www.gnu.org/licenses/>.
#

#------------------------------- IMPORTS --------------------------------

# Morg
import morg_error

# Standard library
import cmd
import readline
import shlex
import logging
import sys

#---------------------------- MODULE LOGGER -----------------------------

logger = logging.getLogger(__name__)

#---------------------------- COMMANDS BASE -----------------------------

class base:
    '''Base class for Morg commands.

    This class acts as a base for commands provided by Morg. Using it in
    conjunction with Python's introspection capabilities allows us to
    automate initialization of the dispatch table and to ease end-user
    extension.

    All Morg commands should derive from this class and implement the
    <tt>__call__</tt> method, which will be passed a list of strings
    containing the command-line used to invoke that command. The first
    member of this list will be the command name itself; the remaining
    elements are the command-line arguments.

    To automate help processing, each command should be able to handle a
    <tt>--help</tt> option.

    '''
    def __call__(self, argv):
        '''Execute command.

        @param argv (string list) Arguments provided via command-line.

        The call method acts as the means to execute commands. In this
        base class, we simply raise an exception to indicate that a
        subclass has failed to implement the desired functionality.

        '''
        raise unimplemented_cmd(argv[0])

class unimplemented_cmd(morg_error.error_base):
    '''For reporting unimplemented commands.'''
    def __str__(self):
        return '{}: command not implemented'.format(self.message)

#--------------------------- DISPATCH TABLE -----------------------------

class dispatcher:
    '''Commands dispatching mechanism.

    This class implements a dispatch table that maps command names to
    their corresponding functions or function objects. When morglib is
    initialized, it automatically instantiates this class and makes the
    resulting instance available to clients as the
    <tt>morglib.interpreter</tt> variable. Additionally, morglib
    populates the dispatch table with the callables for all the built-in
    commands.

    Clients can extend the Morg interpreter by adding their own
    command-callable mappings to <tt>morglib.interpreter</tt>.

    '''
    def __init__(self):
        '''Construct an empty dispatch table.'''
        self._dispatch_table = {}

    def __getitem__(self, key):
        '''Return specified element of dispatch table.

        @param  key (string) The name of the command.
        @return Callable corresponding to command.

        '''
        if (isinstance(key, str)):
            if (key in self._dispatch_table):
                return self._dispatch_table[key]
            raise KeyError('no entry in dispatch table for key {}'.
                           format(key))
        raise TypeError('bad dispatch table key type {}'.
                        format(key.__class__))

    def __setitem__(self, key, val):
        '''Set specified element of dispatch table.

        @param key (string) The name of the command.
        @param val (callable) The command's callable.

        '''
        if (isinstance(key, str)):
            self._dispatch_table[key] = val
            return
        raise TypeError('bad dispatch table key type {}'.
                        format(key.__class__))

    def __iter__(self):
        '''Return an iterator over the keys.'''
        return self._dispatch_table.iterkeys()

    def iterkeys(self):
        '''Return an iterator over the keys.'''
        return self._dispatch_table.iterkeys()

    def iteritems(self):
        '''Return an iterator over the key-value pairs.'''
        return self._dispatch_table.iteritems()

    def execute(self, key, args):
        '''Execute specified command.

        @param key (string) Command name.
        @param args (string list) Command-line arguments.

        '''
        try:
            logger.debug('executing command: {}'.format(args))
            f = self[key]
            f(args)
        except KeyError:
            logger.debug('unable to find command {}'.format(key))
            potential_completions = filter(lambda k: k.startswith(key),
                                           self._dispatch_table)
            n = len(potential_completions)
            logger.debug('found {} potential completions'.format(n))
            if (n <= 0):
                raise unknown_cmd(key)
            elif (n == 1):
                k = potential_completions[0]
                f = self[k]
                logger.debug('{} is a unique prefix for {}'.format(key, k))
                args = [k] + args[1:]
                f(args)
            else:
                raise ambiguous_prefix(key, potential_completions)

class unknown_cmd(morg_error.error_base):
    '''For reporting unknown commands.'''
    def __str__(self):
        return '{}: no such command'.format(self.message)

class ambiguous_prefix(morg_error.error_base):
    '''For reporting ambiguous command prefixes.'''
    def __init__(self, prefix, completions):
        msg = ('ambiguous prefix: {}; can complete: {}'.
               format(prefix, ', '.join(completions)))
        morg_error.error_base.__init__(self, msg)

#-------------------------------- REPL ----------------------------------

class repl(cmd.Cmd):
    '''Interactive mode's main loop.

    This class implements a read-eval-print loop (i.e., a REPL) so that
    users can interact with Morg beyond the "one-liner" permitted by the
    invocation via command-line arguments.

    '''
    def __init__(self, interpreter):
        '''Construct repl object.

        @param interpreter (dispatcher) For executing Morg commands.

        When instantiating the <tt>repl</tt> class, you should supply it
        with the <tt>morglib.interpreter</tt> so that this object knows
        how to interpret and execute the commands it receives
        interactively.

        '''
        cmd.Cmd.__init__(self)
        self.prompt = 'morg> '
        self._interpreter = interpreter

        # Helper function object to call the _dispatch_cmd() method,
        # passing it the command name plus the input line.
        class _dispatch:
            def __init__(self, cmd_name, repl_obj):
                self._cmd  = cmd_name
                self._repl = repl_obj

            # This is the do_*() method for each different command. Base
            # class triggers it upon receiving a line of input. But it
            # strips out the command from the input line and passes only
            # arguments. The Morg interpreter, however, requires command
            # plus argument. That's why we had to hang on to the command
            # name. Here, we stitch the command name and arguments
            # together to restore the input line to its unmangled form.
            def __call__(self, line):
                self._repl._dispatch_cmd(self._cmd + ' ' + line)

        # Base class, viz., cmd.Cmd, requires a do_*() method for each
        # command to be able to handle completion, etc. Each of these
        # do_*() methods will be passed the input line stripped of the
        # command (leaving only the arguments). So we need above
        # _dispatch helper to retain command name and pass it to
        # _dispatch_cmd() for proper dispatch via the Morg interpreter.
        for kmd in self._interpreter:
            setattr(self.__class__, 'do_' + kmd, _dispatch(kmd, self))

        # Commence REPL
        self.cmdloop()

    def _dispatch_cmd(self, line):
        '''Dispatch interactive commands via Morg interpreter.

        @param line (string) The line typed in by the user.

        This is the dispatch function for Morg's interactive console. It
        simply splits the input line using shell-like syntax and then
        uses the morglib interpreter to execute the command.

        '''
        try:
            args = shlex.split(line)
            if (args):
                self._interpreter.execute(args[0], args)

        except morg_error.error_base, e:
            logger.error(e)
            sys.stderr.write('{}\n'.format(e))

    def default(self, line):
        '''How to deal with unrecognized input.

        @param line (string) The line typed in by the user.

        This method overrides the base class version so that the Morg
        interpreter can print an appropriate error message rather than
        having cmd.Cmd do it.

        '''
        self._dispatch_cmd(line)

    def emptyline(self):
        '''What to do when user hits ENTER without any command.

        Override base class version, which reruns previous command. We'd
        rather do nothing.

        '''
        pass

#------------------------------------------------------------------------

##############################################
# Editor config:                             #
##############################################
# Local Variables:                           #
# indent-tabs-mode: nil                      #
# py-indent-offset: 4                        #
# python-indent: 4                           #
# End:                                       #
##############################################
# vim: set expandtab shiftwidth=4 tabstop=4: #
##############################################