summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDoug Hellmann <doug.hellmann@dreamhost.com>2012-04-28 18:26:31 -0400
committerDoug Hellmann <doug.hellmann@dreamhost.com>2012-04-28 19:37:22 -0400
commitb17d091258ab2d637f6c2b2d79ecd16e3df771d2 (patch)
tree4a2661400d7cd0d14694fb321922f418394d2b4f
parentf63bb5962635b0639bed283c2eb787dd7f36f6c9 (diff)
downloadcliff-tablib-b17d091258ab2d637f6c2b2d79ecd16e3df771d2.tar.gz
first pass at interactive app
-rw-r--r--README.rst4
-rw-r--r--cliff/app.py46
-rw-r--r--cliff/help.py8
-rw-r--r--cliff/interactive.py81
-rw-r--r--demoapp/cliffdemo/main.py3
5 files changed, 129 insertions, 13 deletions
diff --git a/README.rst b/README.rst
index cb03c26..9fa104c 100644
--- a/README.rst
+++ b/README.rst
@@ -25,5 +25,7 @@ To do
to manage transactions
- switch setup/teardown functions in app to use some sort of context
manager?
-- interactive shell mode
- add options to csv formatter to control output (delimiter, etc.)
+- option to spit out bash completion data
+- move command execution into a separate class to be used by App and
+ InteractiveApp?
diff --git a/cliff/app.py b/cliff/app.py
index 344d3da..bd42477 100644
--- a/cliff/app.py
+++ b/cliff/app.py
@@ -8,6 +8,7 @@ import os
import sys
from .help import HelpAction, HelpCommand
+from .interactive import InteractiveApp
LOG = logging.getLogger(__name__)
@@ -39,6 +40,7 @@ class App(object):
self.stdout = stdout or sys.stdout
self.stderr = stderr or sys.stderr
self.parser = self.build_option_parser(description, version)
+ self.interactive_mode = False
def build_option_parser(self, description, version):
"""Return an argparse option parser for this application.
@@ -77,7 +79,7 @@ class App(object):
help="show this help message and exit",
)
parser.add_argument(
- '--debug',
+ '--debug',
default=False,
action='store_true',
help='show tracebacks on errors',
@@ -112,6 +114,28 @@ class App(object):
root_logger.addHandler(console)
return
+ def run(self, argv):
+ """Equivalent to the main program for the application.
+ """
+ self.options, remainder = self.parser.parse_known_args(argv)
+ self.configure_logging()
+ self.initialize_app()
+ result = 1
+ if not remainder:
+ result = self.interact()
+ else:
+ result = self.run_subcommand(remainder)
+ return result
+
+ # FIXME(dhellmann): Consider moving these command handling methods
+ # to a separate class.
+ def initialize_app(self):
+ """Hook for subclasses to take global initialization action
+ after the arguments are parsed but before a command is run.
+ Invoked only once, even in interactive mode.
+ """
+ return
+
def prepare_to_run_command(self, cmd):
"""Perform any preliminary work needed to run a command.
"""
@@ -122,20 +146,22 @@ class App(object):
"""
return
- def run(self, argv):
- """Equivalent to the main program for the application.
- """
- if not argv:
- argv = ['-h']
- self.options, remainder = self.parser.parse_known_args(argv)
- self.configure_logging()
- cmd_factory, cmd_name, sub_argv = self.command_manager.find_command(remainder)
+ def interact(self):
+ self.interactive_mode = True
+ interpreter = InteractiveApp(self, self.command_manager, self.stdin, self.stdout)
+ interpreter.prompt = '(%s) ' % self.NAME
+ interpreter.cmdloop()
+ return 0
+
+ def run_subcommand(self, argv):
+ cmd_factory, cmd_name, sub_argv = self.command_manager.find_command(argv)
cmd = cmd_factory(self, self.options)
err = None
result = 1
try:
self.prepare_to_run_command(cmd)
- cmd_parser = cmd.get_parser(' '.join([self.NAME, cmd_name]))
+ full_name = cmd_name if self.interactive_mode else ' '.join([self.NAME, cmd_name])
+ cmd_parser = cmd.get_parser(full_name)
parsed_args = cmd_parser.parse_args(sub_argv)
result = cmd.run(parsed_args)
except Exception as err:
diff --git a/cliff/help.py b/cliff/help.py
index 905f968..391d8cc 100644
--- a/cliff/help.py
+++ b/cliff/help.py
@@ -38,9 +38,13 @@ class HelpCommand(Command):
def run(self, parsed_args):
if parsed_args.cmd:
- cmd_factory, name, search_args = self.app.command_manager.find_command(parsed_args.cmd)
+ cmd_factory, cmd_name, search_args = self.app.command_manager.find_command(parsed_args.cmd)
cmd = cmd_factory(self.app, search_args)
- cmd_parser = cmd.get_parser(' '.join([self.app.NAME, name]))
+ full_name = (cmd_name
+ if self.app.interactive_mode
+ else ' '.join([self.app.NAME, cmd_name])
+ )
+ cmd_parser = cmd.get_parser(full_name)
else:
cmd_parser = self.get_parser(' '.join([self.app.NAME, 'help']))
cmd_parser.parse_args(['--help'])
diff --git a/cliff/interactive.py b/cliff/interactive.py
new file mode 100644
index 0000000..33aefc6
--- /dev/null
+++ b/cliff/interactive.py
@@ -0,0 +1,81 @@
+"""Application base class.
+"""
+
+import itertools
+import logging
+import logging.handlers
+import shlex
+
+import cmd2
+
+LOG = logging.getLogger(__name__)
+
+
+class InteractiveApp(cmd2.Cmd):
+
+ use_rawinput = True
+ doc_header = "Shell commands (type help <topic>):"
+ app_cmd_header = "Application commands (type help <topic>):"
+
+ def __init__(self, parent_app, command_manager, stdin, stdout):
+ self.parent_app = parent_app
+ self.command_manager = command_manager
+ cmd2.Cmd.__init__(self, 'tab', stdin=stdin, stdout=stdout)
+
+ def default(self, line):
+ # Tie in the the default command processor to
+ # dispatch commands known to the command manager.
+ # We send the message through our parent app,
+ # since it already has the logic for executing
+ # the subcommand.
+ line_parts = shlex.split(line)
+ self.parent_app.run_subcommand(line_parts)
+
+ def completedefault(self, text, line, begidx, endidx):
+ """Tab-completion for commands known to the command manager.
+
+ Does not handle options on the commands.
+ """
+ if not text:
+ completions = sorted(n for n, v in self.command_manager)
+ else:
+ completions = sorted(n for n, v in self.command_manager
+ if n.startswith(text)
+ )
+ return completions
+
+ def help_help(self):
+ # Use the command manager to get instructions for "help"
+ self.default('help help')
+
+ def do_help(self, arg):
+ if arg:
+ # Check if the arg is a builtin command or something
+ # coming from the command manager
+ arg_parts = shlex.split(arg)
+ method_name = '_'.join(
+ itertools.chain(['do'],
+ itertools.takewhile(lambda x: not x.startswith('-'),
+ arg_parts)
+ )
+ )
+ # Have the command manager version of the help
+ # command produce the help text since cmd and
+ # cmd2 do not provide help for "help"
+ if hasattr(self, method_name):
+ return cmd2.Cmd.do_help(self, arg)
+ # Dispatch to the underlying help command,
+ # which knows how to provide help for extension
+ # commands.
+ self.default('help ' + arg)
+ else:
+ cmd2.Cmd.do_help(self, arg)
+ cmd_names = [n for n, v in self.command_manager]
+ self.print_topics(self.app_cmd_header, cmd_names, 15, 80)
+ return
+
+ def get_names(self):
+ return [n
+ for n in cmd2.Cmd.get_names(self)
+ if not n.startswith('do__')
+ ]
diff --git a/demoapp/cliffdemo/main.py b/demoapp/cliffdemo/main.py
index 9059227..c535419 100644
--- a/demoapp/cliffdemo/main.py
+++ b/demoapp/cliffdemo/main.py
@@ -16,6 +16,9 @@ class DemoApp(App):
command_manager=CommandManager('cliff.demo'),
)
+ def initialize_app(self):
+ self.log.debug('initialize_app')
+
def prepare_to_run_command(self, cmd):
self.log.debug('prepare_to_run_command %s', cmd.__class__.__name__)