Add `run_shell` argument handling tests (#62)
diff --git a/tests/run_shell/BUILD b/tests/run_shell/BUILD
new file mode 100644
index 0000000..71b8d92
--- /dev/null
+++ b/tests/run_shell/BUILD
@@ -0,0 +1,155 @@
+load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
+load(
+    ":exec_fixtures.bzl",
+    "run_shell_helper_script_args",
+    "run_shell_long_output",
+    "run_shell_output",
+    "run_shell_output_with_action_env",
+    "run_shell_script_args",
+)
+load(":run_shell_tests.bzl", "run_shell_test_suite")
+
+run_shell_test_suite(name = "run_shell_tests")
+
+platform(
+    name = "linux_exec",
+    constraint_values = ["@platforms//os:linux"],
+)
+
+platform(
+    name = "windows_exec",
+    constraint_values = ["@platforms//os:windows"],
+)
+
+run_shell_output(
+    name = "basic_output",
+    command = "echo 'hello world' > $1",
+)
+
+diff_test(
+    name = "basic_output_test",
+    size = "small",
+    file1 = ":basic_output",
+    file2 = "basic_output.golden",
+)
+
+run_shell_output(
+    name = "positional_arguments",
+    command = "echo \"$2 $3\" > $1",
+    extra_arguments = [
+        "foo",
+        "bar",
+    ],
+)
+
+diff_test(
+    name = "positional_arguments_test",
+    size = "small",
+    file1 = ":positional_arguments",
+    file2 = "positional_arguments.golden",
+)
+
+run_shell_output(
+    name = "env",
+    command = "echo \"$FOO\" > $1",
+    env = {"FOO": "bar"},
+)
+
+diff_test(
+    name = "env_test",
+    size = "small",
+    file1 = ":env",
+    file2 = "env.golden",
+)
+
+# positional parameters of the command string, not of the script it runs.
+# Codifies https://github.com/bazelbuild/bazel/issues/6391 (working as intended).
+run_shell_script_args(
+    name = "script_args_not_forwarded",
+    forward_arguments = False,
+)
+
+diff_test(
+    name = "script_args_not_forwarded_test",
+    size = "small",
+    file1 = ":script_args_not_forwarded",
+    file2 = "script_args_not_forwarded.golden",
+)
+
+# Explicitly forwarding with "$@" passes `arguments` through to the script.
+run_shell_script_args(
+    name = "script_args_forwarded",
+    forward_arguments = True,
+)
+
+diff_test(
+    name = "script_args_forwarded_test",
+    size = "small",
+    file1 = ":script_args_forwarded",
+    file2 = "script_args_forwarded.golden",
+)
+
+# A command referencing $1 receives `arguments` when it runs inline...
+run_shell_helper_script_args(
+    name = "helper_script_args_inline",
+    long = False,
+)
+
+diff_test(
+    name = "helper_script_args_inline_test",
+    size = "small",
+    file1 = ":helper_script_args_inline",
+    file2 = "helper_script_args_inline.golden",
+)
+
+# ...but the same command spilled into a helper script does not: $1 is empty.
+# Codifies https://github.com/bazelbuild/bazel/issues/29365 (currently open bug).
+run_shell_helper_script_args(
+    name = "helper_script_args_spilled",
+    long = True,
+)
+
+diff_test(
+    name = "helper_script_args_spilled_test",
+    size = "small",
+    file1 = ":helper_script_args_spilled",
+    file2 = "helper_script_args_spilled.golden",
+)
+
+run_shell_long_output(
+    name = "long_command",
+)
+
+diff_test(
+    name = "long_command_test",
+    size = "small",
+    file1 = ":long_command",
+    file2 = "long_command.golden",
+)
+
+run_shell_output_with_action_env(
+    name = "use_default_shell_env",
+    command = "echo \"$FROM_ACTION_ENV\" > $1",
+    use_default_shell_env = True,
+)
+
+diff_test(
+    name = "use_default_shell_env_test",
+    size = "small",
+    file1 = ":use_default_shell_env",
+    file2 = "use_default_shell_env.golden",
+)
+
+run_shell_output_with_action_env(
+    name = "fixed_env_overrides_action_env",
+    command = "echo \"$FROM_ACTION_ENV\" > $1",
+    env = {"FROM_ACTION_ENV": "fixed_env_value"},
+    use_default_shell_env = True,
+)
+
+diff_test(
+    name = "fixed_env_overrides_action_env_test",
+    size = "small",
+    file1 = ":fixed_env_overrides_action_env",
+    file2 = "fixed_env_overrides_action_env.golden",
+)
diff --git a/tests/run_shell/BUILD.bazel b/tests/run_shell/BUILD.bazel
deleted file mode 100644
index 3fb5064..0000000
--- a/tests/run_shell/BUILD.bazel
+++ /dev/null
@@ -1,99 +0,0 @@
-load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
-load(
-    ":exec_fixtures.bzl",
-    "run_shell_long_output",
-    "run_shell_output",
-    "run_shell_output_with_action_env",
-)
-load(":run_shell_tests.bzl", "run_shell_test_suite")
-
-run_shell_test_suite(name = "run_shell_tests")
-
-platform(
-    name = "linux_exec",
-    constraint_values = ["@platforms//os:linux"],
-)
-
-platform(
-    name = "windows_exec",
-    constraint_values = ["@platforms//os:windows"],
-)
-
-run_shell_output(
-    name = "basic_output",
-    command = "echo 'hello world' > $1",
-)
-
-diff_test(
-    name = "basic_output_test",
-    size = "small",
-    file1 = ":basic_output",
-    file2 = "basic_output.golden",
-)
-
-run_shell_output(
-    name = "positional_arguments",
-    command = "echo \"$2 $3\" > $1",
-    extra_arguments = [
-        "foo",
-        "bar",
-    ],
-)
-
-diff_test(
-    name = "positional_arguments_test",
-    size = "small",
-    file1 = ":positional_arguments",
-    file2 = "positional_arguments.golden",
-)
-
-run_shell_output(
-    name = "env",
-    command = "echo \"$FOO\" > $1",
-    env = {"FOO": "bar"},
-)
-
-diff_test(
-    name = "env_test",
-    size = "small",
-    file1 = ":env",
-    file2 = "env.golden",
-)
-
-run_shell_long_output(
-    name = "long_command",
-)
-
-diff_test(
-    name = "long_command_test",
-    size = "small",
-    file1 = ":long_command",
-    file2 = "long_command.golden",
-)
-
-run_shell_output_with_action_env(
-    name = "use_default_shell_env",
-    command = "echo \"$FROM_ACTION_ENV\" > $1",
-    use_default_shell_env = True,
-)
-
-diff_test(
-    name = "use_default_shell_env_test",
-    size = "small",
-    file1 = ":use_default_shell_env",
-    file2 = "use_default_shell_env.golden",
-)
-
-run_shell_output_with_action_env(
-    name = "fixed_env_overrides_action_env",
-    command = "echo \"$FROM_ACTION_ENV\" > $1",
-    env = {"FROM_ACTION_ENV": "fixed_env_value"},
-    use_default_shell_env = True,
-)
-
-diff_test(
-    name = "fixed_env_overrides_action_env_test",
-    size = "small",
-    file1 = ":fixed_env_overrides_action_env",
-    file2 = "fixed_env_overrides_action_env.golden",
-)
diff --git a/tests/run_shell/exec_fixtures.bzl b/tests/run_shell/exec_fixtures.bzl
index 71943df..64a4b18 100644
--- a/tests/run_shell/exec_fixtures.bzl
+++ b/tests/run_shell/exec_fixtures.bzl
@@ -40,6 +40,68 @@
         .build()
 )
 
