Skip to content

fix(windows): run run= scripts with sh when available - #765

Draft
JamBalaya56562 wants to merge 1 commit into
jdx:mainfrom
JamBalaya56562:fix-windows-sh-run
Draft

fix(windows): run run= scripts with sh when available#765
JamBalaya56562 wants to merge 1 commit into
jdx:mainfrom
JamBalaya56562:fix-windows-sh-run

Conversation

@JamBalaya56562

@JamBalaya56562 JamBalaya56562 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Compatibility first

There is no (OS, PATH) combination where this is worse than today. The cmd /c path is preserved byte for byte and is still used whenever sh cannot be found, so a Windows machine without a POSIX shell behaves exactly as it does now — mount run="mise tasks --usage" and other plain command invocations keep working. Where sh is available, shebang mounts and POSIX one-liners start working instead of silently doing the wrong thing. Unix is untouched.

Problem

Executing a spec's run= is split across two nearly identical private functions:

Runs Windows
lib/src/sh.rs mount run= (via SpecCommand::mount) cmd /c
cli/src/cli/complete_word.rs:414 complete run= sh -c, no platform branch

Everything else about them is identical — stdin closed, stderr inherited, __USAGE set, same error handling. The cli copy exists only because lib/src/lib.rs declares pub(crate) mod sh. The result is that the same run= syntax runs under a different shell depending on which KDL node it was written in.

run= is a POSIX command line. The reference docs' own examples are ls modules/{{words[PREV]}}/controllers and echo {{ words | slice(start=-4) | join(...) }}. examples/mise.usage.kdl:1163 is mise alias ls {{words[PREV]}} | awk '{print $2}', and :1177-1215 is a multi-line case $cur in … esac with cut and sed. cmd /c cannot run any of that:

> cmd /c "printf 'cmd \"task-a\"\ncmd \"task-b\"\n'"
'printf' is not recognized as an internal or external command

Worse, for the shebang-script case it does not fail at all. cmd /c ./mounted.sh --mount hands the .sh to whatever program is registered for the extension — on my machine that opened an editor window — and then returns exit code 0 with empty stdout. That empty string goes straight to the spec parser, which accepts it as a spec with nothing in it. A mounted command silently completes to nothing.

cmd /c arrived in v2.16.0, "(windows) add Windows binaries and fix completion support (#472)". That PR was about shipping Windows binaries; which shell interprets run= does not look like it was the question being answered.

Fix

lib/src/sh.rs becomes the only implementation, exposed as usage::sh::sh (pub(crate) mod shpub mod sh; additive, cargo semver-checks check-release reports no semver update required). The cli copy is deleted and its call site now uses the shared one.

Shell selection: run sh -c; on Windows, if that fails with ErrorKind::NotFound, retry with cmd /c; if neither can start, return an explicit error naming both. Any other spawn failure is reported as-is rather than falling back — quietly demoting a broken sh to a different shell is the same class of silent wrongness this is meant to remove.

The platform branch lives in one pure function (fallback_for) behind cfg!(windows) rather than #[cfg(windows)], so the fallback logic is still compiled, linted and unit-tested on the Linux CI. On Unix it returns None, so the syscall sequence is identical to today's.

bash is deliberately not searched for. C:\Windows\System32\bash.exe is the WSL launcher and wins over PATH on any machine with WSL installed, which would run completion scripts against a different filesystem. Git for Windows ships sh.exe and bash.exe side by side, so looking for bash would not widen coverage anyway. No which-style probe either: a pre-flight spawn costs a process on the completion hot path and races the real call, while a failed CreateProcess already answers the question using the OS's own resolution rules.

Also fixed in passing: the cli copy hardcoded format!("sh -c {script}") as the error context, so Windows users were told about a shell that had not actually run.

Verified

Windows 11, sh on PATH via Git for Windows.

A mount run= holding a POSIX one-liner — the case cmd /c cannot run at all:

$ usage cw --shell fish -f mount-posix.usage.kdl mycli -- exec-task ""
task-a
task-b

cargo test -p usage-cli --test complete_word went from 82 seconds for a single test, with an editor window opening per fixture, to 2.7 seconds for the whole file with none. The eight mount tests still fail on this particular machine, but for an unrelated reason that this change makes visible instead of hiding: the fixtures are #!/usr/bin/env -S usage bash scripts, and usage bash resolves to WSL's bash here, which cannot open a C:/… path. Previously that surfaced as exit 0 and an empty spec; now it is exited with code 127 — sh -c mounted.sh --mount. On a Windows machine without WSL those eight would pass.

Also run: cargo test -p usage-lib --all-features (311 pass), cargo clippy --all --all-features -- -D warnings, cargo fmt --all -- --check, prettier -c on the touched docs.

Tests

lib/src/sh.rs gets its first unit tests. The shell-selection and message-building functions are pure, so they run on every platform — including that PermissionDenied does not fall back, and that the message names both shells and truncates a multi-line script to its first line.

cli/tests/complete_word.rs gains a skip_if_posix_shell_missing guard on the eight mount tests, mirroring skip_if_shell_missing in shell_completions_integration.rs including the panic! under CI. Those fixtures are shebang scripts and cannot work without a POSIX shell; without the guard, a Windows machine lacking one falls into the cmd /c path and gets an editor window per test rather than a failure. The Linux CI is unaffected — sh is always there, and a missing one under CI still fails loudly.

Deliberately not doing

  • Adding a Windows CI runner. test.yml is ubuntu-latest only, so the fallback's real behaviour is not covered here; that belongs to a separate CI discussion.
  • Adding a UsageErr variant for "no shell". That would be a major semver bump (as fix(parse): enforce double_dash="required" for positional args #762 is currently discovering), so the message rides on the existing XXError::Error.
  • A way to point usage at a specific shell binary. Worth having, but it is a design question rather than a bug fix.

Non-UTF-8 output (added after review)

String::from_utf8(...).expect(...) used to panic on non-UTF-8 stdout. I had this down as a follow-up, but it belongs here: the cmd /c fallback emits the console code page, which is not UTF-8 outside English locales, so on a Japanese or Cyrillic Windows a single run= script could take the whole process down. It is now an error naming the script and the shell that ran it.

Reported rather than converted lossily: a mount's output is parsed as a spec, and replacement characters there would resurface as a baffling KDL syntax error instead of an encoding one. No new UsageErr variant — the message rides on the existing XXError::Error, so the public API is unchanged.


This pull request was generated by Claude Code.

Summary by CodeRabbit

  • New Features

    • Shell-based commands now run consistently through POSIX shells, with a Windows fallback when unavailable.
    • Improved command execution diagnostics, multiline script handling, and shell integration.
    • Shell execution is now publicly available for broader integrations.
  • Documentation

    • Documented shell selection, fallback behavior, script limitations, input handling, and environment details.
  • Bug Fixes

    • Improved handling of shell startup failures while preserving meaningful command errors.
    • Shell-dependent tests now skip appropriately when the required shell is unavailable.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The shared sh helper is now public and handles POSIX shell execution with a Windows fallback. Completion commands use this helper. Shell-dependent tests and reference documentation cover the updated behavior.

Changes

Shell execution and completion

Layer / File(s) Summary
Public shell helper and fallback execution
lib/src/lib.rs, lib/src/sh.rs
The public sh helper prefers sh -c, uses cmd /c on Windows when sh is unavailable, preserves execution behavior, and adds fallback and diagnostic tests.
Completion integration and shell-dependent validation
cli/src/cli/complete_word.rs, cli/tests/complete_word.rs, docs/spec/reference/cmd.md, docs/spec/reference/complete.md
Completion commands use the shared helper. Shell-dependent tests handle missing sh. Documentation describes shell selection, fallback behavior, streams, limitations, and __USAGE.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Poem

A rabbit runs scripts through shells in the night,
sh -c hops first, then cmd /c in sight.
Completion follows the shared little trail,
Tests skip without sh, or warn when they fail.
Docs mark the path with a bright carrot sign.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: Windows run scripts now use sh when it is available.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/src/sh.rs`:
- Line 88: Update the stdout conversion in sh() to use a lossy UTF-8 conversion
instead of String::from_utf8(...).expect(...), so non-UTF-8 run= script output
is converted with replacement characters and does not panic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: c7cb8947-9c97-4803-bd01-40779eab582f

📥 Commits

Reviewing files that changed from the base of the PR and between 4b2fde7 and afe25b1.

📒 Files selected for processing (6)
  • cli/src/cli/complete_word.rs
  • cli/tests/complete_word.rs
  • docs/spec/reference/cmd.md
  • docs/spec/reference/complete.md
  • lib/src/lib.rs
  • lib/src/sh.rs

Comment thread lib/src/sh.rs Outdated
Executing a spec's `run=` was split across two nearly identical private
functions — `lib/src/sh.rs` for `mount run=`, and a copy in
`cli/src/cli/complete_word.rs` for `complete run=`. They differed in exactly
one respect: the lib copy used `cmd /c` on Windows, while the cli copy had no
platform branch at all and always used `sh -c`. The same `run=` syntax
therefore ran under a different shell depending on which KDL node it was
written in.

`run=` is a POSIX command line. The reference docs' own examples are `ls
modules/{{words[PREV]}}/controllers` and `echo {{ words | slice(...) }}`;
mise's spec uses `| awk`, and a multi-line `case ... esac` with `cut` and
`sed`. `cmd /c` cannot run any of it, and does not fail when it can't:
`cmd /c ./mounted.sh --mount` hands the `.sh` to whatever program is
registered for the extension, then returns exit code 0 with empty stdout,
which the spec parser accepts as an empty spec.

Both call sites now use `usage::sh::sh`. It runs `sh -c`, and on Windows
retries with `cmd /c` only when `sh` could not be found — so the `cmd /c`
path is preserved byte for byte for machines without a POSIX shell, and
`mount run="mise tasks --usage"` keeps working there. A shell that exists but
fails to start is reported rather than quietly demoted.

Output that is not valid UTF-8 is now an error rather than a panic. That
mattered less when this ran only under `sh`; the `cmd /c` fallback emits the
console code page, which is not UTF-8 outside English locales, so a single
`run=` script could take the process down.

The context string on process errors is also fixed: the cli copy hardcoded
`sh -c {script}`, so Windows users were told about a shell that had not run.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant