|
6 | 6 | "fmt" |
7 | 7 | "os" |
8 | 8 | "os/exec" |
| 9 | + "strings" |
9 | 10 |
|
10 | 11 | "github.com/amterp/rad/rts/rl" |
11 | 12 | ) |
@@ -197,33 +198,84 @@ func realShellExecutor(invocation ShellInvocation) (string, string, int) { |
197 | 198 | return stdout, stderr, exitCode |
198 | 199 | } |
199 | 200 |
|
| 201 | +// resolveCmdSimple resolves the shell to use for the given command string and |
| 202 | +// returns a prepared *exec.Cmd. Panics with a user-facing message if no shell |
| 203 | +// can be found. |
200 | 204 | func resolveCmdSimple(cmdStr string) *exec.Cmd { |
201 | | - if shell := os.Getenv("SHELL"); shell != "" { |
202 | | - return buildCmd(shell, cmdStr) |
| 205 | + path, flag, err := resolveShell(os.Getenv, exec.LookPath, IsWindows()) |
| 206 | + if err != nil { |
| 207 | + panic(err.Error()) |
203 | 208 | } |
| 209 | + return buildCmd(path, flag, cmdStr) |
| 210 | +} |
204 | 211 |
|
205 | | - if _, err := exec.LookPath("/bin/sh"); err == nil { |
206 | | - return buildCmd("/bin/sh", cmdStr) |
| 212 | +// resolveShell picks a shell to use for executing a command string. It is a |
| 213 | +// pure function: all platform/env dependencies are passed in so it is testable |
| 214 | +// without mutating global state. |
| 215 | +// |
| 216 | +// Resolution order: |
| 217 | +// 1. SHELL env var if set, but on Windows only if it actually resolves to an |
| 218 | +// executable. Git Bash / MSYS2 / Cygwin set SHELL to a Unix-style path |
| 219 | +// (e.g. /usr/bin/bash) that native Win32 exec can't find, so on Windows |
| 220 | +// we fall through to the candidate chain in that case rather than crash. |
| 221 | +// 2. Windows: pwsh.exe -> powershell.exe -> cmd.exe |
| 222 | +// 3. Other: /bin/sh |
| 223 | +// |
| 224 | +// Returns the resolved shell path and the flag to use for command-string |
| 225 | +// invocation (e.g. "-c" for POSIX shells and PowerShell, "/c" for cmd.exe). |
| 226 | +func resolveShell( |
| 227 | + getEnv func(string) string, |
| 228 | + lookPath func(string) (string, error), |
| 229 | + isWindows bool, |
| 230 | +) (path, flag string, err error) { |
| 231 | + if shell := strings.TrimSpace(getEnv("SHELL")); shell != "" { |
| 232 | + if !isWindows { |
| 233 | + return shell, shellExecFlag(shell), nil |
| 234 | + } |
| 235 | + // On Windows, only honor SHELL if it actually resolves - otherwise |
| 236 | + // fall through. This rescues the common Git Bash case where SHELL is |
| 237 | + // set to /usr/bin/bash but the native Win32 binary can't see it. |
| 238 | + if resolved, lookErr := lookPath(shell); lookErr == nil { |
| 239 | + return resolved, shellExecFlag(resolved), nil |
| 240 | + } |
207 | 241 | } |
208 | 242 |
|
209 | | - panic("Cannot run shell cmd as no shell found. Please set the SHELL environment variable.") |
210 | | -} |
211 | | - |
212 | | -func resolveCmd(i *Interpreter, shellNode rl.Node, cmdStr string) *exec.Cmd { |
213 | | - if shell := os.Getenv("SHELL"); shell != "" { |
214 | | - return buildCmd(shell, cmdStr) |
| 243 | + var candidates []string |
| 244 | + if isWindows { |
| 245 | + candidates = []string{"pwsh.exe", "powershell.exe", "cmd.exe"} |
| 246 | + } else { |
| 247 | + candidates = []string{"/bin/sh"} |
215 | 248 | } |
216 | 249 |
|
217 | | - if _, err := exec.LookPath("/bin/sh"); err == nil { |
218 | | - return buildCmd("/bin/sh", cmdStr) |
| 250 | + for _, c := range candidates { |
| 251 | + if resolved, lookErr := lookPath(c); lookErr == nil { |
| 252 | + return resolved, shellExecFlag(resolved), nil |
| 253 | + } |
219 | 254 | } |
220 | 255 |
|
221 | | - i.emitError(rl.ErrGenericRuntime, shellNode, "Cannot run shell cmd as no shell found. Please set the SHELL environment variable") |
222 | | - panic(UNREACHABLE) |
| 256 | + return "", "", errors.New("Cannot run shell cmd as no shell found. Please set the SHELL environment variable") |
| 257 | +} |
| 258 | + |
| 259 | +// shellExecFlag returns the flag a given shell expects for invoking a command |
| 260 | +// string. Defaults to "-c" (POSIX shells, bash/zsh, and PowerShell which |
| 261 | +// accepts "-c" as a short form of "-Command"). Only cmd.exe needs "/c". |
| 262 | +// |
| 263 | +// We don't use filepath.Base because its separator handling is GOOS-specific |
| 264 | +// (only "/" on Unix), which would mis-handle Windows-style paths that may |
| 265 | +// arrive via env vars or mixed environments. |
| 266 | +func shellExecFlag(shellPath string) string { |
| 267 | + if i := strings.LastIndexAny(shellPath, `/\`); i >= 0 { |
| 268 | + shellPath = shellPath[i+1:] |
| 269 | + } |
| 270 | + base := strings.TrimSuffix(strings.ToLower(shellPath), ".exe") |
| 271 | + if base == "cmd" { |
| 272 | + return "/c" |
| 273 | + } |
| 274 | + return "-c" |
223 | 275 | } |
224 | 276 |
|
225 | | -func buildCmd(shellStr string, cmdStr string) *exec.Cmd { |
226 | | - cmd := exec.Command(shellStr, "-c", cmdStr) |
| 277 | +func buildCmd(shellStr string, flag string, cmdStr string) *exec.Cmd { |
| 278 | + cmd := exec.Command(shellStr, flag, cmdStr) |
227 | 279 | cmd.Stdin = RIo.StdIn.Unwrap() |
228 | 280 | return cmd |
229 | 281 | } |
|
0 commit comments