diff --git a/HISTORY.rst b/HISTORY.rst index 082ebad..a1a62e1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,7 @@ Release History +++++ * Adds ability to declare that command groups, commands, and arguments are in a preview status and therefore might change or be removed. This is done by passing the kwarg `is_preview=True`. * Adds a generic `TagDecorator` class to `knack.util` that allows you to create your own colorized tags like `[Preview]` and `[Deprecated]`. +* When an incorrect command name is entered, Knack will now attempt to suggest the closest alternative. 0.6.1 +++++ diff --git a/knack/parser.py b/knack/parser.py index 6ad4d0a..02b8180 100644 --- a/knack/parser.py +++ b/knack/parser.py @@ -3,6 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +from __future__ import print_function + import argparse from .deprecation import Deprecated @@ -255,3 +257,27 @@ def parse_args(self, args=None, namespace=None): """ self._expand_prefixed_files(args) return super(CLICommandParser, self).parse_args(args) + + def _check_value(self, action, value): + # Override to customize the error message when a argument is not among the available choices + # converted value must be one of the choices (if specified) + import difflib + import sys + + if action.choices is not None and value not in action.choices: + # parser has no `command_source`, value is part of command itself + error_msg = "{prog}: '{value}' is not in the '{prog}' command group. See '{prog} --help'.".format( + prog=self.prog, value=value) + logger.error(error_msg) + candidates = difflib.get_close_matches(value, action.choices, cutoff=0.7) + if candidates: + print_args = { + 's': 's' if len(candidates) > 1 else '', + 'verb': 'are' if len(candidates) > 1 else 'is', + 'value': value + } + suggestion_msg = "\nThe most similar choice{s} to '{value}' {verb}:\n".format(**print_args) + suggestion_msg += '\n'.join(['\t' + candidate for candidate in candidates]) + print(suggestion_msg, file=sys.stderr) + + self.exit(2) diff --git a/tests/test_cli_scenarios.py b/tests/test_cli_scenarios.py index 782e40d..63ec93f 100644 --- a/tests/test_cli_scenarios.py +++ b/tests/test_cli_scenarios.py @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- import os +from collections import OrderedDict import unittest try: import mock @@ -11,7 +12,6 @@ from unittest import mock import mock -from collections import OrderedDict from six import StringIO from knack import CLI diff --git a/tests/test_command_with_configured_defaults.py b/tests/test_command_with_configured_defaults.py index 365addd..ac2cde5 100644 --- a/tests/test_command_with_configured_defaults.py +++ b/tests/test_command_with_configured_defaults.py @@ -5,17 +5,15 @@ from __future__ import print_function import os import logging +import sys import unittest try: import mock except ImportError: from unittest import mock -from six import StringIO -import sys from knack.arguments import ArgumentsContext -from knack.commands import CLICommandsLoader, CLICommand, CommandGroup -from knack.config import CLIConfig +from knack.commands import CLICommandsLoader, CommandGroup from tests.util import DummyCLI, redirect_io diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index 9e16d9d..fc2165b 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -28,6 +28,7 @@ def example_arg_handler(arg1, opt1, arg2=None, opt2=None, arg3=None, pass +# pylint: disable=line-too-long class TestCommandDeprecation(unittest.TestCase): def setUp(self): @@ -132,7 +133,8 @@ def test_deprecate_command_expired_execute(self): with self.assertRaises(SystemExit): self.cli_ctx.invoke('cmd5 -h'.split()) actual = self.io.getvalue() - self.assertTrue(u'invalid choice' in actual and u'cmd5' in actual) + expected = """The most similar choices to 'cmd5'""" + self.assertIn(expected, actual) class TestCommandGroupDeprecation(unittest.TestCase): @@ -231,7 +233,8 @@ def test_deprecate_command_group_expired(self): with self.assertRaises(SystemExit): self.cli_ctx.invoke('group5 -h'.split()) actual = self.io.getvalue() - self.assertTrue(u'invalid choice' in actual and u'group5' in actual) + expected = """The most similar choices to 'group5'""" + self.assertIn(expected, actual) @redirect_io def test_deprecate_command_implicitly(self): diff --git a/tests/test_introspection.py b/tests/test_introspection.py index c296455..078a687 100644 --- a/tests/test_introspection.py +++ b/tests/test_introspection.py @@ -3,12 +3,7 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import os -import stat import unittest -import tempfile -import mock -from six.moves import configparser from knack.introspection import extract_full_summary_from_signature, option_descriptions, extract_args_from_signature