Repository: conan-io/conan

Issue:
[ux] Make the error on incorrect command input more helpful
Example
```
$ conan remoe
'remoe' is not a Conan command. See 'conan --help'

The most similar commands are
    remove
    remote

ERROR: Unknown command 'remoe'
```



Discussion:

Resolving patch:
diff --git a/conans/client/command.py b/conans/client/command.py
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -6,6 +6,7 @@
 import argparse
 import six
 from argparse import ArgumentError
+from difflib import get_close_matches
 
 from conans import __version__ as client_version
 from conans.client.cmd.uploader import UPLOAD_POLICY_FORCE, \
@@ -1831,6 +1832,25 @@ def _commands(self):
                     result[method_name] = method
         return result
 
+    def _print_similar(self, command):
+        """ looks for a similar commands and prints them if found
+        """
+        matches = get_close_matches(
+            word=command, possibilities=self._commands().keys(), n=5, cutoff=0.75)
+
+        if len(matches) == 0:
+            return
+
+        if len(matches) > 1:
+            self._out.writeln("The most similar commands are")
+        else:
+            self._out.writeln("The most similar command is")
+
+        for match in matches:
+            self._out.writeln("    %s" % match)
+
+        self._out.writeln("")
+
     def _warn_python2(self):
         if six.PY2:
             self._out.writeln("")
@@ -1854,10 +1874,19 @@ def run(self, *args):
                 if command in ["-v", "--version"]:
                     self._out.success("Conan version %s" % client_version)
                     return False
+
                 self._warn_python2()
-                self._show_help()
+
                 if command in ["-h", "--help"]:
+                    self._show_help()
                     return False
+
+                self._out.writeln(
+                    "'%s' is not a Conan command. See 'conan --help'." % command)
+                self._out.writeln("")
+
+                self._print_similar(command)
+
                 raise ConanException("Unknown command %s" % str(exc))
             except IndexError:  # No parameters
                 self._show_help()


Test patch:
diff --git a/conans/test/functional/command/help_test.py b/conans/test/functional/command/help_test.py
--- a/conans/test/functional/command/help_test.py
+++ b/conans/test/functional/command/help_test.py
@@ -1,5 +1,6 @@
 import sys
 import unittest
+import textwrap
 
 from six import StringIO
 
@@ -20,6 +21,52 @@ def help_test(self):
         client.run("some_unknown_command123", assert_error=True)
         self.assertIn("ERROR: Unknown command 'some_unknown_command123'", client.out)
 
+    def unknown_command_test(self):
+        client = TestClient()
+
+        client.run("some_unknown_command123", assert_error=True)
+        expected_output = textwrap.dedent(
+            """\
+                'some_unknown_command123' is not a Conan command. See 'conan --help'.
+
+                ERROR: Unknown command 'some_unknown_command123'
+            """)
+        self.assertIn(
+            expected_output, client.out)
+
+        # Check for a single suggestion
+        client.run("instal", assert_error=True)
+
+        expected_output = textwrap.dedent(
+            """\
+                'instal' is not a Conan command. See 'conan --help'.
+
+                The most similar command is
+                    install
+
+                ERROR: Unknown command 'instal'
+            """)
+        self.assertIn(
+            expected_output, client.out)
+
+        # Check for multiple suggestions
+        client.run("remoe", assert_error=True)
+        self.assertIn(
+            "", client.out)
+
+        expected_output = textwrap.dedent(
+            """\
+                'remoe' is not a Conan command. See 'conan --help'.
+
+                The most similar commands are
+                    remove
+                    remote
+
+                ERROR: Unknown command 'remoe'
+            """)
+        self.assertIn(
+            expected_output, client.out)
+
     def help_cmd_test(self):
         client = TestClient()
         try: