From 83e7c6025d1e4980eeb689a7dc9a3275601ada6c Mon Sep 17 00:00:00 2001 From: Shinae Woo Date: Sun, 15 Jul 2018 15:54:45 -0700 Subject: [PATCH 1/4] Remove CLI ambiguity if extact match exist In case of following set of commands \tcommand \tcommand_extends Before, $ command --> returns ambiguity Now, $ command --> running command --- bessctl/cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bessctl/cli.py b/bessctl/cli.py index b159bbaa0..39a483ef7 100644 --- a/bessctl/cli.py +++ b/bessctl/cli.py @@ -345,6 +345,10 @@ def find_cmd(self, line): return matched[0] elif len(matched) >= 2: + for m in matched: # return if exact match exists + if line.strip() == m[0]: + return m + self.err('Ambiguous command "%s". Candidates:' % line.strip()) for cmd, desc, _ in matched + matched_low: self.ferr.write(' %-50s%s\n' % (cmd, desc)) From d04f5f9261fe0454d8e58bc22132a20e248546cb Mon Sep 17 00:00:00 2001 From: Shinae Woo Date: Sun, 15 Jul 2018 16:08:09 -0700 Subject: [PATCH 2/4] Fix bug on restore shell prompt after command auto-completion - By flushing fout --- bessctl/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bessctl/cli.py b/bessctl/cli.py index 39a483ef7..cd160324d 100644 --- a/bessctl/cli.py +++ b/bessctl/cli.py @@ -307,6 +307,7 @@ def _do_complete(self, line, partial_word): self.fout.write('\n') self.fout.write(''.join(buf)) self.fout.write('%s%s' % (self.get_prompt(), line)) + self.fout.flush() return [] From de1da4ed9a3d70dfb063452bfd373318ed89e939 Mon Sep 17 00:00:00 2001 From: Shinae Woo Date: Mon, 16 Jul 2018 14:25:30 -0700 Subject: [PATCH 3/4] Cleanup code per comment --- bessctl/cli.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bessctl/cli.py b/bessctl/cli.py index cd160324d..352aef512 100644 --- a/bessctl/cli.py +++ b/bessctl/cli.py @@ -341,27 +341,28 @@ def get_prompt(self): def find_cmd(self, line): matched, matched_low = self.list_matched(line, ['full']) + line_stripped = line.strip() if len(matched) == 1: return matched[0] elif len(matched) >= 2: - for m in matched: # return if exact match exists - if line.strip() == m[0]: + for m in matched: # return if exact match exists + if line_stripped == m[0]: return m - self.err('Ambiguous command "%s". Candidates:' % line.strip()) + self.err('Ambiguous command "%s". Candidates:' % line_stripped) for cmd, desc, _ in matched + matched_low: self.ferr.write(' %-50s%s\n' % (cmd, desc)) elif len(matched) == 0: matched, matched_low = self.list_matched(line, ['partial']) if len(matched) > 0: - self.err('Incomplete command "%s". Candidates:' % line.strip()) + self.err('Incomplete command "%s". Candidates:' % line_stripped) for cmd, desc, _ in matched + matched_low: self.ferr.write(' %-50s%s\n' % (cmd, desc)) else: - self.err('Unknown command "%s".' % line.strip()) + self.err('Unknown command "%s".' % line_stripped) raise self.InvalidCommandError() From edeebfe227f5feb1996bc1dc167a5f38f5016d23 Mon Sep 17 00:00:00 2001 From: Shinae Woo Date: Wed, 18 Jul 2018 13:37:13 -0700 Subject: [PATCH 4/4] Fix CLI bug: for single token both exact and partial matching work While giving priority of exacting matching to remove ambiguous, still partial matching should be work. --- bessctl/cli.py | 64 ++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/bessctl/cli.py b/bessctl/cli.py index 352aef512..02a6f540a 100644 --- a/bessctl/cli.py +++ b/bessctl/cli.py @@ -30,6 +30,7 @@ import sys import os +from operator import itemgetter class ColorizedOutput(object): # for pretty printing @@ -144,10 +145,12 @@ def bind_var(self, var_type, line): # candidates is a list of suggested strings to be added as the last token. # syntax_token is where the user input is currently on, if any. # score is the number of matched keywords + # exact_score is the number of "exactly" matched keywords def match(self, syntax, line): candidates = [] remainder = line score = 0 + exact_score = 0 new_token = (line != '' and line[-1] == ' ') @@ -176,11 +179,13 @@ def match(self, syntax, line): candidates.append(syntax_token) if syntax_token[0] == '[': # skippable? - return 'full', candidates, syntax_token, score - return 'partial', candidates, syntax_token, score + return 'full', candidates, syntax_token, score, \ + exact_score + return 'partial', candidates, syntax_token, score, \ + exact_score return 'partial', candidates, \ - syntax_tokens[max(0, i - 1)], score + syntax_tokens[max(0, i - 1)], score, exact_score token, remainder = self.split_var(var_type, remainder) remainder = remainder.lstrip() @@ -193,9 +198,11 @@ def match(self, syntax, line): candidates = [syntax_token] else: if not syntax_token.startswith(token): - return 'nonmatch', [], '', score + return 'nonmatch', [], '', score, exact_score candidates = [syntax_token] score += 1 + if syntax_token.strip() == token: + exact_score += 1 else: if new_token: candidates = var_candidates @@ -207,35 +214,38 @@ def match(self, syntax, line): if remainder.strip() == '': if '...' in syntax_token: - return 'full', candidates, syntax_token, score + return 'full', candidates, syntax_token, score, exact_score if new_token: - return 'full', ['\n'], '', score - return 'full', candidates, syntax_token, score - return 'nonmatch', [], '', score + return 'full', ['\n'], '', score, exact_score + return 'full', candidates, syntax_token, score, exact_score + return 'nonmatch', [], '', score, exact_score - # filters is a list of 'full', 'partial', 'nonmatch' - def list_matched(self, line, filters): + # filter is one of 'full', 'partial', 'nonmatch' + def list_matched(self, line, filter): matched_list = [] for cmd in self.cmdlist: syntax = cmd[0] - match_type, _, _, score = self.match(syntax, line) + match_type, _, _, score, exact_score = self.match(syntax, line) - if match_type in filters: - matched_list.append((cmd, score)) + if match_type == filter: + matched_list.append((cmd, score, exact_score)) if len(matched_list) == 0: return [], [] max_score = max([x[1] for x in matched_list]) - ret = [] - ret_low = [] - for cmd, score in matched_list: - if score == max_score: - ret.append(cmd) - else: - ret_low.append(cmd) + ret = [m[0] for m in matched_list if m[1] == max_score] + ret_low = [m[0] for m in matched_list if m[1] != max_score] + + # Find exact matches without ambiguity + if filter == 'full' and len(ret) > 1: + full_matches = [m for m in matched_list if m[1] == max_score] + # sorted by exact score + full_matches.sort(key=itemgetter(2), reverse=True) + if full_matches[0][2] != full_matches[1][2]: + return [full_matches[0][0]], [] return ret, ret_low @@ -247,7 +257,7 @@ def _do_complete(self, line, partial_word): for cmd in self.cmdlist: syntax = cmd[0] match_type, sub_candidates, \ - syntax_token, score = self.match(syntax, line) + syntax_token, _, _ = self.match(syntax, line) if match_type in ['full', 'partial']: possible_cmds.append((cmd, match_type, syntax_token)) @@ -340,25 +350,23 @@ def get_prompt(self): return '> ' def find_cmd(self, line): - matched, matched_low = self.list_matched(line, ['full']) + # return commands being matched with every token + matched, matched_low = self.list_matched(line, 'full') line_stripped = line.strip() if len(matched) == 1: return matched[0] elif len(matched) >= 2: - for m in matched: # return if exact match exists - if line_stripped == m[0]: - return m - self.err('Ambiguous command "%s". Candidates:' % line_stripped) for cmd, desc, _ in matched + matched_low: self.ferr.write(' %-50s%s\n' % (cmd, desc)) elif len(matched) == 0: - matched, matched_low = self.list_matched(line, ['partial']) + matched, matched_low = self.list_matched(line, 'partial') if len(matched) > 0: - self.err('Incomplete command "%s". Candidates:' % line_stripped) + self.err('Incomplete command "%s". Candidates:' % + line_stripped) for cmd, desc, _ in matched + matched_low: self.ferr.write(' %-50s%s\n' % (cmd, desc)) else: