Skip to content

Commit c35f291

Browse files
committed
handle executable paths with spaces in CLI arguments, close Effect-TS#5845 (Effect-TS#5853)
1 parent 5e7feeb commit c35f291

File tree

3 files changed

+37
-9
lines changed

3 files changed

+37
-9
lines changed

.changeset/cute-lizards-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect/cli": patch
3+
---
4+
5+
handle executable paths with spaces in CLI arguments

packages/cli/src/internal/cliApp.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ export const run = dual<
102102
InternalCommand.getHelp(e.command, config)
103103
),
104104
execute,
105-
config
105+
config,
106+
args
106107
)
107108
)
108109
: Option.none()
@@ -115,7 +116,7 @@ export const run = dual<
115116
})
116117
}
117118
case "BuiltIn": {
118-
return handleBuiltInOption(self, executable, filteredArgs, directive.option, execute, config).pipe(
119+
return handleBuiltInOption(self, executable, filteredArgs, directive.option, execute, config, args).pipe(
119120
Effect.catchSome((e) =>
120121
InternalValidationError.isValidationError(e)
121122
? Option.some(Effect.zipRight(printDocs(e.error), Effect.fail(e)))
@@ -151,7 +152,8 @@ const handleBuiltInOption = <R, E, A>(
151152
args: ReadonlyArray<string>,
152153
builtIn: BuiltInOptions.BuiltInOptions,
153154
execute: (a: A) => Effect.Effect<void, E, R>,
154-
config: CliConfig.CliConfig
155+
config: CliConfig.CliConfig,
156+
originalArgs: ReadonlyArray<string>
155157
): Effect.Effect<
156158
void,
157159
E | ValidationError.ValidationError,
@@ -161,7 +163,10 @@ const handleBuiltInOption = <R, E, A>(
161163
case "SetLogLevel": {
162164
console.log("\n========== ACTUAL TEST ==========")
163165
console.log("executable:", executable)
164-
console.log("Has spaces in paths?", executable.includes("Program Files") || executable.includes("Program Files (x86)"))
166+
console.log(
167+
"Has spaces in paths?",
168+
executable.includes("Program Files") || executable.includes("Program Files (x86)")
169+
)
165170

166171
const nextArgs = executable.split(/\s+/)
167172
console.log("After split:", nextArgs.length, "elements")
@@ -176,7 +181,7 @@ const handleBuiltInOption = <R, E, A>(
176181
if (isLogLevelArg(args[i]) || isLogLevelArg(args[i - 1])) {
177182
continue
178183
}
179-
nextArgs.push(args[i])
184+
filteredArgs.push(args[i])
180185
}
181186

182187
console.log("Final nextArgs length:", nextArgs.length)
@@ -285,10 +290,11 @@ const handleBuiltInOption = <R, E, A>(
285290
active: "yes",
286291
inactive: "no"
287292
}).pipe(Effect.flatMap((shouldRunCommand) => {
288-
const finalArgs = pipe(
289-
Arr.drop(args, 1),
290-
Arr.prependAll(executable.split(/\s+/))
291-
)
293+
// Use first 2 elements from originalArgs (runtime + script) to preserve paths with spaces
294+
// This mimics executable.split() behavior but without breaking Windows paths
295+
const baseArgs = Arr.take(originalArgs, 2)
296+
const wizardArgs = Arr.drop(args, 1)
297+
const finalArgs = Arr.appendAll(baseArgs, wizardArgs)
292298
return shouldRunCommand
293299
? Console.log().pipe(Effect.zipRight(run(self, finalArgs, execute)))
294300
: Effect.void

packages/cli/test/CliApp.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as Args from "@effect/cli/Args"
12
import type * as CliApp from "@effect/cli/CliApp"
23
import * as CliConfig from "@effect/cli/CliConfig"
34
import * as Command from "@effect/cli/Command"
@@ -113,5 +114,21 @@ describe("CliApp", () => {
113114
yield* cli(["node", "logging.js", "--log-level=debug"])
114115
expect(logLevel).toEqual(LogLevel.Debug)
115116
}).pipe(runEffect))
117+
118+
it("should handle paths with spaces when using --log-level", () =>
119+
Effect.gen(function*() {
120+
let executedValue: string | undefined = undefined
121+
const cmd = Command.make("test", { value: Args.text() }, ({ value }) =>
122+
Effect.sync(() => {
123+
executedValue = value
124+
}))
125+
const cli = Command.run(cmd, {
126+
name: "Test",
127+
version: "1.0.0"
128+
})
129+
// Simulate Windows path with spaces (e.g., "C:\Program Files\nodejs\node.exe")
130+
yield* cli(["C:\\Program Files\\node.exe", "C:\\My Scripts\\test.js", "--log-level", "info", "hello"])
131+
expect(executedValue).toEqual("hello")
132+
}).pipe(runEffect))
116133
})
117134
})

0 commit comments

Comments
 (0)