Repository: incubator-impala Updated Branches: refs/heads/master ed87c4060 -> 679ebc1ac
IMPALA-992: Rerun past queries from history in shell This patch adds a new command "rerun" and a shortcut "@" to impala-shell . Users can rerun a certain query by its index given by history command. A valid index is an integer in [1, history_length] or [-history_length, -1]. Negative values index history in reverse order. For example, "@1;" or "rerun 1;" reruns the first query shown in history and "@-1;" reruns the last query. The rerun command itself won't appear in history. The history index is 1-based and increasing. Old entries might be truncated when impala-shell starts, and the indexes will be realigned to 1, so the same index may refer to different commands among multiple impala-shell instances. Testing: A test case test_rerun is added to shell/test_shell_interactive.py Change-Id: Ifc28e8ce07845343267224c3b9ccb71b29a524d2 Reviewed-on: http://gerrit.cloudera.org:8080/7674 Reviewed-by: Sailesh Mukil <[email protected]> Tested-by: Impala Public Jenkins Project: http://git-wip-us.apache.org/repos/asf/incubator-impala/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-impala/commit/c871e007 Tree: http://git-wip-us.apache.org/repos/asf/incubator-impala/tree/c871e007 Diff: http://git-wip-us.apache.org/repos/asf/incubator-impala/diff/c871e007 Branch: refs/heads/master Commit: c871e007bed662732944fd5bb8d88e306ff4865b Parents: ed87c40 Author: Tianyi Wang <[email protected]> Authored: Mon Aug 14 19:03:09 2017 -0700 Committer: Impala Public Jenkins <[email protected]> Committed: Wed Aug 23 03:34:45 2017 +0000 ---------------------------------------------------------------------- shell/impala_shell.py | 49 +++++++++++++++++++++++++++--- tests/shell/test_shell_interactive.py | 36 ++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/c871e007/shell/impala_shell.py ---------------------------------------------------------------------- diff --git a/shell/impala_shell.py b/shell/impala_shell.py index c45f932..6d9df4f 100755 --- a/shell/impala_shell.py +++ b/shell/impala_shell.py @@ -46,6 +46,8 @@ from thrift.Thrift import TException VERSION_FORMAT = "Impala Shell v%(version)s (%(git_hash)s) built on %(build_date)s" VERSION_STRING = "build version not available" +READLINE_UNAVAILABLE_ERROR = "The readline module was either not found or disabled. " \ + "Command history will not be collected." # Tarball / packaging build makes impala_build_version available try: @@ -76,7 +78,7 @@ class ImpalaPrettyTable(prettytable.PrettyTable): value = unicode(value, self.encoding, "replace") return value -class ImpalaShell(cmd.Cmd): +class ImpalaShell(object, cmd.Cmd): """ Simple Impala Shell. Basic usage: type connect <host:port> to connect to an impalad @@ -1068,15 +1070,40 @@ class ImpalaShell(cmd.Cmd): def do_history(self, args): """Display command history""" - # Deal with readline peculiarity. When history does not exists, + # Deal with readline peculiarity. When history does not exist, # readline returns 1 as the history length and stores 'None' at index 0. if self.readline and self.readline.get_current_history_length() > 0: for index in xrange(1, self.readline.get_current_history_length() + 1): cmd = self.readline.get_history_item(index) print_to_stderr('[%d]: %s' % (index, cmd)) else: - print_to_stderr("The readline module was either not found or disabled. Command " - "history will not be collected.") + print_to_stderr(READLINE_UNAVAILABLE_ERROR) + + def do_rerun(self, args): + """Rerun a command with an command index in history + Example: @1; + """ + history_len = self.readline.get_current_history_length() + # Rerun command shouldn't appear in history + self.readline.remove_history_item(history_len - 1) + history_len -= 1 + if not self.readline: + print_to_stderr(READLINE_UNAVAILABLE_ERROR) + return CmdStatus.ERROR + try: + cmd_idx = int(args) + except ValueError: + print_to_stderr("Command index to be rerun must be an integer.") + return CmdStatus.ERROR + if not (0 < cmd_idx <= history_len or -history_len <= cmd_idx < 0): + print_to_stderr("Command index out of range. Valid range: [1, {0}] and [-{0}, -1]" + .format(history_len)) + return CmdStatus.ERROR + if cmd_idx < 0: + cmd_idx += history_len + 1 + cmd = self.readline.get_history_item(cmd_idx) + print_to_stderr("Rerunning " + cmd) + return self.onecmd(cmd.rstrip(";")) def do_tip(self, args): """Print a random tip""" @@ -1124,6 +1151,20 @@ class ImpalaShell(cmd.Cmd): # The history file is not writable, disable readline. self._disable_readline() + def parseline(self, line): + """Parse the line into a command name and a string containing + the arguments. Returns a tuple containing (command, args, line). + 'command' and 'args' may be None if the line couldn't be parsed. + 'line' in return tuple is the rewritten original line, with leading + and trailing space removed and special characters transformed into + their aliases. + """ + line = line.strip() + if line and line[0] == '@': + line = 'rerun ' + line[1:] + return super(ImpalaShell, self).parseline(line) + + def _replace_history_delimiters(self, src_delim, tgt_delim): """Replaces source_delim with target_delim for all items in history. http://git-wip-us.apache.org/repos/asf/incubator-impala/blob/c871e007/tests/shell/test_shell_interactive.py ---------------------------------------------------------------------- diff --git a/tests/shell/test_shell_interactive.py b/tests/shell/test_shell_interactive.py index 9222637..7b85a55 100755 --- a/tests/shell/test_shell_interactive.py +++ b/tests/shell/test_shell_interactive.py @@ -232,6 +232,42 @@ class TestImpalaShellInteractive(object): assert query in result.stderr, "'%s' not in '%s'" % (query, result.stderr) @pytest.mark.execute_serially + def test_rerun(self): + """Smoke test for the 'rerun' command""" + # Clear history first. + if os.path.exists(SHELL_HISTORY_FILE): + os.remove(SHELL_HISTORY_FILE) + assert not os.path.exists(SHELL_HISTORY_FILE) + child_proc = pexpect.spawn(SHELL_CMD) + child_proc.expect(":21000] >") + self._expect_with_cmd(child_proc, "@1", ("Command index out of range")) + self._expect_with_cmd(child_proc, "rerun -1", ("Command index out of range")) + self._expect_with_cmd(child_proc, "select 'first_command'", ("first_command")) + self._expect_with_cmd(child_proc, "rerun 1", ("first_command")) + self._expect_with_cmd(child_proc, "@ -1", ("first_command")) + self._expect_with_cmd(child_proc, "select 'second_command'", ("second_command")) + child_proc.sendline('history;') + child_proc.expect(":21000] >") + assert '[1]: select \'first_command\';' in child_proc.before; + assert '[2]: select \'second_command\';' in child_proc.before; + assert '[3]: history;' in child_proc.before; + # Rerunning command should not add an entry into history. + assert '[4]' not in child_proc.before; + self._expect_with_cmd(child_proc, "@0", ("Command index out of range")) + self._expect_with_cmd(child_proc, "rerun 4", ("Command index out of range")) + self._expect_with_cmd(child_proc, "@-4", ("Command index out of range")) + self._expect_with_cmd(child_proc, " @ 3 ", ("second_command")) + self._expect_with_cmd(child_proc, "@-3", ("first_command")) + self._expect_with_cmd(child_proc, "@", + ("Command index to be rerun must be an integer.")) + self._expect_with_cmd(child_proc, "@1foo", + ("Command index to be rerun must be an integer.")) + self._expect_with_cmd(child_proc, "@1 2", + ("Command index to be rerun must be an integer.")) + self._expect_with_cmd(child_proc, "rerun1", ("Syntax error")) + child_proc.sendline('quit;') + + @pytest.mark.execute_serially def test_tip(self): """Smoke test for the TIP command""" # Temporarily add impala_shell module to path to get at TIPS list for verification
