215 lines
8.2 KiB
Python
215 lines
8.2 KiB
Python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""Application base class.
|
|
"""
|
|
|
|
import itertools
|
|
import shlex
|
|
import sys
|
|
|
|
import autopage.argparse
|
|
import cmd2
|
|
|
|
|
|
class InteractiveApp(cmd2.Cmd):
|
|
"""Provides "interactive mode" features.
|
|
|
|
Refer to the cmd2_ and cmd_ documentation for details
|
|
about subclassing and configuring this class.
|
|
|
|
.. _cmd2: https://cmd2.readthedocs.io/en/latest/
|
|
.. _cmd: http://docs.python.org/library/cmd.html
|
|
|
|
:param parent_app: The calling application (expected to be derived
|
|
from :class:`cliff.main.App`).
|
|
:param command_manager: A :class:`cliff.commandmanager.CommandManager`
|
|
instance.
|
|
:param stdin: Standard input stream
|
|
:param stdout: Standard output stream
|
|
"""
|
|
|
|
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,
|
|
errexit=False):
|
|
self.parent_app = parent_app
|
|
if not hasattr(sys.stdin, 'isatty') or sys.stdin.isatty():
|
|
self.prompt = '(%s) ' % parent_app.NAME
|
|
else:
|
|
# batch/pipe mode
|
|
self.prompt = ''
|
|
self.command_manager = command_manager
|
|
self.errexit = errexit
|
|
cmd2.Cmd.__init__(self, 'tab', stdin=stdin, stdout=stdout)
|
|
|
|
def _split_line(self, line):
|
|
try:
|
|
return shlex.split(line.parsed.raw)
|
|
except AttributeError:
|
|
# cmd2 >= 0.9.1 gives us a Statement not a PyParsing parse
|
|
# result.
|
|
parts = shlex.split(line)
|
|
if getattr(line, 'command', None):
|
|
parts.insert(0, line.command)
|
|
return parts
|
|
|
|
def default(self, line):
|
|
# Tie in 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 = self._split_line(line)
|
|
ret = self.parent_app.run_subcommand(line_parts)
|
|
if self.errexit:
|
|
# Only provide this if errexit is enabled,
|
|
# otherise keep old behaviour
|
|
return ret
|
|
|
|
def completenames(self, text, line, begidx, endidx):
|
|
"""Tab-completion for command prefix without completer delimiter.
|
|
|
|
This method returns cmd style and cliff style commands matching
|
|
provided command prefix (text).
|
|
"""
|
|
completions = cmd2.Cmd.completenames(self, text, line, begidx, endidx)
|
|
completions += self._complete_prefix(text)
|
|
return completions
|
|
|
|
def completedefault(self, text, line, begidx, endidx):
|
|
"""Default tab-completion for command prefix with completer delimiter.
|
|
|
|
This method filters only cliff style commands matching provided
|
|
command prefix (line) as cmd2 style commands cannot contain spaces.
|
|
This method returns text + missing command part of matching commands.
|
|
This method does not handle options in cmd2/cliff style commands, you
|
|
must define complete_$method to handle them.
|
|
"""
|
|
return [x[begidx:] for x in self._complete_prefix(line)]
|
|
|
|
def _complete_prefix(self, prefix):
|
|
"""Returns cliff style commands with a specific prefix."""
|
|
if not prefix:
|
|
return [n for n, v in self.command_manager]
|
|
return [n for n, v in self.command_manager if n.startswith(prefix)]
|
|
|
|
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.
|
|
try:
|
|
# NOTE(coreycb): This try path can be removed once
|
|
# requirements.txt has cmd2 >= 0.7.3.
|
|
parsed = self.parsed
|
|
except AttributeError:
|
|
try:
|
|
parsed = self.parser_manager.parsed
|
|
except AttributeError:
|
|
# cmd2 >= 0.9.1 does not have a parser manager
|
|
parsed = lambda x: x # noqa
|
|
self.default(parsed('help ' + arg))
|
|
else:
|
|
stdout = self.stdout
|
|
try:
|
|
with autopage.argparse.help_pager(stdout) as paged_out:
|
|
self.stdout = paged_out
|
|
|
|
cmd2.Cmd.do_help(self, arg)
|
|
cmd_names = sorted([n for n, v in self.command_manager])
|
|
self.print_topics(self.app_cmd_header, cmd_names, 15, 80)
|
|
finally:
|
|
self.stdout = stdout
|
|
return
|
|
|
|
# Create exit alias to quit the interactive shell.
|
|
do_exit = cmd2.Cmd.do_quit
|
|
|
|
def get_names(self):
|
|
# Override the base class version to filter out
|
|
# things that look like they should be hidden
|
|
# from the user.
|
|
return [n
|
|
for n in cmd2.Cmd.get_names(self)
|
|
if not n.startswith('do__')
|
|
]
|
|
|
|
def precmd(self, statement):
|
|
"""Hook method executed just before the command is executed by
|
|
:meth:`~cmd2.Cmd.onecmd` and after adding it to history.
|
|
|
|
:param statement: subclass of str which also contains the parsed input
|
|
:return: a potentially modified version of the input Statement object
|
|
"""
|
|
# NOTE(mordred): The above docstring is copied in from cmd2 because
|
|
# current cmd2 has a docstring that sphinx finds if we don't override
|
|
# it, and it breaks sphinx.
|
|
|
|
# Pre-process the parsed command in case it looks like one of
|
|
# our subcommands, since cmd2 does not handle multi-part
|
|
# command names by default.
|
|
line_parts = self._split_line(statement)
|
|
try:
|
|
the_cmd = self.command_manager.find_command(line_parts)
|
|
cmd_factory, cmd_name, sub_argv = the_cmd
|
|
except ValueError:
|
|
# Not a plugin command
|
|
pass
|
|
else:
|
|
if hasattr(statement, 'parsed'):
|
|
# Older cmd2 uses PyParsing
|
|
statement.parsed.command = cmd_name
|
|
statement.parsed.args = ' '.join(sub_argv)
|
|
else:
|
|
# cmd2 >= 0.9.1 uses shlex and gives us a Statement.
|
|
statement = cmd2.Statement(
|
|
' '.join(sub_argv),
|
|
raw=statement.raw,
|
|
command=cmd_name,
|
|
arg_list=sub_argv,
|
|
multiline_command=statement.multiline_command,
|
|
terminator=statement.terminator,
|
|
suffix=statement.suffix,
|
|
pipe_to=statement.pipe_to,
|
|
output=statement.output,
|
|
output_to=statement.output_to,
|
|
)
|
|
return statement
|
|
|
|
def cmdloop(self):
|
|
# We don't want the cmd2 cmdloop() behaviour, just call the old one
|
|
# directly. In part this is because cmd2.cmdloop() doe not return
|
|
# anything useful and we want to have a useful exit code.
|
|
return self._cmdloop()
|