1# Licensed under the Apache License, Version 2.0 (the "License"); you may 2# not use this file except in compliance with the License. You may obtain 3# a copy of the License at 4# 5# http://www.apache.org/licenses/LICENSE-2.0 6# 7# Unless required by applicable law or agreed to in writing, software 8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10# License for the specific language governing permissions and limitations 11# under the License. 12 13"""Application base class. 14""" 15 16import itertools 17import shlex 18import sys 19 20import cmd2 21 22 23class InteractiveApp(cmd2.Cmd): 24 """Provides "interactive mode" features. 25 26 Refer to the cmd2_ and cmd_ documentation for details 27 about subclassing and configuring this class. 28 29 .. _cmd2: https://cmd2.readthedocs.io/en/latest/ 30 .. _cmd: http://docs.python.org/library/cmd.html 31 32 :param parent_app: The calling application (expected to be derived 33 from :class:`cliff.main.App`). 34 :param command_manager: A :class:`cliff.commandmanager.CommandManager` 35 instance. 36 :param stdin: Standard input stream 37 :param stdout: Standard output stream 38 """ 39 40 use_rawinput = True 41 doc_header = "Shell commands (type help <topic>):" 42 app_cmd_header = "Application commands (type help <topic>):" 43 44 def __init__(self, parent_app, command_manager, stdin, stdout, 45 errexit=False): 46 self.parent_app = parent_app 47 if not hasattr(sys.stdin, 'isatty') or sys.stdin.isatty(): 48 self.prompt = '(%s) ' % parent_app.NAME 49 else: 50 # batch/pipe mode 51 self.prompt = '' 52 self.command_manager = command_manager 53 self.errexit = errexit 54 cmd2.Cmd.__init__(self, 'tab', stdin=stdin, stdout=stdout) 55 56 def _split_line(self, line): 57 try: 58 return shlex.split(line.parsed.raw) 59 except AttributeError: 60 # cmd2 >= 0.9.1 gives us a Statement not a PyParsing parse 61 # result. 62 parts = shlex.split(line) 63 if getattr(line, 'command', None): 64 parts.insert(0, line.command) 65 return parts 66 67 def default(self, line): 68 # Tie in the default command processor to 69 # dispatch commands known to the command manager. 70 # We send the message through our parent app, 71 # since it already has the logic for executing 72 # the subcommand. 73 line_parts = self._split_line(line) 74 ret = self.parent_app.run_subcommand(line_parts) 75 if self.errexit: 76 # Only provide this if errexit is enabled, 77 # otherise keep old behaviour 78 return ret 79 80 def completenames(self, text, line, begidx, endidx): 81 """Tab-completion for command prefix without completer delimiter. 82 83 This method returns cmd style and cliff style commands matching 84 provided command prefix (text). 85 """ 86 completions = cmd2.Cmd.completenames(self, text, line, begidx, endidx) 87 completions += self._complete_prefix(text) 88 return completions 89 90 def completedefault(self, text, line, begidx, endidx): 91 """Default tab-completion for command prefix with completer delimiter. 92 93 This method filters only cliff style commands matching provided 94 command prefix (line) as cmd2 style commands cannot contain spaces. 95 This method returns text + missing command part of matching commands. 96 This method does not handle options in cmd2/cliff style commands, you 97 must define complete_$method to handle them. 98 """ 99 return [x[begidx:] for x in self._complete_prefix(line)] 100 101 def _complete_prefix(self, prefix): 102 """Returns cliff style commands with a specific prefix.""" 103 if not prefix: 104 return [n for n, v in self.command_manager] 105 return [n for n, v in self.command_manager if n.startswith(prefix)] 106 107 def help_help(self): 108 # Use the command manager to get instructions for "help" 109 self.default('help help') 110 111 def do_help(self, arg): 112 if arg: 113 # Check if the arg is a builtin command or something 114 # coming from the command manager 115 arg_parts = shlex.split(arg) 116 method_name = '_'.join( 117 itertools.chain( 118 ['do'], 119 itertools.takewhile(lambda x: not x.startswith('-'), 120 arg_parts) 121 ) 122 ) 123 # Have the command manager version of the help 124 # command produce the help text since cmd and 125 # cmd2 do not provide help for "help" 126 if hasattr(self, method_name): 127 return cmd2.Cmd.do_help(self, arg) 128 # Dispatch to the underlying help command, 129 # which knows how to provide help for extension 130 # commands. 131 try: 132 # NOTE(coreycb): This try path can be removed once 133 # requirements.txt has cmd2 >= 0.7.3. 134 parsed = self.parsed 135 except AttributeError: 136 try: 137 parsed = self.parser_manager.parsed 138 except AttributeError: 139 # cmd2 >= 0.9.1 does not have a parser manager 140 parsed = lambda x: x # noqa 141 self.default(parsed('help ' + arg)) 142 else: 143 cmd2.Cmd.do_help(self, arg) 144 cmd_names = sorted([n for n, v in self.command_manager]) 145 self.print_topics(self.app_cmd_header, cmd_names, 15, 80) 146 return 147 148 # Create exit alias to quit the interactive shell. 149 do_exit = cmd2.Cmd.do_quit 150 151 def get_names(self): 152 # Override the base class version to filter out 153 # things that look like they should be hidden 154 # from the user. 155 return [n 156 for n in cmd2.Cmd.get_names(self) 157 if not n.startswith('do__') 158 ] 159 160 def precmd(self, statement): 161 """Hook method executed just before the command is executed by 162 :meth:`~cmd2.Cmd.onecmd` and after adding it to history. 163 164 :param statement: subclass of str which also contains the parsed input 165 :return: a potentially modified version of the input Statement object 166 """ 167 # NOTE(mordred): The above docstring is copied in from cmd2 because 168 # current cmd2 has a docstring that sphinx finds if we don't override 169 # it, and it breaks sphinx. 170 171 # Pre-process the parsed command in case it looks like one of 172 # our subcommands, since cmd2 does not handle multi-part 173 # command names by default. 174 line_parts = self._split_line(statement) 175 try: 176 the_cmd = self.command_manager.find_command(line_parts) 177 cmd_factory, cmd_name, sub_argv = the_cmd 178 except ValueError: 179 # Not a plugin command 180 pass 181 else: 182 if hasattr(statement, 'parsed'): 183 # Older cmd2 uses PyParsing 184 statement.parsed.command = cmd_name 185 statement.parsed.args = ' '.join(sub_argv) 186 else: 187 # cmd2 >= 0.9.1 uses shlex and gives us a Statement. 188 statement = cmd2.Statement( 189 ' '.join(sub_argv), 190 raw=statement.raw, 191 command=cmd_name, 192 arg_list=sub_argv, 193 multiline_command=statement.multiline_command, 194 terminator=statement.terminator, 195 suffix=statement.suffix, 196 pipe_to=statement.pipe_to, 197 output=statement.output, 198 output_to=statement.output_to, 199 ) 200 return statement 201 202 def cmdloop(self): 203 # We don't want the cmd2 cmdloop() behaviour, just call the old one 204 # directly. In part this is because cmd2.cmdloop() doe not return 205 # anything useful and we want to have a useful exit code. 206 return self._cmdloop() 207