diff --git a/README.rst b/README.rst index cb03c26d..9fa104c6 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 344d3da0..bd42477a 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 905f968b..391d8cc1 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 00000000..33aefc6d --- /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 ):" + app_cmd_header = "Application commands (type help ):" + + 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 90592277..c535419a 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__)