Repository: spack/spack

Issue:
[feature request] better error messages when `spack load` has multiple matches
### Summary

Instead of " Use a more specific spec." the message to users should also mention how to load by hash (ie prepending `/`)

Even better, the message should also give some context about what the difference between matches is (or indicate how to find that). 

### Rationale

Consider

```
perlmutter:login35:> module load e4s
perlmutter:login35:~> spack load libxml2
==> Error: libxml2 matches multiple packages.
 Matching packages:
  4eodgpo libxml2@2.9.13%cce@13.0.2 arch=cray-sles15-zen3
  uaarf7u libxml2@2.9.13%gcc@11.2.0 arch=cray-sles15-zen3
  u2ai4xj libxml2@2.9.13%gcc@11.2.0 arch=cray-sles15-zen3
 Use a more specific spec.
perlmutter:login35:~> 
perlmutter:login35:~> spack load libxml2@2.9.13%gcc@11.2.0 arch=cray-sles15-zen3
==> Error: libxml2@2.9.13%gcc@11.2.0 arch=cray-sles15-zen3 matches multiple packages.
 Matching packages:
  uaarf7u libxml2@2.9.13%gcc@11.2.0 arch=cray-sles15-zen3
  u2ai4xj libxml2@2.9.13%gcc@11.2.0 arch=cray-sles15-zen3
 Use a more specific spec.
> spack load uaarf7u
==> Error: Spec 'uaarf7u' matches no installed packages.
```

1. One has to "just know" to add a `/` for `spack load /uaarf7u` to work
2. There is no obvious difference between "uaarf7u" and "u2ai4xj"



### Description

_No response_

### Additional information

```
> spack --version
0.18.0.dev0 (6040c82740449632aa1d6faab08f93f5e4c54615)
```

### General information

- [X] I have run `spack --version` and reported the version of Spack
- [X] I have searched the issues of this repo and believe this is not a duplicate


Discussion:
Personally I would love for `spack load` to behave like `module load` and use `--first` by default. By the way, if you didn't know, you can use `spack load --first` if you don't care about the difference.
Could we make the default behavior `spack load <spec>` emulate `spack load --first` and that way we don't need `--first` as an option. If one specifies an explicit spec then it should still work and only error out if the spec is not in the database or invalid hash has been specified. Right now one can't do `spack load cmake` or anything since spack install a bunch of specs at the same version and one has to be very explicit.


> Could we make the default behavior `spack load <spec>` emulate `spack load --first` and that way we don't need `--first` as an option. If one specifies an explicit spec then it should still work and only error out if the spec is not in the database or invalid hash has been specified. Right now one can't do `spack load cmake` or anything since spack install a bunch of specs at the same version and one has to be very explicit.

Ah.  After I already started adding unit tests for the requested fix.

Resolving patch:
diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py
--- a/lib/spack/spack/cmd/__init__.py
+++ b/lib/spack/spack/cmd/__init__.py
@@ -291,19 +291,24 @@ def disambiguate_spec_from_hashes(spec, hashes, local=False, installed=True, fir
     elif first:
         return matching_specs[0]
 
-    elif len(matching_specs) > 1:
-        format_string = "{name}{@version}{%compiler}{arch=architecture}"
-        args = ["%s matches multiple packages." % spec, "Matching packages:"]
-        args += [
-            colorize("  @K{%s} " % s.dag_hash(7)) + s.cformat(format_string)
-            for s in matching_specs
-        ]
-        args += ["Use a more specific spec."]
-        tty.die(*args)
+    ensure_single_spec_or_die(spec, matching_specs)
 
     return matching_specs[0]
 
 
+def ensure_single_spec_or_die(spec, matching_specs):
+    if len(matching_specs) <= 1:
+        return
+
+    format_string = "{name}{@version}{%compiler}{arch=architecture}"
+    args = ["%s matches multiple packages." % spec, "Matching packages:"]
+    args += [
+        colorize("  @K{%s} " % s.dag_hash(7)) + s.cformat(format_string) for s in matching_specs
+    ]
+    args += ["Use a more specific spec (e.g., prepend '/' to the hash)."]
+    tty.die(*args)
+
+
 def gray_hash(spec, length):
     if not length:
         # default to maximum hash length
diff --git a/lib/spack/spack/cmd/view.py b/lib/spack/spack/cmd/view.py
--- a/lib/spack/spack/cmd/view.py
+++ b/lib/spack/spack/cmd/view.py
@@ -35,7 +35,6 @@
 """
 import llnl.util.tty as tty
 from llnl.util.link_tree import MergeConflictError
-from llnl.util.tty.color import colorize
 
 import spack.cmd
 import spack.environment as ev
@@ -66,16 +65,7 @@ def squash(matching_specs):
             tty.die("Spec matches no installed packages.")
 
         matching_in_view = [ms for ms in matching_specs if ms in view_specs]
-
-        if len(matching_in_view) > 1:
-            spec_format = "{name}{@version}{%compiler}{arch=architecture}"
-            args = ["Spec matches multiple packages.", "Matching packages:"]
-            args += [
-                colorize("  @K{%s} " % s.dag_hash(7)) + s.cformat(spec_format)
-                for s in matching_in_view
-            ]
-            args += ["Use a more specific spec."]
-            tty.die(*args)
+        spack.cmd.ensure_single_spec_or_die("Spec", matching_in_view)
 
         return matching_in_view[0] if matching_in_view else matching_specs[0]
 


Test patch:
diff --git a/lib/spack/spack/test/cmd/load.py b/lib/spack/spack/test/cmd/load.py
--- a/lib/spack/spack/test/cmd/load.py
+++ b/lib/spack/spack/test/cmd/load.py
@@ -10,7 +10,7 @@
 
 import spack.spec
 import spack.user_environment as uenv
-from spack.main import SpackCommand, SpackCommandError
+from spack.main import SpackCommand
 
 load = SpackCommand("load")
 unload = SpackCommand("unload")
@@ -115,10 +115,12 @@ def test_load_first(install_mockery, mock_fetch, mock_archive, mock_packages):
     """Test with and without the --first option"""
     install("libelf@0.8.12")
     install("libelf@0.8.13")
-    # Now there are two versions of libelf
-    with pytest.raises(SpackCommandError):
-        # This should cause an error due to multiple versions
-        load("--sh", "libelf")
+
+    # Now there are two versions of libelf, which should cause an error
+    out = load("--sh", "libelf", fail_on_error=False)
+    assert "matches multiple packages" in out
+    assert "Use a more specific spec" in out
+
     # Using --first should avoid the error condition
     load("--sh", "--first", "libelf")