Repository: pantsbuild/pants

Issue:
`shell_command` `workdir=""` is documented as `"/"`, but behaves as `"."`
**Describe the bug**

The `workdir` field for `shell_command` is documented with the empty string behaving like `"/"` (use the build root), but actually behaves like `".`" (the default):

https://github.com/pantsbuild/pants/blob/a39c1b970be2cb4421b20728c21c3222aeb4b015/src/python/pants/backend/adhoc/target_types.py#L194-L208

This differs to `adhoc_tool`, which matches the documented behaviour.

Reproducer:

```shell
cat > pants.toml <<EOF
[GLOBAL]
pants_version = "2.16.0rc1"
backend_packages = ["pants.backend.experimental.adhoc", "pants.backend.shell"]

[anonymous-telemetry]
enabled = false
EOF

mkdir -p dir/

cat > dir/BUILD <<EOF
shell_command(
    name="shell",
    command="echo \$PWD",
    workdir="",
    log_output=True,
)

system_binary(name="bash", binary_name="bash")
adhoc_tool(
    name="adhoc",
    runnable=":bash",
    args=["-c", "echo \$PWD"],
    log_output=True,
    workdir=""
)
EOF

pants --no-pantsd export-codegen dir:shell
# BUG: .../pants-sandbox-ZvPaba/dir
pants --no-pantsd export-codegen dir:adhoc
# OK: .../pants-sandbox-LcHTdf
```

Running that gives output like:

```
21:36:26.45 [INFO] /private/var/folders/sv/vd266m4d4lvctgs2wpnhjs9w0000gn/T/pants-sandbox-rWkfVJ/dir

21:36:26.45 [INFO] Writing generated files to dist/codegen
21:36:30.33 [INFO] /private/var/folders/sv/vd266m4d4lvctgs2wpnhjs9w0000gn/T/pants-sandbox-LcHTdf

21:36:30.33 [INFO] Writing generated files to dist/codegen
```

Note the `/dir` at the end of the path for the first invocation (`shell`): it should match the second (`adhoc`), and just be the raw sandbox path.


**Pants version**
2.16.0rc1

**OS**
macOS

**Additional info**
N/A


Discussion:

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
@@ -76,9 +76,7 @@ async def _prepare_process_request_from_target(
     description = f"the `{shell_command.alias}` at `{shell_command.address}`"
 
     working_directory = shell_command[ShellCommandWorkdirField].value
-
-    if not working_directory:
-        working_directory = "."
+    assert working_directory is not None, "working_directory should always be a string"
 
     command = shell_command[ShellCommandCommandField].value
     if not command:
@@ -241,9 +239,8 @@ async def _interactive_shell_command(
     )
     dependencies_digest = execution_environment.digest
 
-    _working_directory = working_directory or "."
     relpath = os.path.relpath(
-        _working_directory, start="/" if os.path.isabs(_working_directory) else "."
+        working_directory, start="/" if os.path.isabs(working_directory) else "."
     )
     boot_script = f"cd {shlex.quote(relpath)}; " if relpath != "." else ""
 


Test patch:
diff --git a/src/python/pants/backend/adhoc/adhoc_tool_test.py b/src/python/pants/backend/adhoc/adhoc_tool_test.py
--- a/src/python/pants/backend/adhoc/adhoc_tool_test.py
+++ b/src/python/pants/backend/adhoc/adhoc_tool_test.py
@@ -3,7 +3,6 @@
 
 from __future__ import annotations
 
-import os
 from textwrap import dedent
 
 import pytest
@@ -183,40 +182,37 @@ def test_adhoc_tool_capture_stdout_err(rule_runner: PythonRuleRunner) -> None:
 
 
 @pytest.mark.parametrize(
-    ("workdir", "file_location"),
+    ("workdir", "expected_dir"),
     (
-        ("src", "src"),
-        (".", "src"),
-        ("./", "src"),
+        ("src", "/src"),
+        (".", "/src"),
+        ("./", "/src"),
+        ("./dst", "/src/dst"),
         ("/", ""),
         ("", ""),
-        ("/src", "src"),
+        ("/src", "/src"),
+        ("/dst", "/dst"),
+        (None, "/src"),
     ),
 )
 def test_working_directory_special_values(
-    rule_runner: PythonRuleRunner, workdir: str, file_location: str
+    rule_runner: PythonRuleRunner, workdir: str, expected_dir: str
 ) -> None:
     rule_runner.write_files(
         {
-            "src/fruitcake.py": dedent(
-                """\
-                f = open("fruitcake.txt", "w")
-                f.write("fruitcake\\n")
-                f.close()
-                """
-            ),
             "src/BUILD": dedent(
                 f"""\
-                python_source(
-                    source="fruitcake.py",
-                    name="fruitcake",
-                )
+                system_binary(name="bash", binary_name="bash")
+                system_binary(name="sed", binary_name="sed", fingerprint_args=["q"])
 
                 adhoc_tool(
-                  name="run_fruitcake",
-                  runnable=":fruitcake",
-                  output_files=["fruitcake.txt"],
-                  workdir="{workdir}",
+                  name="workdir",
+                  runnable=":bash",
+                  args=['-c', 'echo $PWD | sed s@^{{chroot}}@@ > out.log'],
+                  runnable_dependencies=[":sed"],
+                  workdir={workdir!r},
+                  output_files=["out.log"],
+                  root_output_directory=".",
                 )
                 """
             ),
@@ -225,8 +221,8 @@ def test_working_directory_special_values(
 
     assert_adhoc_tool_result(
         rule_runner,
-        Address("src", target_name="run_fruitcake"),
-        expected_contents={os.path.join(file_location, "fruitcake.txt"): "fruitcake\n"},
+        Address("src", target_name="workdir"),
+        expected_contents={"out.log": f"{expected_dir}\n"},
     )
 
 
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
@@ -789,3 +789,47 @@ def test_env_vars(rule_runner: RuleRunner) -> None:
         Address("src", target_name="envvars"),
         expected_contents={"out.log": f"{envvar_value}\n"},
     )
+
+
+_DEFAULT = object()
+
+
+@pytest.mark.parametrize(
+    ("workdir", "expected_dir"),
+    [
+        ("src", "/src"),
+        (".", "/src"),
+        ("./", "/src"),
+        ("./dst", "/src/dst"),
+        ("/", ""),
+        ("", ""),
+        ("/src", "/src"),
+        ("/dst", "/dst"),
+        (None, "/src"),
+    ],
+)
+def test_working_directory_special_values(
+    rule_runner: RuleRunner, workdir: str | None, expected_dir: str
+) -> None:
+    rule_runner.write_files(
+        {
+            "src/BUILD": dedent(
+                f"""\
+                shell_command(
+                  name="workdir",
+                  tools=['sed'],
+                  command='echo $PWD | sed s@^{{chroot}}@@ > out.log',
+                  workdir={workdir!r},
+                  output_files=["out.log"],
+                  root_output_directory=".",
+                )
+                """
+            ),
+        }
+    )
+
+    assert_shell_command_result(
+        rule_runner,
+        Address("src", target_name="workdir"),
+        expected_contents={"out.log": f"{expected_dir}\n"},
+    )