Repository: spack/spack
Pull request #41455: New subcommand `spack config scopes [-hips]`
Author: login: greenc-FNAL
Labels: name: tests
description: General test capability(ies)
name: commands
name: shell-support
name: core
description: PR affects Spack core functionality
name: pipelines:urgent
description: Skip "deferred pipelines" check. Only use if rebasing on a tested develop commit is unfeasible
Created: 2023-12-06T14:23:27Z | Merged: 2025-05-28T23:15:37Z

List the scopes available in the current environment in descending priority order.

* With `-p|--path-scopes`, list only writable, file-based scopes (ignoring `command_line` and `_builtin`).
* With `-i|--included`, list only included scopes (specified in `include.yaml` or an environment `include:` clause).
* With `-s|--show-paths`, annotate scopes with their directory or file (with non-option argument `section`) locations when appropriate.
* Automatically takes account of `SPACK_DISABLE_LOCAL_CONFIG=true`

Useful for understanding the best scope in which to put a particular specific configuration item, or for debugging "missing" config.

### Examples

* MacOS:
  ```console
  $ spack arch
  darwin-sequoia-m1
  $ spack config scopes
  command_line user site system defaults defaults:darwin defaults:base _builtin
  ```

* Linux:
  ```console
  $ spack arch
  linux-almalinux9-cascadelake
  $ spack config scopes
  command_line user site system defaults defaults:linux defaults:base _builtin
  $ SPACK_DISABLE_LOCAL_CONFIG=1 spack config scopes
  command_line site defaults defaults:linux defaults:base _builtin
  $ SPACK_DISABLE_LOCAL_CONFIG=1 spack config --scope=site scopes --path-scopes
  site defaults defaults:linux defaults:base
  $ SPACK_DISABLE_LOCAL_CONFIG=1 spack config --scope=site scopes --included
  defaults:linux defaults:base
  $ SPACK_DISABLE_LOCAL_CONFIG=1 spack config --scope=site scopes --included --show-paths
  defaults:linux (/home/greenc/work/cet-is/sources/Spack/spack/run/etc/spack/defaults/linux/) defaults:base (/home/greenc/work/cet-is/sources/Spack/spack/run/etc/spack/defaults/base/)
  $ SPACK_DISABLE_LOCAL_CONFIG=1 spack config --scope=site scopes modules
  command_line site (/home/greenc/work/cet-is/sources/Spack/spack/run/etc/spack/) defaults (/home/greenc/work/cet-is/sources/Spack/spack/run/etc/spack/defaults/modules.yaml) defaults:linux (/home/greenc/work/cetis/sources/Spack/spack/run/etc/spack/defaults/linux/modules.yaml) defaults:base (/home/greenc/work/cet-is/sources/Spack/spack/run/etc/spack/defaults/base/) builtin
  ```


Diff:
diff --git a/lib/spack/spack/cmd/config.py b/lib/spack/spack/cmd/config.py
index 0b5d469459391f..9380968b197271 100644
--- a/lib/spack/spack/cmd/config.py
+++ b/lib/spack/spack/cmd/config.py
@@ -68,6 +68,35 @@ def setup_parser(subparser):
 
     sp.add_parser("list", help="list configuration sections")
 
