From 141c7de5d89f32b203438e1d6fca1efd817299f7 Mon Sep 17 00:00:00 2001 From: Michael Kuhn Date: Sun, 5 Nov 2023 23:32:09 +0100 Subject: Add command and package suggestions (#40895) * Add command suggestions This adds suggestions of similar commands in case users mistype a command. Before: ``` $ spack spack ==> Error: spack is not a recognized Spack command or extension command; check with `spack commands`. ``` After: ``` $ spack spack ==> Error: spack is not a recognized Spack command or extension command; check with `spack commands`. Did you mean one of the following commands? spec patch ``` * Add package name suggestions * Remove suggestion to run spack clean -m --- lib/spack/spack/cmd/dev_build.py | 5 +---- lib/spack/spack/cmd/edit.py | 5 +---- lib/spack/spack/extensions.py | 12 +++++++++++- lib/spack/spack/repo.py | 14 +++++++++++++- lib/spack/spack/test/cmd/dev_build.py | 11 +++++++++-- 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/lib/spack/spack/cmd/dev_build.py b/lib/spack/spack/cmd/dev_build.py index d8a7b447a2..90008c8b3e 100644 --- a/lib/spack/spack/cmd/dev_build.py +++ b/lib/spack/spack/cmd/dev_build.py @@ -99,10 +99,7 @@ def dev_build(self, args): spec = specs[0] if not spack.repo.PATH.exists(spec.name): - tty.die( - "No package for '{0}' was found.".format(spec.name), - " Use `spack create` to create a new package", - ) + raise spack.repo.UnknownPackageError(spec.name) if not spec.versions.concrete_range_as_version: tty.die( diff --git a/lib/spack/spack/cmd/edit.py b/lib/spack/spack/cmd/edit.py index 15aeea31b3..79f441a67a 100644 --- a/lib/spack/spack/cmd/edit.py +++ b/lib/spack/spack/cmd/edit.py @@ -43,10 +43,7 @@ def edit_package(name, repo_path, namespace): if not os.access(path, os.R_OK): tty.die("Insufficient permissions on '%s'!" % path) else: - tty.die( - "No package for '{0}' was found.".format(spec.name), - " Use `spack create` to create a new package", - ) + raise spack.repo.UnknownPackageError(spec.name) editor(path) diff --git a/lib/spack/spack/extensions.py b/lib/spack/spack/extensions.py index af900722cc..0ee01a22a1 100644 --- a/lib/spack/spack/extensions.py +++ b/lib/spack/spack/extensions.py @@ -5,6 +5,7 @@ """Service functions and classes to implement the hooks for Spack's command extensions. """ +import difflib import importlib import os import re @@ -176,10 +177,19 @@ class CommandNotFoundError(spack.error.SpackError): """ def __init__(self, cmd_name): - super().__init__( + msg = ( "{0} is not a recognized Spack command or extension command;" " check with `spack commands`.".format(cmd_name) ) + long_msg = None + + similar = difflib.get_close_matches(cmd_name, spack.cmd.all_commands()) + + if 1 <= len(similar) <= 5: + long_msg = "\nDid you mean one of the following commands?\n " + long_msg += "\n ".join(similar) + + super().__init__(msg, long_msg) class ExtensionNamingError(spack.error.SpackError): diff --git a/lib/spack/spack/repo.py b/lib/spack/spack/repo.py index a89b5dd407..5918454005 100644 --- a/lib/spack/spack/repo.py +++ b/lib/spack/spack/repo.py @@ -6,6 +6,7 @@ import abc import collections.abc import contextlib +import difflib import errno import functools import importlib @@ -1516,7 +1517,18 @@ class UnknownPackageError(UnknownEntityError): long_msg = "Did you mean to specify a filename with './{0}'?" long_msg = long_msg.format(name) else: - long_msg = "You may need to run 'spack clean -m'." + long_msg = "Use 'spack create' to create a new package." + + if not repo: + repo = spack.repo.PATH + + # We need to compare the base package name + pkg_name = name.rsplit(".", 1)[-1] + similar = difflib.get_close_matches(pkg_name, repo.all_package_names()) + + if 1 <= len(similar) <= 5: + long_msg += "\n\nDid you mean one of the following packages?\n " + long_msg += "\n ".join(similar) super().__init__(msg, long_msg) self.name = name diff --git a/lib/spack/spack/test/cmd/dev_build.py b/lib/spack/spack/test/cmd/dev_build.py index c5a7b5c3bb..85199eddd6 100644 --- a/lib/spack/spack/test/cmd/dev_build.py +++ b/lib/spack/spack/test/cmd/dev_build.py @@ -163,8 +163,15 @@ def test_dev_build_fails_multiple_specs(mock_packages): def test_dev_build_fails_nonexistent_package_name(mock_packages): - output = dev_build("no_such_package", fail_on_error=False) - assert "No package for 'no_such_package' was found" in output + output = "" + + try: + dev_build("no_such_package") + assert False, "no exception was raised!" + except spack.repo.UnknownPackageError as e: + output = e.message + + assert "Package 'no_such_package' not found" in output def test_dev_build_fails_no_version(mock_packages): -- cgit v1.2.3-60-g2f50