Skip to content

Commit f257d49

Browse files
ctruedenclaude
andcommitted
Encapsulate Windows exec logic in platform.command(exe)
Replace the old base_command() function (which always returned ["cmd.exe", "/c"] on Windows, regardless of whether that was appropriate) with a new command(exe) function that takes the executable into account and returns the full invocation list. This makes the "should I wrap in a shell?" decision live in platform where it belongs, ensures it can't be called without providing the executable, and eliminates the footgun that caused the parentheses bug. As per apposed/appose-java@4849c5a Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent 7d2e080 commit f257d49

File tree

2 files changed

+21
-17
lines changed

2 files changed

+21
-17
lines changed

src/appose/tool/__init__.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -288,17 +288,8 @@ def _do_exec(
288288
self._captured_output.clear()
289289
self._captured_error.clear()
290290

291-
# Build command.
292-
# On Windows, cmd.exe /c is needed for shell scripts and PATH resolution,
293-
# but absolute paths to native executables must be invoked directly:
294-
# cmd.exe treats parentheses and other characters as shell metacharacters,
295-
# so a path like C:\Users\foo\Fiji(1)\bin\pixi.exe would be misinterpreted.
296-
cmd = (
297-
[]
298-
if platform.is_windows() and Path(self.command).is_absolute()
299-
else platform.base_command()
300-
)
301-
cmd.append(self.command)
291+
# Build command
292+
cmd = platform.command(self.command)
302293
if include_flags:
303294
cmd.extend(self._flags)
304295
cmd.extend(args)

src/appose/util/platform.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,26 @@ def is_executable(file: Path) -> bool:
124124
return file.exists() and os.access(file, os.X_OK)
125125

126126

127-
def base_command() -> list[str]:
127+
def command(exe: str) -> list[str]:
128128
"""
129-
Get the arguments to prefix to execute a command in a separate process.
129+
Get the command list needed to invoke the given executable.
130+
131+
On Windows, absolute paths to native executables are invoked directly
132+
via subprocess, bypassing cmd.exe entirely. This is necessary because
133+
cmd.exe treats parentheses and other characters as shell metacharacters,
134+
which would break paths like C:\\Users\\foo\\Fiji(1)\\bin\\pixi.exe.
135+
Relative names (which need shell PATH resolution) still get
136+
['cmd.exe', '/c'] prepended.
137+
138+
Args:
139+
exe: The executable path or name to invoke.
130140
131141
Returns:
132-
['cmd.exe', '/c'] for Windows and an empty list otherwise.
142+
A list starting with the executable, preceded by ['cmd.exe', '/c']
143+
when needed.
133144
"""
134-
if is_windows():
135-
return ["cmd.exe", "/c"]
136-
return []
145+
from pathlib import Path
146+
147+
if is_windows() and not Path(exe).is_absolute():
148+
return ["cmd.exe", "/c", exe]
149+
return [exe]

0 commit comments

Comments
 (0)