Skip to content

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

Merged
jdx merged 1 commit into
jdx:mainfrom
JamBalaya56562:fix-windows-sh-run
Aug 2, 2026
Merged

fix(windows): run run= scripts with sh when available#765
jdx merged 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.
    • Shell execution is available for broader integrations.
    • Command output, status, standard input, error output, and usage context remain supported.
  • Bug Fixes

    • Improved diagnostics for shell startup failures and multiline scripts.
    • Invalid command output is reported safely without unexpected crashes.
    • Shell-dependent tests now skip appropriately when the required shell is unavailable.
  • Documentation

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

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: e96bef65-8c9b-4950-9c28-dbbee60743b9

📥 Commits

Reviewing files that changed from the base of the PR and between 2f3c019 and bfef73d.

📒 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

📝 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 main Windows change: using sh for run scripts when 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
@JamBalaya56562
JamBalaya56562 marked this pull request as ready for review August 2, 2026 00:56
@greptile-apps

greptile-apps Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR centralizes run= execution in the library and prefers sh -c, with a Windows-only cmd /c fallback when sh is unavailable.

  • Reuses the shared shell runner for dynamic completions and mounts.
  • Reports non-UTF-8 shell output as an error rather than panicking.
  • Documents shell selection and adds shell-aware mount test guards.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported shell probe issue is fixed by checking that the probe exits successfully.

Important Files Changed

Filename Overview
lib/src/sh.rs Centralizes shell selection, Windows fallback, process errors, and UTF-8 validation with focused unit coverage.
cli/src/cli/complete_word.rs Removes the duplicate shell runner and delegates dynamic completion scripts to the shared library implementation.
cli/tests/complete_word.rs The revised probe now requires sh -c to exit successfully, resolving the previously reported exit-status issue.
lib/src/lib.rs Exposes the shared shell module publicly so the CLI can use the same implementation.
docs/spec/reference/cmd.md Documents shell behavior for mount commands consistently with the implementation.
docs/spec/reference/complete.md Documents POSIX execution, Windows fallback, and process environment behavior for completion scripts.

Reviews (3): Last reviewed commit: "fix(windows): run `run=` scripts with sh..." | Re-trigger Greptile

Comment thread cli/tests/complete_word.rs Outdated
@JamBalaya56562
JamBalaya56562 marked this pull request as draft August 2, 2026 00:59
@JamBalaya56562
JamBalaya56562 marked this pull request as ready for review August 2, 2026 01:04
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.
@JamBalaya56562

Copy link
Copy Markdown
Contributor Author

Good catch on the shell probe — fixed in bfef73d, and rebased onto main while I was there.

output().is_ok() only says a process was created, so an sh that starts and then fails read as a working POSIX shell. That is the case that matters here: on Windows sh can resolve to something that spawns and immediately errors, and the guard would have waved the mount tests through into exactly the confusing failure it exists to prevent. It now runs sh -c 'exit 0' and checks the exit status.

Verified all three paths on Linux, with a stub sh that always exits 3 placed ahead of the real one on PATH:

# broken sh
Skipping test - no usable POSIX shell (`sh`)
test complete_word_mounted ... ok

# broken sh, CI set
no usable POSIX shell (`sh`) but CI is set — refusing to skip
test result: FAILED

# real sh
test result: ok. 28 passed

Wording changed from "not on PATH" to "no usable POSIX shell", since the shell may now be present but rejected.

The same weakness exists in skip_if_shell_missing in cli/tests/shell_completions_integration.rs, which this PR does not touch — happy to fix it in a follow-up if you want it, though --version is a weaker probe to begin with and those tests fail loudly rather than opening editor windows.


This comment was generated by Claude Code.

@jdx
jdx merged commit 8f342a0 into jdx:main Aug 2, 2026
7 of 8 checks passed
@JamBalaya56562
JamBalaya56562 deleted the fix-windows-sh-run branch August 2, 2026 01:22
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.

2 participants