+def _run_shell_script_args_impl(ctx):
+    out = ctx.actions.declare_file(ctx.label.name + ".out")
+
+    # A script that records its own positional arguments.
+    script = ctx.actions.declare_file(ctx.label.name + "_echo_args.sh")
+    ctx.actions.write(
+        output = script,
+        content = """#!/bin/bash
+set -euo pipefail
+echo "args=($*)" > "$OUT"
+""",
+        is_executable = True,
+    )
+
+    # `arguments` are passed as the positional parameters ($1, $2, ...) of the
+    # command string (with an empty $0), not to a script named within it.
+    command = script.path
+    if ctx.attr.forward_arguments:
+        command += " \"$@\""
+
+    ctx.actions.run_shell(
+        outputs = [out],
+        tools = [script],
+        command = command,
+        arguments = ["a", "b", "c"],
+        env = {"OUT": out.path},
+        mnemonic = "RunShellScriptArgs",
+    )
+    return [DefaultInfo(files = depset([out]))]
+
+run_shell_script_args = rule(
+    implementation = _run_shell_script_args_impl,
+    attrs = {
+        "forward_arguments": attr.bool(),
+    },
+)
+
+def _run_shell_helper_script_args_impl(ctx):
+    out = ctx.actions.declare_file(ctx.label.name + ".out")
+
+    command = 'echo "arg=$1" > "$OUT"'
+    if ctx.attr.long:
+        # Pad past the spill threshold with a trailing comment so the command is
+        # written to a helper script without otherwise changing its behavior.
+        command += " #" + ("x" * 70000)
+
+    ctx.actions.run_shell(
+        outputs = [out],
+        command = command,
+        arguments = ["the_argument"],
+        env = {"OUT": out.path},
+        mnemonic = "RunShellHelperScriptArgs",
+    )
+    return [DefaultInfo(files = depset([out]))]
+
+run_shell_helper_script_args = rule(
+    implementation = _run_shell_helper_script_args_impl,
+    attrs = {
+        "long": attr.bool(),
+    },
+)
+
 def _run_shell_long_output_impl(ctx):
     out = ctx.actions.declare_file(ctx.label.name + ".out")
 
diff --git a/tests/run_shell/helper_script_args_inline.golden b/tests/run_shell/helper_script_args_inline.golden
new file mode 100644
index 0000000..43a1696
--- /dev/null
+++ b/tests/run_shell/helper_script_args_inline.golden
@@ -0,0 +1 @@
+arg=the_argument
diff --git a/tests/run_shell/helper_script_args_spilled.golden b/tests/run_shell/helper_script_args_spilled.golden
new file mode 100644
index 0000000..474b415
--- /dev/null
+++ b/tests/run_shell/helper_script_args_spilled.golden
@@ -0,0 +1 @@
+arg=
diff --git a/tests/run_shell/script_args_forwarded.golden b/tests/run_shell/script_args_forwarded.golden
new file mode 100644
index 0000000..a4107e3
--- /dev/null
+++ b/tests/run_shell/script_args_forwarded.golden
@@ -0,0 +1 @@
+args=(a b c)
diff --git a/tests/run_shell/script_args_not_forwarded.golden b/tests/run_shell/script_args_not_forwarded.golden
new file mode 100644
index 0000000..210a2fc
--- /dev/null
+++ b/tests/run_shell/script_args_not_forwarded.golden
@@ -0,0 +1 @@
+args=()