Skip to content

Commit 1b23741

Browse files
authored
handle executable paths with spaces in CLI arguments, close #5845 (#5853)
1 parent fb78c40 commit 1b23741

File tree

3 files changed

+38
-10
lines changed

3 files changed

+38
-10
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: 16 additions & 10 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,22 +152,26 @@ 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,
158160
R | CliApp.CliApp.Environment | Terminal.Terminal
159161
> => {
160162
switch (builtIn._tag) {
161163
case "SetLogLevel": {
162-
const nextArgs = executable.split(/\s+/)
163-
// Filter out the log level option before re-executing the command
164+
// Use first 2 elements from originalArgs (runtime + script) to preserve paths with spaces
165+
// Filter out --log-level from args before re-executing
166+
const baseArgs = Arr.take(originalArgs, 2)
167+
const filteredArgs: Array<string> = []
164168
for (let i = 0; i < args.length; i++) {
165169
if (isLogLevelArg(args[i]) || isLogLevelArg(args[i - 1])) {
166170
continue
167171
}
168-
nextArgs.push(args[i])
172+
filteredArgs.push(args[i])
169173
}
174+
const nextArgs = Arr.appendAll(baseArgs, filteredArgs)
170175
return run(self, nextArgs, execute).pipe(
171176
Logger.withMinimumLogLevel(builtIn.level)
172177
)
@@ -269,10 +274,11 @@ const handleBuiltInOption = <R, E, A>(
269274
active: "yes",
270275
inactive: "no"
271276
}).pipe(Effect.flatMap((shouldRunCommand) => {
272-
const finalArgs = pipe(
273-
Arr.drop(args, 1),
274-
Arr.prependAll(executable.split(/\s+/))
275-
)
277+
// Use first 2 elements from originalArgs (runtime + script) to preserve paths with spaces
278+
// This mimics executable.split() behavior but without breaking Windows paths
279+
const baseArgs = Arr.take(originalArgs, 2)
280+
const wizardArgs = Arr.drop(args, 1)
281+
const finalArgs = Arr.appendAll(baseArgs, wizardArgs)
276282
return shouldRunCommand
277283
? Console.log().pipe(Effect.zipRight(run(self, finalArgs, execute)))
278284
: 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)