Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions e2e-win/task.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,44 @@ esac
Remove-Item -Path "$TestDrive\mise.cygdrive_repro.toml" -ErrorAction SilentlyContinue
}
}

It 'forwards args to a bash subshell task without shifting $0' {
# Repro for the Windows non-cmd POSIX-shell arg-swallow bug (#9355): with
# shell = "bash -c", a forwarded arg used to be passed as a separate argv
# to `bash -c`, so the user's first arg became $0. Inline TOML scripts
# append args to the command (like Unix), so $0 stays the shell (bash) and
# the arg is appended after it — not `using shell myarg`, where the arg had
# been swallowed into $0.
if (-not (Get-Command bash.exe -ErrorAction SilentlyContinue)) {
Set-ItResult -Skipped -Because "bash.exe (Git Bash / MSYS) not on PATH"
return
}

@'
[tasks.args_repro]
shell = "bash -c"
run = 'echo "using shell $0"'
'@ | Out-File -FilePath "mise.args_repro.toml" -Encoding utf8NoBOM

$oldConfig = $env:MISE_CONFIG_FILE
$env:MISE_CONFIG_FILE = "$TestDrive\mise.args_repro.toml"
try {
# $0 is the shell bash was invoked as: "bash" on some setups, a full
# path like "/usr/bin/bash" on Git Bash. Assert on the shape that
# proves the fix regardless of that form — $0 still names bash (not
# the forwarded arg) and "myarg" is appended as the trailing token,
# rather than being swallowed into $0 (the old bug printed
# "using shell myarg").
$output = mise run args_repro -- myarg 2>&1 | Select -Last 1
$output | Should -BeLike '*bash* myarg'
}
finally {
if ($null -eq $oldConfig) {
Remove-Item -Path Env:\MISE_CONFIG_FILE -ErrorAction SilentlyContinue
} else {
$env:MISE_CONFIG_FILE = $oldConfig
}
Remove-Item -Path "$TestDrive\mise.args_repro.toml" -ErrorAction SilentlyContinue
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
}
}
4 changes: 4 additions & 0 deletions e2e/tasks/test_task_shell
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ run = 'echo "using shell $0"'
EOF

assert "mise run shell" "using shell bash"
# A forwarded arg is appended to the inline command, so it does NOT shift the
# shell's $0 (which stays "bash"); before the Windows fix, `bash -c <script> arg`
# made the first arg $0. See #9355.
assert "mise run shell -- myarg" "using shell bash myarg"
assert_fail "mise run shell-invalid" "invalid shell"

cat <<EOF >mise.toml
Expand Down
28 changes: 19 additions & 9 deletions src/task/task_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -910,18 +910,28 @@ impl TaskExecutor {
let cmd_args = crate::path::cmd_verbatim_args(&shell[1..], script, args);
return Ok((shell[0].clone(), cmd_args, true));
}
full_args.push(script.to_string());
full_args.extend(args.iter().cloned());
// Non-POSIX, non-cmd shells (e.g. `pwsh -Command`) use a different
// quoting convention than `shell_words` (which is POSIX), so keep
// passing their forwarded args as separate argv. Only POSIX shells
// (`bash -c`, `sh -c`, …) fall through to the shared append below.
if !crate::path::is_posix_shell_program(Path::new(&shell[0])) {
full_args.push(script.to_string());
full_args.extend(args.iter().cloned());
return Ok((full_args[0].clone(), full_args[1..].to_vec(), false));
}
}

#[cfg(unix)]
{
let mut script = script.to_string();
if !args.is_empty() {
script = format!("{script} {}", shell_words::join(args));
}
full_args.push(script);
// Shared (Unix, and Windows POSIX shells like `bash -c`): append the
// forwarded args to the command string so they reach an inline `-c` shell
// as part of the command — the documented behavior for inline TOML
// scripts — rather than as positional parameters. Passing them as separate
// argv to `bash -c` on Windows shifted the user's first arg into `$0`.
// See #9355.
let mut script = script.to_string();
if !args.is_empty() {
script = format!("{script} {}", shell_words::join(args));
}
full_args.push(script);
Ok((full_args[0].clone(), full_args[1..].to_vec(), false))
}

Expand Down
Loading