Repository: pantsbuild/pants

Issue:
The global `--help` option no longer works when specs used.
Given a Pants command line:
```
./pants test --output=all --force src/python/pants/bsp/protocol_test.py
```

I can no longer insert a request for `--help`:
```
$ ./pants --help test src/python/pants/bsp/protocol_test.py
Unknown entity: src/python/pants/bsp/protocol_test.py
$ ./pants test --help src/python/pants/bsp/protocol_test.py
Unknown entity: src/python/pants/bsp/protocol_test.py
$ ./pants test src/python/pants/bsp/protocol_test.py --help
11:00:27.87 [INFO] Completed: Run Pytest - src/python/pants/bsp/protocol_test.py:tests succeeded.

✓ src/python/pants/bsp/protocol_test.py:tests succeeded in 1.10s (cached locally).
```

I have to issue one of these with the specs removed:
```
$ ./pants help test
$ ./pants test --help
$ ./pants --help test
```




Discussion:
There appear to be 2 different problems.

1. I bisected `./pants test --help src/python/pants/util/strutil_test.py` to #15646:
    ```
    $ git bisect good
    2c629bda26452483bb02457741506f779344750a is the first bad commit
    commit 2c629bda26452483bb02457741506f779344750a
    Author: Andreas Stenius <andreas.stenius@svenskaspel.se>
    Date:   Thu May 26 11:25:52 2022 +0200
    
        Provide more "Did you mean ..." suggestions for the CLI help. (#15646)
        
        [ci skip-rust]
    
     src/python/pants/help/help_printer.py | 177 +++++++++++++++++++++++++---------
     1 file changed, 129 insertions(+), 48 deletions(-)
    ```
2. I quasi-bisected `./pants test src/python/pants/util/strutil_test.py --help` to #15626
    ```
    ^jsirois@gill ~/dev/pantsbuild/pants ((23d15ab18...)|BISECTING) $ ./pants test src/python/pants/util/strutil_test.py --help
    11:50:47.27 [INFO] Initializing scheduler...
    11:50:47.62 [INFO] Scheduler initialized.
    11:50:47.74 [ERROR] 1 Exception encountered:
    
    Engine traceback:
      in select
      in pants.core.goals.test.run_tests
      in pants.engine.internals.specs_rules.find_valid_field_sets_for_target_roots
      in pants.engine.internals.specs_rules.filter_targets
      in pants.engine.internals.graph.resolve_targets
      in pants.engine.internals.graph.resolve_unexpanded_targets
      in pants.engine.internals.specs_rules.resolve_addresses_from_specs
      in pants.engine.internals.specs_rules.addresses_from_specs_without_file_owners
      in paths
    Traceback (no traceback):
      <pants native internals>
    Exception: Unmatched glob from CLI arguments: "--help/*"
    
    Do the file(s) exist? If so, check if the file(s) are in your `.gitignore` or the global `pants_ignore` option, which may result in Pants not being able to see the file(s) even though they exist on disk. Refer to https://www.pantsbuild.org/v2.13/docs/troubleshooting#pants-cannot-find-a-file-in-your-project.
    ```

@kaos and @Eric-Arellano I'm going to turn lazy and hand off to you two for 1 & 2 respectively at this point.
I updated the PR title to mention that Specs are the main issue. Pants thinks naively that you're trying to get help on the spec `src/python/pants/bsp/protocol_test.py`. 

Given that we have tight naming rules for what you can get help with—options scopes, target types, and now API types (Python classes)—it might make sense to ignore what we're reasonably confident is a spec. `src/python/pants/bsp/protocol_test.py` violates those naming rules, and you can't get help on it.

So, I think we should ignore whatever we think is a spec:

https://github.com/pantsbuild/pants/blob/c386d24444f92473e1437e4d02f4d5d168b3e7a6/src/python/pants/option/arg_splitter.py#L230-L237

Thoughts?
That sounds fine. Beyond naming though, cant target addresses (path part) and file specs both be run through an os.path.exists check? IIRC we did something like this in v1 with the only ambiguity left being goal names that matched top-level dir names.
Ah, missed it - you do that. SGTM.

Resolving patch:
diff --git a/src/python/pants/goal/help.py b/src/python/pants/goal/help.py
--- a/src/python/pants/goal/help.py
+++ b/src/python/pants/goal/help.py
@@ -79,9 +79,11 @@ class ThingHelpBuiltinGoalBase(HelpBuiltinGoalBase):
     def create_help_request(self, options: Options) -> HelpRequest:
         # Include the `options.specs` for things to give help on, as args with any . : * or # in
         # them is deemed "likely a spec" by `pants.option.arg_splitter:ArgSplitter.likely_a_spec()`.
+        # We want to support getting help on API types that may contain periods.
         return ThingHelp(
             advanced=self._advanced,
-            things=tuple(options.goals) + tuple(options.unknown_goals) + tuple(options.specs),
+            things=tuple(options.goals) + tuple(options.unknown_goals),
+            likely_specs=tuple(options.specs),
         )
 
 
diff --git a/src/python/pants/help/help_printer.py b/src/python/pants/help/help_printer.py
--- a/src/python/pants/help/help_printer.py
+++ b/src/python/pants/help/help_printer.py
@@ -161,12 +161,17 @@ def _print_thing_help(self) -> Literal[0, 1]:
         Note: Ony useful if called after options have been registered.
         """
         help_request = cast(ThingHelp, self._help_request)
-        things = set(help_request.things)
+        # API types may end up in `likely_specs`, so include them in things to get help for.
+        things = set(help_request.things + help_request.likely_specs)
         help_table = self._get_thing_help_table()
         maybe_unknown_things = {thing for thing in things if thing not in help_table}
         disambiguated_things, unknown_things = self._disambiguate_things(
             maybe_unknown_things, help_table.keys()
         )
+        things = things - maybe_unknown_things | disambiguated_things
+        # Filter out likely specs from unknown things, as we don't want them to interfere.
+        unknown_things -= set(help_request.likely_specs)
+
         if unknown_things:
             # Only print help and suggestions for the first unknown thing.
             # It gets confusing to try and show suggestions for multiple cases.
@@ -183,7 +188,6 @@ def _print_thing_help(self) -> Literal[0, 1]:
             )
             return 1
 
-        things = things - maybe_unknown_things | disambiguated_things
         if not things:
             self._print_global_help()
             return 0
diff --git a/src/python/pants/option/arg_splitter.py b/src/python/pants/option/arg_splitter.py
--- a/src/python/pants/option/arg_splitter.py
+++ b/src/python/pants/option/arg_splitter.py
@@ -40,6 +40,7 @@ class ThingHelp(HelpRequest):
 
     advanced: bool = False
     things: tuple[str, ...] = ()
+    likely_specs: tuple[str, ...] = ()
 
 
 class VersionHelp(HelpRequest):


Test patch:
diff --git a/src/python/pants/help/help_integration_test.py b/src/python/pants/help/help_integration_test.py
--- a/src/python/pants/help/help_integration_test.py
+++ b/src/python/pants/help/help_integration_test.py
@@ -204,3 +204,11 @@ def test_help_provided_target_plugin_field() -> None:
         )
         in pants_run.stdout
     )
+
+
+def test_help_ignore_specs() -> None:
+    pants_run = run_pants(
+        ["test", "src/python/pants/bsp/protocol_test.py", "--help"],
+    )
+    pants_run.assert_success()
+    assert "`test` goal options" in pants_run.stdout