11import sys
22
3+ import difflib
4+
35from py42 .exceptions import Py42HTTPError , Py42ForbiddenError
46
57from code42cli .compat import str
68from code42cli .errors import Code42CLIError
79from code42cli .parser import ArgumentParserError , CommandParser
810from code42cli .logger import get_main_cli_logger
911
12+ _DIFFLIB_CUT_OFF = 0.7
13+
1014
1115class CommandInvoker (object ):
16+
17+ _COMMAND_KEYWORDS = {}
18+ _COMMAND_ARG_KEYWORDS = {}
19+
1220 def __init__ (self , top_command , cmd_parser = None ):
1321 self ._top_command = top_command
1422 self ._cmd_parser = cmd_parser or CommandParser ()
@@ -72,6 +80,7 @@ def _load_subcommands(self, path, node):
7280 for command in node .subcommands :
7381 new_key = u"{} {}" .format (path , command .name ).strip ()
7482 self ._commands [new_key ] = command
83+ self ._set_command_keywords (new_key )
7584
7685 def _try_run_command (self , command , path_parts , input_args ):
7786 """Runs a command called using `path_parts` by parsing
@@ -82,9 +91,65 @@ def _try_run_command(self, command, path_parts, input_args):
8291 parser = self ._cmd_parser .prepare_cli_help (command )
8392 else :
8493 parser = self ._cmd_parser .prepare_command (command , path_parts )
94+ self ._set_argument_keywords (path_parts [0 ], command .get_arg_configs ())
8595 parsed_args = self ._cmd_parser .parse_args (input_args )
8696 parsed_args .func (parsed_args )
8797 except ArgumentParserError as err :
88- get_main_cli_logger ().log_error (err )
89- parser .print_help (sys .stderr )
98+ logger = get_main_cli_logger ()
99+ logger .print_and_log_error (u"{}" .format (err ))
100+ possible_correct_words = self ._find_incorrect_word_match (err , path_parts )
101+ if possible_correct_words :
102+ logger .print_and_log_error (u"Did you mean one of the following?" )
103+ for possible_correct_word in possible_correct_words :
104+ logger .print_info (u" {}" .format (possible_correct_word ))
105+
106+ else :
107+ parser .print_help (sys .stderr )
90108 sys .exit (2 )
109+
110+ @staticmethod
111+ def _get_arg_flags (arguments ):
112+ flag_names = []
113+ for arg in arguments .values ():
114+ arg_flags = [name for name in arg .settings ["options_list" ] if name .startswith ("-" )]
115+ flag_names .extend (arg_flags )
116+ return flag_names
117+
118+ def _set_argument_keywords (self , command_key , arguments ):
119+ self ._COMMAND_ARG_KEYWORDS [command_key ] = set ()
120+ self ._COMMAND_ARG_KEYWORDS [command_key ].update (CommandInvoker ._get_arg_flags (arguments ))
121+
122+ def _set_command_keywords (self , new_key ):
123+ """Creates a dictionary, with top level command as key and set of all its subcommands
124+ as values.
125+ """
126+ command_keys = new_key .split ()
127+ if len (command_keys ) == 1 :
128+ self ._COMMAND_KEYWORDS [command_keys [0 ]] = set ()
129+ else :
130+ self ._COMMAND_KEYWORDS [command_keys [0 ]].update (command_keys [1 :])
131+
132+ def _find_incorrect_word_match (self , error , path_parts ):
133+ possible_correct_words = []
134+
135+ try :
136+ # Here we assume the error string contains ":", for case where it doesn't we
137+ # assume the error is not due to misspelled word and we return error as is.
138+ error_detail , unmatched_words = str (error ).split (u":" )
139+ except ValueError :
140+ return possible_correct_words
141+
142+ if not unmatched_words or error_detail != u"unrecognized arguments" :
143+ return possible_correct_words
144+
145+ # Arg-parser sets the first/leftmost incorrect command keyword in the error message.
146+ unmatched_word = unmatched_words .split ()[0 ]
147+
148+ if not path_parts :
149+ available_values = self ._COMMAND_KEYWORDS .keys ()
150+ elif unmatched_word .strip ().startswith ('-' ):
151+ available_values = self ._COMMAND_ARG_KEYWORDS [path_parts [0 ]]
152+ else :
153+ available_values = self ._COMMAND_KEYWORDS [path_parts [0 ]]
154+
155+ return difflib .get_close_matches (unmatched_word , available_values , cutoff = _DIFFLIB_CUT_OFF )
0 commit comments