Repository: pantsbuild/pants

Issue:
`experimental_run_shell_command` eats first pass-through arg
**Describe the bug**
When I pass arguments from the command line to an `experimental_run_shell_command` my first argument gets "eaten" as being arg 0 (the executable name).

**Pants version**
`main`

**OS**
Mac

**Additional info**
May apply to `experimental_shell_command` as well, relating to #17616 

Example:
```python
experimental_run_shell_command(
    name="args-test",
    command='echo "cmd name (arg 0)=$0, args:$@"',
)
```

```shell
./pants run :args-test -- arg1 arg2 arg3 arg4
cmd name (arg 0)=arg1, args:arg2 arg3 arg4
```

I would prefer if we injected a made up value for arg 0 (perhaps the target address?), as I don't expect to pass that on the command line.


Discussion:
This looks like a weird interaction with how `bash` handles `-c` arguments. Rather than treating the value of `-c blah` as the `$0` argument, the argument numbers are treated as if `-c blah` isn't even there. I suspect rather than trying to predict the behaviour of `bash` here, we may actually want to dump the boot script and command into a shell script and execute that instead.

@kaos any indication if this is a regression from behaviour in a released version?
No, I don't think this is a regression, only something I've never considered before.

Sounds like a good move to generate a boot script to run rather than using `-c` 👍🏽 
Cool, I'll make that change. We can name the file after an fs-safe target spec too
using an actual shell script makes bash set the working directory to the location of the shell script. I'll stick with `-c` and adjust all the tests.
PR incoming. This is cool!

```
$ cat ersc_errors/BUILD
experimental_run_shell_command(
    name="errors",
    command="source please_fail",
)
$ ./pants_from_sources run ersc_errors:errors
ersc_errors.errors: please_fail: No such file or directory
```

Resolving patch:
diff --git a/src/python/pants/backend/shell/util_rules/shell_command.py b/src/python/pants/backend/shell/util_rules/shell_command.py
--- a/src/python/pants/backend/shell/util_rules/shell_command.py
+++ b/src/python/pants/backend/shell/util_rules/shell_command.py
@@ -84,6 +84,7 @@ class GenerateFilesFromRunInSandboxRequest(GenerateSourcesRequest):
 @dataclass(frozen=True)
 class ShellCommandProcessRequest:
     description: str
+    shell_name: str
     interactive: bool
     working_directory: str | None
     command: str
@@ -125,6 +126,7 @@ async def _prepare_process_request_from_target(shell_command: Target) -> ShellCo
 
     return ShellCommandProcessRequest(
         description=description,
+        shell_name=shell_command.address.spec,
         interactive=interactive,
         working_directory=working_directory,
         command=command,
@@ -349,6 +351,7 @@ async def run_in_sandbox_request(
 
     process_request = ShellCommandProcessRequest(
         description=description,
+        shell_name=shell_command.address.spec,
         interactive=False,
         working_directory=working_directory,
         command=" ".join(shlex.quote(arg) for arg in (run_request.args + extra_args)),
@@ -426,6 +429,7 @@ async def prepare_shell_command_process(
 ) -> Process:
 
     description = shell_command.description
+    shell_name = shell_command.shell_name
     interactive = shell_command.interactive
     working_directory = shell_command.working_directory
     command = shell_command.command
@@ -485,7 +489,7 @@ async def prepare_shell_command_process(
         )
 
     proc = Process(
-        argv=(bash.path, "-c", boot_script + command),
+        argv=(bash.path, "-c", boot_script + command, shell_name),
         description=f"Running {description}",
         env=command_env,
         input_digest=input_digest,


Test patch:
diff --git a/src/python/pants/backend/shell/util_rules/shell_command_integration_test.py b/src/python/pants/backend/shell/util_rules/shell_command_integration_test.py
new file mode 100644
--- /dev/null
+++ b/src/python/pants/backend/shell/util_rules/shell_command_integration_test.py
@@ -0,0 +1,29 @@
+# Copyright 2023 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+from textwrap import dedent
+
+from pants.testutil.pants_integration_test import run_pants, setup_tmpdir
+
+
+def test_passthrough_args() -> None:
+    sources = {
+        "src/BUILD": dedent(
+            """\
+            experimental_run_shell_command(
+                name="args-test",
+                command='echo "cmd name (arg 0)=$0, args:$@"',
+            )
+            """
+        ),
+    }
+    with setup_tmpdir(sources) as tmpdir:
+        args = [
+            "--backend-packages=pants.backend.shell",
+            f"--source-root-patterns=['{tmpdir}/src']",
+            "run",
+            f"{tmpdir}/src:args-test",
+            "--",
+        ] + [f"arg{i}" for i in range(1, 4)]
+        result = run_pants(args)
+        assert "arg1 arg2 arg3" in result.stdout.strip()
diff --git a/src/python/pants/backend/shell/util_rules/shell_command_test.py b/src/python/pants/backend/shell/util_rules/shell_command_test.py
--- a/src/python/pants/backend/shell/util_rules/shell_command_test.py
+++ b/src/python/pants/backend/shell/util_rules/shell_command_test.py
@@ -573,8 +573,11 @@ def assert_run_args(target: str, args: tuple[str, ...]) -> None:
         assert args[0] in request.args[0]
         assert request.args[1:] == args[1:]
 
-    assert_run_args("test", ("bash", "-c", "some cmd string"))
-    assert_run_args("cd-test", ("bash", "-c", "cd 'src/with space'\"'\"'n quote'; some cmd string"))
+    assert_run_args("test", ("bash", "-c", "some cmd string", "src:test"))
+    assert_run_args(
+        "cd-test",
+        ("bash", "-c", "cd 'src/with space'\"'\"'n quote'; some cmd string", "src:cd-test"),
+    )
 
 
 def test_shell_command_boot_script(rule_runner: RuleRunner) -> None:
@@ -608,6 +611,7 @@ def test_shell_command_boot_script(rule_runner: RuleRunner) -> None:
             'export PATH="$PWD/.bin";'
             "./command.script"
         )
+        + " src:boot-script-test"
     )
 
     tools = sorted({"python3_8", "mkdir", "ln"})
@@ -645,6 +649,7 @@ def test_shell_command_boot_script_in_build_root(rule_runner: RuleRunner) -> Non
             'export PATH="$PWD/.bin";'
             "./command.script"
         )
+        + " //:boot-script-test"
     )