diff options
-rw-r--r--[-rwxr-xr-x] | cmd2/cmd2.py | 109 | ||||
-rw-r--r-- | cmd2/rl_utils.py | 32 |
2 files changed, 128 insertions, 13 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index a2d67def..a534f2a1 100755..100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -64,6 +64,9 @@ if rl_type == RlType.NONE: else: from .rl_utils import rl_force_redisplay, readline + # Used by rlcompleter in Python console loaded by py command + orig_rl_delims = readline.get_completer_delims() + if rl_type == RlType.PYREADLINE: # Save the original pyreadline display completion function since we need to override it and restore it @@ -79,6 +82,9 @@ else: import ctypes from .rl_utils import readline_lib + rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters") + orig_rl_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value + from .argparse_completer import AutoCompleter, ACArgumentParser # Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure @@ -486,7 +492,7 @@ class Cmd(cmd.Cmd): ############################################################################################################ # The following variables are used by tab-completion functions. They are reset each time complete() is run - # using set_completion_defaults() and it is up to completer functions to set them before returning results. + # in reset_completion_defaults() and it is up to completer functions to set them before returning results. ############################################################################################################ # If true and a single match is returned to complete(), then a space will be appended @@ -651,7 +657,7 @@ class Cmd(cmd.Cmd): # ----- Methods related to tab completion ----- - def set_completion_defaults(self): + def reset_completion_defaults(self): """ Resets tab completion settings Needs to be called each time readline runs tab completion @@ -1291,7 +1297,7 @@ class Cmd(cmd.Cmd): """ if state == 0 and rl_type != RlType.NONE: unclosed_quote = '' - self.set_completion_defaults() + self.reset_completion_defaults() # lstrip the original line orig_line = readline.get_line_buffer() @@ -2025,12 +2031,10 @@ class Cmd(cmd.Cmd): # Set GNU readline's rl_basic_quote_characters to NULL so it won't automatically add a closing quote # We don't need to worry about setting rl_completion_suppress_quote since we never declared # rl_completer_quote_characters. - basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters") - old_basic_quote_characters = ctypes.cast(basic_quote_characters, ctypes.c_void_p).value - basic_quote_characters.value = None + old_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value + rl_basic_quote_characters.value = None old_completer = readline.get_completer() - old_delims = readline.get_completer_delims() readline.set_completer(self.complete) # Break words on whitespace and quotes when tab completing @@ -2040,6 +2044,7 @@ class Cmd(cmd.Cmd): # If redirection is allowed, then break words on those characters too completer_delims += ''.join(constants.REDIRECTION_CHARS) + old_delims = readline.get_completer_delims() readline.set_completer_delims(completer_delims) # Enable tab completion @@ -2076,7 +2081,7 @@ class Cmd(cmd.Cmd): if rl_type == RlType.GNU: readline.set_completion_display_matches_hook(None) - basic_quote_characters.value = old_basic_quote_characters + rl_basic_quote_characters.value = old_basic_quotes elif rl_type == RlType.PYREADLINE: readline.rl.mode._display_completions = orig_pyreadline_display @@ -2503,7 +2508,6 @@ Usage: Usage: unalias [-a] name [name ...] index_dict = {1: self.shell_cmd_complete} return self.index_based_complete(text, line, begidx, endidx, index_dict, self.path_complete) - # noinspection PyBroadException def do_py(self, arg): """ Invoke python command, shell, or script @@ -2520,6 +2524,7 @@ Usage: Usage: unalias [-a] name [name ...] return self._in_py = True + # noinspection PyBroadException try: arg = arg.strip() @@ -2535,6 +2540,7 @@ Usage: Usage: unalias [-a] name [name ...] except IOError as e: self.perror(e) + # noinspection PyUnusedLocal def onecmd_plus_hooks(cmd_plus_args): """Run a cmd2.Cmd command from a Python script or the interactive Python console. @@ -2556,6 +2562,8 @@ Usage: Usage: unalias [-a] name [name ...] if arg: interp.runcode(arg) + + # If there are no args, then we will open an interactive Python console else: # noinspection PyShadowingBuiltins def quit(): @@ -2567,6 +2575,52 @@ Usage: Usage: unalias [-a] name [name ...] keepstate = None try: + if rl_type != RlType.NONE: + # Save cmd2 history + saved_cmd2_history = [] + for i in range(1, readline.get_current_history_length() + 1): + saved_cmd2_history.append(readline.get_history_item(i)) + + readline.clear_history() + + # Restore py's history + # noinspection PyAttributeOutsideInit + self.py_history = getattr(self, 'py_history', []) + for item in self.py_history: + readline.add_history(item) + + if self.use_rawinput and self.completekey: + # Set up tab completion for the Python console + # rlcompleter relies on the default settings of the Python readline module + if rl_type == RlType.GNU: + old_basic_quotes = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value + rl_basic_quote_characters.value = orig_rl_basic_quotes + + if 'gnureadline' in sys.modules: + # rlcompleter imports readline by name, so it won't use gnureadline + # Force rlcompleter to use gnureadline instead so it has our settings and history + saved_readline = None + if 'readline' in sys.modules: + saved_readline = sys.modules['readline'] + + sys.modules['readline'] = sys.modules['gnureadline'] + + old_delims = readline.get_completer_delims() + readline.set_completer_delims(orig_rl_delims) + + # rlcompleter will not need cmd2's custom display function + # This will be restored by cmd2 the next time complete() is called + if rl_type == RlType.GNU: + readline.set_completion_display_matches_hook(None) + elif rl_type == RlType.PYREADLINE: + readline.rl.mode._display_completions = self._display_matches_pyreadline + + # Load rlcompleter so it sets its completer function + old_completer = readline.get_completer() + import rlcompleter + import importlib + importlib.reload(rlcompleter) + cprt = 'Type "help", "copyright", "credits" or "license" for more information.' keepstate = Statekeeper(sys, ('stdin', 'stdout')) sys.stdout = self.stdout @@ -2577,8 +2631,39 @@ Usage: Usage: unalias [-a] name [name ...] docstr)) except EmbeddedConsoleExit: pass - if keepstate is not None: - keepstate.restore() + + finally: + if keepstate is not None: + keepstate.restore() + + if rl_type != RlType.NONE: + # Save py's history + # noinspection PyAttributeOutsideInit + self.py_history = [] + for i in range(1, readline.get_current_history_length() + 1): + self.py_history.append(readline.get_history_item(i)) + + readline.clear_history() + + # Restore cmd2's history + for item in saved_cmd2_history: + readline.add_history(item) + + if self.use_rawinput and self.completekey: + # Restore cmd2's tab completion settings + readline.set_completer(old_completer) + readline.set_completer_delims(old_delims) + + if rl_type == RlType.GNU: + rl_basic_quote_characters.value = old_basic_quotes + + if 'gnureadline' in sys.modules: + # Restore what the readline module pointed to + if saved_readline is None: + del(sys.modules['readline']) + else: + sys.modules['readline'] = saved_readline + except Exception: pass finally: @@ -3430,5 +3515,3 @@ class CmdResult(namedtuple_with_two_defaults('CmdResult', ['out', 'err', 'war']) def __bool__(self): """If err is an empty string, treat the result as a success; otherwise treat it as a failure.""" return not self.err - - diff --git a/cmd2/rl_utils.py b/cmd2/rl_utils.py index 8ef65d28..55ca4a12 100644 --- a/cmd2/rl_utils.py +++ b/cmd2/rl_utils.py @@ -33,6 +33,38 @@ rl_type = RlType.NONE if 'pyreadline' in sys.modules: rl_type = RlType.PYREADLINE + ############################################################################################################ + # pyreadline is incomplete in terms of the Python readline API. Add the missing functions we need. + ############################################################################################################ + # readline.redisplay() + try: + getattr(readline, 'redisplay') + except AttributeError: + # noinspection PyProtectedMember + readline.redisplay = readline.rl.mode._update_line + + # readline.remove_history_item() + try: + getattr(readline, 'remove_history_item') + except AttributeError: + # noinspection PyProtectedMember + def pyreadline_remove_history_item(pos: int) -> None: + """ + An implementation of remove_history_item() for pyreadline + :param pos: The 0-based position in history to remove + """ + # Save of the current location of the history cursor + saved_cursor = readline.rl.mode._history.history_cursor + + # Delete the history item + del(readline.rl.mode._history.history[pos]) + + # Update the cursor if needed + if saved_cursor > pos: + readline.rl.mode._history.history_cursor -= 1 + + readline.remove_history_item = pyreadline_remove_history_item + elif 'gnureadline' in sys.modules or 'readline' in sys.modules: # We don't support libedit if 'libedit' not in readline.__doc__: |