+    scopes_parser = sp.add_parser(
+        "scopes", help="list defined scopes in descending order of precedence"
+    )
+    scopes_parser.add_argument(
+        "-i", "--included", action="store_true", default=False, help="list only included scopes"
+    )
+    scopes_parser.add_argument(
+        "-p",
+        "--path-scopes",
+        action="store_true",
+        default=False,
+        help="list only writable scopes with an associated path",
+    )
+    scopes_parser.add_argument(
+        "-s",
+        "--show-paths",
+        action="store_true",
+        default=False,
+        help="show associated paths for appropriate scopes",
+    )
+    scopes_parser.add_argument(
+        "section",
+        help="tailor scope path information to the specified section (implies -s|--show-paths)"
+        "\noptions: %(choices)s",
+        metavar="section",
+        nargs="?",
+        choices=spack.config.SECTION_SCHEMAS,
+    )
+
     add_parser = sp.add_parser("add", help="add configuration parameters")
     add_parser.add_argument(
         "path",
@@ -215,6 +244,34 @@ def config_list(args):
     print(" ".join(list(spack.config.SECTION_SCHEMAS)))
 
 
+def _config_scope_info_string(args, scope):
+    if (args.section or args.show_paths) and hasattr(scope, "path"):
+        section_path = scope.get_section_filename(args.section) if args.section else None
+        path = (
+            section_path
+            if section_path and os.path.exists(section_path)
+            else f"{scope.path}{os.sep}"
+        )
+        return f"{scope.name} ({path})"
+    else:
+        return scope.name
+
+
+def config_scopes(args):
+    """List configured scopes in descending order of precedence."""
+
+    scopes = (
+        spack.config.scopes().reversed_values()
+        if (args.included or not args.path_scopes)
+        else spack.config.writable_scopes()
+    )
+    if args.included:
+        scopes = (i for s in scopes for i in s.included_scopes)
+    info = (_config_scope_info_string(args, s) for s in scopes)
+
+    print(" ".join(info))
+
+
 def config_add(args):
     """Add the given configuration to the specified config scope
 
@@ -580,6 +637,7 @@ def config(parser, args):
         "blame": config_blame,
         "edit": config_edit,
         "list": config_list,
+        "scopes": config_scopes,
         "add": config_add,
         "rm": config_remove,
         "remove": config_remove,
diff --git a/lib/spack/spack/test/cmd/config.py b/lib/spack/spack/test/cmd/config.py
index f203bab1c05f73..e371b4c62e0227 100644
--- a/lib/spack/spack/test/cmd/config.py
+++ b/lib/spack/spack/test/cmd/config.py
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 import functools
 import os
+import re
 
 import pytest
 
@@ -40,6 +41,38 @@ def config_yaml_v015(mutable_config):
     return functools.partial(_create_config, data=old_data, section="config")
 
 
+def test_config_scopes():
+    output = config("scopes")
+    assert "command_line" in output.split()
+    assert "_builtin" in output.split()
+
+
+def test_config_scopes_path_scopes():
+    output = config("scopes", "--path-scopes")
+    assert "site" in output.split()
+    assert "_builtin" not in output.split()
+
+
+@pytest.mark.parametrize("with_path_scopes", [False, True])
+def test_config_scopes_include(with_path_scopes):
+    scopes_cmd = ["scopes", "--included"]
+    if with_path_scopes:
+        scopes_cmd.append("--path-scopes")
+    output = config(*scopes_cmd)
+    assert not output or all(":" in x for x in output.split())
+
+
+scope_path_re = r"\(([^\)]+)\)"
+
+
+def test_config_scopes_path_section():
+    output = config("scopes", "--included", "--show-paths", "modules")
+    assert "_builtin" not in output
+    assert "site" not in output
+    paths = (x[1] for x in (re.fullmatch(scope_path_re, s) for s in output.split()) if x)
+    assert all(os.sep in x for x in paths)
+
+
 def test_get_config_scope(mock_low_high_config):
     assert config("get", "compilers").strip() == "compilers: {}"
 
diff --git a/share/spack/spack-completion.bash b/share/spack/spack-completion.bash
index b8dc5a83ad1f89..7224bce9b02db0 100644
--- a/share/spack/spack-completion.bash
+++ b/share/spack/spack-completion.bash
@@ -832,7 +832,7 @@ _spack_config() {
     then
         SPACK_COMPREPLY="-h --help --scope"
     else
-        SPACK_COMPREPLY="get blame edit list add change prefer-upstream remove rm update revert"
+        SPACK_COMPREPLY="get blame edit list scopes add change prefer-upstream remove rm update revert"
     fi
 }
 
@@ -867,6 +867,15 @@ _spack_config_list() {
     SPACK_COMPREPLY="-h --help"
 }
 
+_spack_config_scopes() {
+    if $list_options
+    then
+        SPACK_COMPREPLY="-h --help -i --included -p --path-scopes -s --show-paths"
+    else
+        _config_sections
+    fi
+}
+
 _spack_config_add() {
     if $list_options
     then
diff --git a/share/spack/spack-completion.fish b/share/spack/spack-completion.fish
index e2edf36ffaf3cf..a56fe06828ce6e 100644
--- a/share/spack/spack-completion.fish
+++ b/share/spack/spack-completion.fish
@@ -1197,6 +1197,7 @@ complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a get -d 'pri
 complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a blame -d 'print configuration annotated with source file:line'
 complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a edit -d 'edit configuration file'
 complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a list -d 'list configuration sections'
+complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a scopes -d 'list defined scopes in descending order of precedence'
 complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a add -d 'add configuration parameters'
 complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a change -d 'swap variants etc. on specs in config'
 complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a prefer-upstream -d 'set package preferences from upstream'
@@ -1234,6 +1235,18 @@ set -g __fish_spack_optspecs_spack_config_list h/help
 complete -c spack -n '__fish_spack_using_command config list' -s h -l help -f -a help
 complete -c spack -n '__fish_spack_using_command config list' -s h -l help -d 'show this help message and exit'
 
+# spack config scopes
+set -g __fish_spack_optspecs_spack_config_scopes h/help i/included p/path-scopes s/show-paths
+complete -c spack -n '__fish_spack_using_command_pos 0 config scopes' -f -a 'bootstrap cdash ci compilers concretizer config definitions develop env_vars include mirrors modules packages repos upstreams view'
+complete -c spack -n '__fish_spack_using_command config scopes' -s h -l help -f -a help
+complete -c spack -n '__fish_spack_using_command config scopes' -s h -l help -d 'show this help message and exit'
+complete -c spack -n '__fish_spack_using_command config scopes' -s i -l included -f -a included
+complete -c spack -n '__fish_spack_using_command config scopes' -s i -l included -d 'list only included scopes'
+complete -c spack -n '__fish_spack_using_command config scopes' -s p -l path-scopes -f -a path_scopes
+complete -c spack -n '__fish_spack_using_command config scopes' -s p -l path-scopes -d 'list only writable scopes with an associated path'
+complete -c spack -n '__fish_spack_using_command config scopes' -s s -l show-paths -f -a show_paths
+complete -c spack -n '__fish_spack_using_command config scopes' -s s -l show-paths -d 'show associated paths for appropriate scopes'
+
 # spack config add
 set -g __fish_spack_optspecs_spack_config_add h/help f/file=
 complete -c spack -n '__fish_spack_using_command_pos 0 config add' -f -a '(__fish_spack_colon_path)'


Reviews:
user: login: tldahlgren
body: LGTM. I have a couple of questions. One is about the mutual exclusion of the options. The other is have you confirmed the outputs in the description are up-to-date?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:32:10Z
user: login: tldahlgren
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T21:51:53Z
user: login: greenc-FNAL
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-24T22:00:16Z
user: login: tldahlgren
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T22:00:55Z
user: login: tldahlgren
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T22:31:54Z
user: login: tldahlgren
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T23:31:33Z
user: login: greenc-FNAL
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T14:48:48Z
user: login: greenc-FNAL
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T14:53:40Z
user: login: tldahlgren
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T16:57:00Z
user: login: tldahlgren
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T22:58:17Z
user: login: tldahlgren
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T00:56:21Z
user: login: tldahlgren
body: See comments. Would like to see a unit test for the `-s | --show-paths` option. IMO it would be sufficient to make sure `os.path.join("etc", "spack", "defaults")` appears in the output.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T19:39:44Z
user: login: greenc-FNAL
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-27T20:56:31Z
user: login: tldahlgren
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T21:30:58Z
user: login: tldahlgren
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T00:29:58Z
user: login: greenc-FNAL
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T13:58:07Z
user: login: greenc-FNAL
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T15:52:09Z
user: login: tldahlgren
body: LGTM.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T23:12:38Z

Review comments:
diff_hunk: @@ -68,6 +68,18 @@ def setup_parser(subparser):
 
     sp.add_parser("list", help="list configuration sections")
 
+    list_scopes_parser = sp.add_parser("list-scopes", help="list defined scopes")
+    stype = list_scopes_parser.add_mutually_exclusive_group(required=False)
path: lib/spack/spack/cmd/config.py
user: login: tldahlgren
body: I know there aren't a lot of scopes by default, but why are these mutually exclusive?
created_at: 2025-03-24T21:20:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -38,6 +38,24 @@ def config_yaml_v015(mutable_config):
     return functools.partial(_create_config, data=old_data, section="config")
 
 
+def test_config_list_scopes():
+    output = config("list-scopes")
+    assert "command_line" in output
+    assert "_builtin" in output
+
+
+def test_config_list_scopes_file():
+    output = config("list-scopes", "--file")
+    assert "site" in output
+    assert "_builtin" not in output
+
+
+def test_config_list_scopes_non_platform():
+    output = config("list-scopes", "--non-platform")
+    assert "site" in output
+    assert "user" in output
path: lib/spack/spack/test/cmd/config.py
user: login: tldahlgren
body: This test fails for me if `SPACK_DISABLE_LOCAL_CONFIG` is `true` when I run the tests.
created_at: 2025-03-24T21:51:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,6 +68,18 @@ def setup_parser(subparser):
 
     sp.add_parser("list", help="list configuration sections")
 
+    list_scopes_parser = sp.add_parser("list-scopes", help="list defined scopes")
+    stype = list_scopes_parser.add_mutually_exclusive_group(required=False)
+    stype.add_argument(
+        "--file",
path: lib/spack/spack/cmd/config.py
user: login: tldahlgren
body: Please add support for `-f` as short-hand for this option.
created_at: 2025-03-24T21:59:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,6 +68,18 @@ def setup_parser(subparser):
 
     sp.add_parser("list", help="list configuration sections")
 
+    list_scopes_parser = sp.add_parser("list-scopes", help="list defined scopes")
+    stype = list_scopes_parser.add_mutually_exclusive_group(required=False)
path: lib/spack/spack/cmd/config.py
user: login: greenc-FNAL
body: I think because the way I was originally obtaining non-platform scopes, they were `--file` by construction.
created_at: 2025-03-24T22:00:16Z
author_association: MEMBER
diff_hunk: @@ -38,6 +38,24 @@ def config_yaml_v015(mutable_config):
     return functools.partial(_create_config, data=old_data, section="config")
 
 
+def test_config_list_scopes():
+    output = config("list-scopes")
+    assert "command_line" in output
+    assert "_builtin" in output
+
+
+def test_config_list_scopes_file():
+    output = config("list-scopes", "--file")
+    assert "site" in output
+    assert "_builtin" not in output
+
+
+def test_config_list_scopes_non_platform():
+    output = config("list-scopes", "--non-platform")
+    assert "site" in output
+    assert "user" in output
path: lib/spack/spack/test/cmd/config.py
user: login: tldahlgren
body: You might be able to get some inspiration from https://github.com/spack/spack/blob/8ac826cca892f9060ced53a071e78e0119dd8aca/lib/spack/spack/test/config.py#L1230.
created_at: 2025-03-24T22:31:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,18 +43,35 @@ def test_config_list_scopes():
     output = config("list-scopes")
     assert "command_line" in output
     assert "_builtin" in output
+    assert f"site/{spack.platforms.host()}" in output
path: lib/spack/spack/test/cmd/config.py
user: login: tldahlgren
body: You probably don't want to add this. The session-level fixture `_use_test_platform()` causes the left hand side to be `site/test`.
created_at: 2025-03-24T23:06:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,18 +43,35 @@ def test_config_list_scopes():
     output = config("list-scopes")
     assert "command_line" in output
     assert "_builtin" in output
+    assert f"site/{spack.platforms.host()}" in output
 
 
 def test_config_list_scopes_file():
     output = config("list-scopes", "--file")
     assert "site" in output
+    assert f"site/{spack.platforms.host()}" in output
     assert "_builtin" not in output
 
 
 def test_config_list_scopes_non_platform():
+    os.environ["SPACK_DISABLE_LOCAL_CONFIG"] = "true"
+    output = config("list-scopes", "--non-platform")
+    assert "site" in output
+    assert "user" not in output
+    assert f"site/{spack.platforms.host()}" not in output
+    del os.environ["SPACK_DISABLE_LOCAL_CONFIG"]
     output = config("list-scopes", "--non-platform")
     assert "site" in output
     assert "user" in output
+    assert f"site/{spack.platforms.host()}" not in output
+
+
+def test_config_list_scopes_file_non_platform():
+    del os.environ["SPACK_DISABLE_LOCAL_CONFIG"]
path: lib/spack/spack/test/cmd/config.py
user: login: tldahlgren
body: This will fail on a key error.
created_at: 2025-03-24T23:08:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,18 +43,35 @@ def test_config_list_scopes():
     output = config("list-scopes")
     assert "command_line" in output
     assert "_builtin" in output
+    assert f"site/{spack.platforms.host()}" in output
 
 
 def test_config_list_scopes_file():
     output = config("list-scopes", "--file")
     assert "site" in output
+    assert f"site/{spack.platforms.host()}" in output
     assert "_builtin" not in output
 
 
 def test_config_list_scopes_non_platform():
+    os.environ["SPACK_DISABLE_LOCAL_CONFIG"] = "true"
+    output = config("list-scopes", "--non-platform")
+    assert "site" in output
+    assert "user" not in output
+    assert f"site/{spack.platforms.host()}" not in output
+    del os.environ["SPACK_DISABLE_LOCAL_CONFIG"]
     output = config("list-scopes", "--non-platform")
     assert "site" in output
     assert "user" in output
+    assert f"site/{spack.platforms.host()}" not in output
+
+
+def test_config_list_scopes_file_non_platform():
+    del os.environ["SPACK_DISABLE_LOCAL_CONFIG"]
path: lib/spack/spack/test/cmd/config.py
user: login: greenc-FNAL
body: Argh, thanks.

created_at: 2025-03-25T14:48:48Z
author_association: MEMBER
diff_hunk: @@ -42,18 +43,35 @@ def test_config_list_scopes():
     output = config("list-scopes")
     assert "command_line" in output
     assert "_builtin" in output
+    assert f"site/{spack.platforms.host()}" in output
path: lib/spack/spack/test/cmd/config.py
user: login: greenc-FNAL
body: What do you mean by, "session-level?" Is that unconditionally invoked for all tests, or as part of an in-test-file directive, or in test configuration, or on the command line? I'm not currently seeing the invocation mechanism, sorry.

created_at: 2025-03-25T14:53:40Z
author_association: MEMBER
diff_hunk: @@ -42,18 +43,35 @@ def test_config_list_scopes():
     output = config("list-scopes")
     assert "command_line" in output
     assert "_builtin" in output
+    assert f"site/{spack.platforms.host()}" in output
path: lib/spack/spack/test/cmd/config.py
user: login: tldahlgren
body: They apply to all test functions run during the test "session" (https://docs.pytest.org/en/stable/explanation/fixtures.html).
created_at: 2025-03-25T16:57:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,6 +68,24 @@ def setup_parser(subparser):
 
     sp.add_parser("list", help="list configuration sections")
 
+    list_scopes_parser = sp.add_parser("list-scopes", help="list defined scopes")
path: lib/spack/spack/cmd/config.py
user: login: tldahlgren
body: Can  this be simplified to `scopes` so `spack config scopes`?
created_at: 2025-05-21T22:58:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,6 +68,24 @@ def setup_parser(subparser):
 
     sp.add_parser("list", help="list configuration sections")
 
+    list_scopes_parser = sp.add_parser("list-scopes", help="list defined scopes")
path: lib/spack/spack/cmd/config.py
user: login: tldahlgren
body: Thanks!
created_at: 2025-05-23T00:56:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +40,36 @@ def config_yaml_v015(mutable_config):
     return functools.partial(_create_config, data=old_data, section="config")
 
 
+def test_config_scopes():
+    output = config("scopes")
+    assert "command_line" in output
+    assert "_builtin" in output
+
+
+def test_config_scopes_path():
+    output = config("scopes", "--path")
+    assert "site" in output
+    assert "_builtin" not in output
+
+
+def test_config_scopes_include():
+    output = config("scopes", "--include")
+    assert "site" in output
+    assert "_builtin" not in output
+
+
+def test_config_scopes_include_path():
+    output = config("scopes", "--include", "--path")
+    assert "site" in output
+    assert "_builtin" not in output
+
+
+def test_config_scopes_path_section():
+    output = config("scopes", "--include", "--path", "config")
+    assert "site" in output
+    assert "_builtin" not in output
path: lib/spack/spack/test/cmd/config.py
user: login: tldahlgren
body: It would be pretty straightforward to parameterize at least a couple of the new tests based on the (additional) options passed to the command, especially without additional checks.

On the chance you haven't done this before, there are two other tests in this module you can look to for reference (e.g., https://github.com/spack/spack/blob/c2e3b086fe2b22aab0d66d0c3cdc660de66761d6/lib/spack/spack/test/config.py#L480).
created_at: 2025-05-27T19:14:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +40,36 @@ def config_yaml_v015(mutable_config):
     return functools.partial(_create_config, data=old_data, section="config")
 
 
+def test_config_scopes():
+    output = config("scopes")
+    assert "command_line" in output
+    assert "_builtin" in output
+
+
+def test_config_scopes_path():
+    output = config("scopes", "--path")
+    assert "site" in output
+    assert "_builtin" not in output
+
+
+def test_config_scopes_include():
+    output = config("scopes", "--include")
+    assert "site" in output
+    assert "_builtin" not in output
path: lib/spack/spack/test/cmd/config.py
user: login: tldahlgren
body: This would be more meaningful IMO if it checked to make sure that the scopes that are output all start with `include` (or `include:`).
created_at: 2025-05-27T19:24:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,6 +243,33 @@ def config_list(args):
     print(" ".join(list(spack.config.SECTION_SCHEMAS)))
 
 
+def _config_scope_info_string(args, scope):
+    if args.show_paths and hasattr(scope, "path"):
+        section_path = scope.get_section_filename(args.section) if args.section else None
+        path = section_path if section_path and os.path.exists(section_path) else f"{scope.path}/"
path: lib/spack/spack/cmd/config.py
user: login: tldahlgren
body: Should this be using `os.sep` in the `else` instead of `/`?
created_at: 2025-05-27T19:29:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +40,36 @@ def config_yaml_v015(mutable_config):
     return functools.partial(_create_config, data=old_data, section="config")
 
 
+def test_config_scopes():
+    output = config("scopes")
+    assert "command_line" in output
+    assert "_builtin" in output
+
+
+def test_config_scopes_path():
+    output = config("scopes", "--path")
+    assert "site" in output
+    assert "_builtin" not in output
+
+
+def test_config_scopes_include():
+    output = config("scopes", "--include")
+    assert "site" in output
+    assert "_builtin" not in output
path: lib/spack/spack/test/cmd/config.py
user: login: tldahlgren
body: Given `--include` takes precedence over `---show-paths`, the same goes for `test_config_scopes_include_path`.
created_at: 2025-05-27T19:31:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,6 +243,33 @@ def config_list(args):
     print(" ".join(list(spack.config.SECTION_SCHEMAS)))
 
 
+def _config_scope_info_string(args, scope):
+    if args.show_paths and hasattr(scope, "path"):
+        section_path = scope.get_section_filename(args.section) if args.section else None
+        path = section_path if section_path and os.path.exists(section_path) else f"{scope.path}/"
+        return f"{scope.name} ({path})"
+    else:
+        return scope.name
+
+
+def config_scopes(args):
+    """List configured scopes in descending order of precedence."""
+
+    if args.section and not args.show_paths:
+        tty.warning(f"[section] ({args.section}) ignored without --show-path")
path: lib/spack/spack/cmd/config.py
user: login: tldahlgren
body: Unfortunately, `tty` uses `warn` instead of `warning`:

```suggestion
        tty.warn(f"[section] ({args.section}) ignored without --show-paths")
```
created_at: 2025-05-27T19:32:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,6 +243,33 @@ def config_list(args):
     print(" ".join(list(spack.config.SECTION_SCHEMAS)))
 
 
+def _config_scope_info_string(args, scope):
+    if args.show_paths and hasattr(scope, "path"):
+        section_path = scope.get_section_filename(args.section) if args.section else None
+        path = section_path if section_path and os.path.exists(section_path) else f"{scope.path}/"
+        return f"{scope.name} ({path})"
+    else:
+        return scope.name
+
+
+def config_scopes(args):
+    """List configured scopes in descending order of precedence."""
+
+    if args.section and not args.show_paths:
+        tty.warning(f"[section] ({args.section}) ignored without --show-path")
path: lib/spack/spack/cmd/config.py
user: login: tldahlgren
body: Would also like to see the shortcut (`-s`) here.
created_at: 2025-05-27T19:36:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +40,36 @@ def config_yaml_v015(mutable_config):
     return functools.partial(_create_config, data=old_data, section="config")
 
 
+def test_config_scopes():
+    output = config("scopes")
+    assert "command_line" in output
+    assert "_builtin" in output
+
+
+def test_config_scopes_path():
+    output = config("scopes", "--path")
+    assert "site" in output
+    assert "_builtin" not in output
+
+
+def test_config_scopes_include():
+    output = config("scopes", "--include")
+    assert "site" in output
+    assert "_builtin" not in output
path: lib/spack/spack/test/cmd/config.py
user: login: greenc-FNAL
body: Of course, as soon as #50603 merges, this will no longer be true :-(
created_at: 2025-05-27T20:56:31Z
author_association: MEMBER
diff_hunk: @@ -215,6 +243,33 @@ def config_list(args):
     print(" ".join(list(spack.config.SECTION_SCHEMAS)))
 
 
+def _config_scope_info_string(args, scope):
+    if args.show_paths and hasattr(scope, "path"):
+        section_path = scope.get_section_filename(args.section) if args.section else None
+        path = section_path if section_path and os.path.exists(section_path) else f"{scope.path}/"
+        return f"{scope.name} ({path})"
+    else:
+        return scope.name
+
+
+def config_scopes(args):
+    """List configured scopes in descending order of precedence."""
+
+    if args.section and not args.show_paths:
+        tty.warning(f"[section] ({args.section}) ignored without --show-path")
path: lib/spack/spack/cmd/config.py
user: login: tldahlgren
body: Thanks
created_at: 2025-05-27T21:30:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,6 +68,34 @@ def setup_parser(subparser):
 
     sp.add_parser("list", help="list configuration sections")
 
+    scopes_parser = sp.add_parser(
+        "scopes", help="list defined scopes in descending order of precedence"
+    )
+    scopes_parser.add_argument(
+        "-i", "--included", action="store_true", default=False, help="list only included scopes"
+    )
+    scopes_parser.add_argument(
+        "-p",
+        "--path-scopes",
+        action="store_true",
+        default=False,
+        help="list only writable scopes with an associated path",
+    )
path: lib/spack/spack/cmd/config.py
user: login: tldahlgren
body: Almost forgot to mention you can use the `add_mutually_exclusive_group()` method to allow the parser to handle the mutual exclusion for the options for you. Adding for these two arguments, you get:

```
$ spack config scopes -h
usage: spack config scopes [-hs] [-i | -p] [section]

positional arguments:
  section            tailor scope path information to the specified section
                     options: compilers, concretizer, definitions, env_vars, include, view, develop, mirrors, repos, packages, modules, config, upstreams, bootstrap, ci, cdash

options:
  -h, --help         show this help message and exit
  -i, --included     list only included scopes
  -p, --path-scopes  list only writable scopes with an associated path
  -s, --show-paths   show associated paths for appropriate scopes

$ spack config scopes -i -p
usage: spack config scopes [-hs] [-i | -p] [section]
spack config scopes: error: argument -p/--path-scopes: not allowed with argument -i/--included
```

created_at: 2025-05-28T00:29:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,6 +68,34 @@ def setup_parser(subparser):
 
     sp.add_parser("list", help="list configuration sections")
 
+    scopes_parser = sp.add_parser(
+        "scopes", help="list defined scopes in descending order of precedence"
+    )
+    scopes_parser.add_argument(
+        "-i", "--included", action="store_true", default=False, help="list only included scopes"
+    )
+    scopes_parser.add_argument(
+        "-p",
+        "--path-scopes",
+        action="store_true",
+        default=False,
+        help="list only writable scopes with an associated path",
+    )
path: lib/spack/spack/cmd/config.py
user: login: greenc-FNAL
body: I had mutually exclusive groups way back when, and then I removed them based on a suggestion from you (although I think the semantics of the relevant options were different). Included scopes are also path scopes, so when `-i` is specified the `-p` is redundant rather than inconsistent. I can certainly have them mutually exclusive in this case if you prefer, though.

What I'd like is for the non-option argument to be bound to the `-s` option if there's a way to do that, although having the presence of the non-option argument _imply_ `-s` would probably be sufficient.
created_at: 2025-05-28T13:58:07Z
author_association: MEMBER
diff_hunk: @@ -40,6 +40,36 @@ def config_yaml_v015(mutable_config):
     return functools.partial(_create_config, data=old_data, section="config")
 
 
+def test_config_scopes():
+    output = config("scopes")
+    assert "command_line" in output
+    assert "_builtin" in output
+
+
+def test_config_scopes_path():
+    output = config("scopes", "--path")
+    assert "site" in output
+    assert "_builtin" not in output
+
+
+def test_config_scopes_include():
+    output = config("scopes", "--include")
+    assert "site" in output
+    assert "_builtin" not in output
path: lib/spack/spack/test/cmd/config.py
user: login: greenc-FNAL
body: The test now ensures that all included scopes contain `:`.
created_at: 2025-05-28T15:52:08Z
author_association: MEMBER

PR comments:
user: login: greenc-FNAL
created_at: 2025-03-24T20:04:31Z
body: @tldahlgren I have (I believe) brought this PR up to date in the light of #48784. I'd be grateful if you could help me get this merged, please.
author_association: MEMBER
user: login: greenc-FNAL
created_at: 2025-03-24T21:58:00Z
body: > LGTM. I have a couple of questions. One is about the mutual exclusion of the options. The other is have you confirmed the outputs in the description are up-to-date?

Looking into removing mutual exclusion, and I have updated the description.
author_association: MEMBER
user: login: greenc-FNAL
created_at: 2025-03-26T14:24:32Z
body: @tldahlgren I think I've addressed your comments, but if you have any tips on how to test either the inclusion/exclusion of platform scopes, or `--included`, I'd appreciate it.

author_association: MEMBER
user: login: greenc-FNAL
created_at: 2025-05-22T13:24:23Z
body: This will need to be adjusted when platform scopes go away with #50603.
author_association: MEMBER
user: login: greenc-FNAL
created_at: 2025-05-22T13:25:51Z
body: Also @tldahlgren @tgamblin Any chance this can be in 1.0? I think this would complement #50603 nicely.
author_association: MEMBER
user: login: tldahlgren
created_at: 2025-05-22T14:32:24Z
body: > Also @tldahlgren @tgamblin Any chance this can be in 1.0? I think this would complement #50603 nicely.

Yes. I didn't get a chance yesterday to look closely at the alternative for fixing the unit test. But I'm planning to do that today.
author_association: CONTRIBUTOR
user: login: greenc-FNAL
created_at: 2025-05-22T15:05:51Z
body: @tldahlgren The pipeline https://gitlab.spack.io/spack/spack/-/pipelines/1095847 seems to indicate that `hep-generate` is not properly configured as a dependency of `hep-build`, because the latter failed while the former was still running. Help?

author_association: MEMBER
user: login: greenc-FNAL
created_at: 2025-05-22T15:16:29Z
body: > > Also @tldahlgren @tgamblin Any chance this can be in 1.0? I think this would complement #50603 nicely.
> 
> Yes. I didn't get a chance yesterday to look closely at the alternative for fixing the unit test. But I'm planning to do that today.

Thanks for this. I think improving the test coverage is the only remaining task for this PR, no?
author_association: MEMBER
user: login: greenc-FNAL
created_at: 2025-05-22T18:30:23Z
body: Would it be reasonable to take optional positional arguments which cause `spack config scopes` to list all the scopes that apply to a specific config type? Then `--file` would cause `include:almalinux9` to be displayed as `$spack/etc/spack/linux/almalinux9/packages.yaml` if that file were present when `spack config scopes --file packages` were invoked.
author_association: MEMBER
user: login: tgamblin
created_at: 2025-05-23T01:43:43Z
body: could we make it `-p / --paths` to be consistent with `spack find --paths`?  And maybe when you supply the arg it displays the paths next to the name?
author_association: MEMBER
user: login: greenc-FNAL
created_at: 2025-05-28T19:16:48Z
body: @tldahlgren pending your re-review, and Gitlab getting around to running the CI pipeline, I think this is ready to go.
author_association: MEMBER
user: login: greenc-FNAL
created_at: 2025-05-28T20:50:30Z
body: @spackbot run pipeline
author_association: MEMBER
user: login: spackbot-app[bot]
created_at: 2025-05-28T20:50:33Z
body: 
I'm sorry, gitlab does not have your latest revision yet, I can't run that pipeline for you right now.

One likely possibility is that your PR pipeline has been temporarily [deferred](https://github.com/spack/spack-infrastructure/blob/main/docs/deferred_pipelines.md), in which case, it is awaiting a `develop` pipeline, and will be run when that finishes.

Please check the gitlab commit status message to see if more information is available.

<details>
<summary><b>Details</b></summary>

```bash
pr head: d590a35, gitlab commit parents: ['7bca5ed', '5f0cf46']
```
</details>

author_association: NONE