Repository: pallets/click (python)

Issue:
Use difflib for potential suggestions in cli error messages
Currently click only does startswith match at https://github.com/pallets/click/blob/6c454c88b76f7a0be396dba72513bd316cd141e6/click/parser.py#L328-L329 so a typo like using `--boun` suggests `--bount` but using `--bound` doesn't make any suggestions. Using [difflib.get_close_matches](https://docs.python.org/3/library/difflib.html#difflib.get_close_matches) could get better suggestions and nearest matches.

Sample implementation : https://github.com/tirkarthi/click/tree/difflib-suggestions

```python
import click

@click.option("--count")
@click.option("--bount")
@click.command()
def cli(count, bount):
    click.echo(f"count = {count}, bount = {bount}")

if __name__ == "__main__":
    cli()
```

```shell
python sample.py --boun 1 
Usage: sample.py [OPTIONS]
Try "sample.py --help" for help.

Error: no such option: --boun  Did you mean --bount?
```

```shell
python sample.py --bound 1
Usage: sample.py [OPTIONS]
Try "sample.py --help" for help.

Error: no such option: --bound
```

Using `--bound` with `difflib.get_close_matches`

```shell
python sample.py --bound 1
Usage: sample.py [OPTIONS]
Try "sample.py --help" for help.

Error: no such option: --bound  (Possible options: --bount, --count)
```

Using `--counter` with `difflib.get_close_matches`

```shell
python sample.py --counter 1
Usage: sample.py [OPTIONS]
Try "sample.py --help" for help.

Error: no such option: --counter  (Possible options: --count, --bount)
```

Pull request description:
use difflib for name suggestions in error messages
fixes #1446

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index d731f9f..cc6a744 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -84,6 +84,8 @@ Unreleased
     the ``-`` value match non-testing behavior. :issue:`1064`
 -   When creating a ``Group``, allow passing a list of commands instead
     of a dict. :issue:`1339`
+-   When a long option name isn't valid, use ``difflib`` to make better
+    suggestions for possible corrections. :issue:`1446`
 
 
 Version 7.1.2
diff --git a/src/click/parser.py b/src/click/parser.py
index b2ed7ad..f613947 100644
--- a/src/click/parser.py
+++ b/src/click/parser.py
@@ -329,7 +329,9 @@ class OptionParser:
 
     def _match_long_opt(self, opt, explicit_value, state):
         if opt not in self._long_opt:
-            possibilities = [word for word in self._long_opt if word.startswith(opt)]
+            from difflib import get_close_matches
+
+            possibilities = get_close_matches(opt, self._long_opt)
             raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx)
 
         option = self._long_opt[opt]


Test patch:
diff --git a/tests/test_options.py b/tests/test_options.py
index 9f88e34..c57525c 100644
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -86,6 +86,22 @@ def test_unknown_options(runner, unknown_flag):
     assert f"no such option: {unknown_flag}" in result.output
 
 
+@pytest.mark.parametrize(
+    ("value", "expect"),
+    [
+        ("--cat", "Did you mean --count?"),
+        ("--bounds", "(Possible options: --bound, --count)"),
+        ("--bount", "(Possible options: --bound, --count)"),
+    ],
+)
+def test_suggest_possible_options(runner, value, expect):
+    cli = click.Command(
+        "cli", params=[click.Option(["--bound"]), click.Option(["--count"])]
+    )
+    result = runner.invoke(cli, [value])
+    assert expect in result.output
+
+
 def test_multiple_required(runner):
     @click.command()
     @click.option("-m", "--message", multiple=True, required=True)