fix(cli): let generate markdown write to stdout - #766
Conversation
📝 WalkthroughWalkthroughFig, manpage, and Markdown generation now share stdout and file output handling. Markdown single-file output defaults to stdout. Progress messages use stderr, with tests and documentation updated. ChangesGenerator output handling
Estimated code review effort: 2 (Simple) | ~10 minutes Sequence Diagram(s)sequenceDiagram
participant GeneratorCommand
participant write_or_stdout
participant stdout
participant OutputFile
GeneratorCommand->>write_or_stdout: pass generated contents and output path
write_or_stdout->>stdout: write when path is missing or "-"
write_or_stdout->>OutputFile: write when path is a file
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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. Comment |
Greptile SummaryThis PR adds portable stdout output for Markdown, manpage, and Fig generation while routing progress messages to stderr.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported stdout panic path now uses fallible writes, treats a closed downstream pipe as normal termination, and propagates other output errors. Important Files Changed
Reviews (2): Last reviewed commit: "fix(cli): let generate markdown write to..." | Re-trigger Greptile |
`generate markdown` was the only generator with no way to reach stdout: `--out-file` was required and nothing else emitted the document. The workaround people reach for is `--out-file /dev/stdout`, which is a device file on unix but just a relative-looking path on Windows — `xx::file::write` creates the parent directory first, so it leaves a real `C:\dev\stdout` behind. This repo's own test suite did exactly that, and the resulting snapshot had a stray `writing to /dev/stdout` line baked into its first line. `--out-file` is now optional on markdown and defaults to stdout, matching manpage, fig, json and completion. `-` also means stdout on markdown, manpage and fig, mirroring the `-f -` convention already used for reading a spec from stdin; a file literally named `-` is spelled `./-`. Both spellings go through one `write_or_stdout` helper sitting next to `parse_file_or_stdin`. That helper writes with `write_all` rather than `print!`, which panics when the write fails. These documents outlast a pipe buffer — 100 KB for mise's spec — so `usage g markdown -f mise.usage.kdl --out-file - | head -1` ended in `failed printing to stdout … (os error 109)` and exit 101. A reader that closed early is not reported: Rust ignores SIGPIPE, so what ends the process silently in C arrives as an ordinary write error. The `writing to ...` progress line moved from stdout to stderr on markdown, manpage, fig and sdk. On stdout it lands inside the document as soon as the document itself goes to stdout, which is what the snapshot was recording. Dropping the `--out-file` requirement would have made `--out-dir` without `--multi` silently print to stdout and ignore the directory, so `--out-dir` now requires `--multi`. `/dev/stdout` is deliberately not special-cased on Windows: it is a unix fd path, and emulating it honestly pulls in `/dev/stderr`, `/dev/null` and `/dev/fd/N` too. `-` is the portable spelling and is now in the help text.
463923f to
085af83
Compare
⚠️ **CAUTION: this is a major update, indicating a breaking change!**⚠️ This MR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [usage](https://github.com/jdx/usage) | tools | major | `3.5.6` → `5.1.0` | MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot). **Proposed changes to behavior should be submitted there as MRs.** --- ### Release Notes <details> <summary>jdx/usage (usage)</summary> ### [`v5.1.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#510---2026-08-09) [Compare Source](jdx/usage@v5.0.0...v5.1.0) ##### 🚀 Features - **(spec)** parse usage comments from strings by [@​jdx](https://github.com/jdx) in [#​782](jdx/usage#782) ##### 🐛 Bug Fixes - **(spec)** avoid inferred metadata from included specs by [@​jdx](https://github.com/jdx) in [#​786](jdx/usage#786) ##### 🧪 Testing - **(windows)** make the suite runnable on Windows by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​771](jdx/usage#771) ##### 📦️ Dependency Updates - update rust crate rmcp to v3 by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​780](jdx/usage#780) ### [`v5.0.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#500---2026-08-02) [Compare Source](jdx/usage@v4.1.0...v5.0.0) ##### 🚀 Features - **(cli)** allow overriding the shell program with USAGE\_SHELL\_<SHELL> by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​767](jdx/usage#767) ##### 🐛 Bug Fixes - **(cli)** forward parsed args to WSL bash via WSLENV on windows by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​764](jdx/usage#764) - **(cli)** let generate markdown write to stdout by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​766](jdx/usage#766) - **(complete)** use `type -P` so the CLI-presence guard ignores shell functions by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​760](jdx/usage#760) - **(parse)** enforce double\_dash="required" for positional args by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​762](jdx/usage#762) - **(windows)** run `run=` scripts with sh when available by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​765](jdx/usage#765) ##### 🎨 Styling - fix clippy and deprecation warnings in test and bench targets by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​763](jdx/usage#763) ### [`v4.1.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#410---2026-07-30) [Compare Source](jdx/usage@v4.0.0...v4.1.0) ##### 🚀 Features - **(cli)** declare what each usage command does to the world by [@​jdx](https://github.com/jdx) in [#​751](jdx/usage#751) - **(mcp)** serve a usage spec to an agent over stdio by [@​jdx](https://github.com/jdx) in [#​746](jdx/usage#746) - **(spec)** add a top-level `repository` field by [@​jdx](https://github.com/jdx) in [#​747](jdx/usage#747) ##### 🐛 Bug Fixes - **(parse)** keep a re-declared global's aliases on one flag by [@​jdx](https://github.com/jdx) in [#​752](jdx/usage#752) - complete repeated variadic args by [@​Jai-JAP](https://github.com/Jai-JAP) in [#​753](jdx/usage#753) ##### New Contributors - [@​Jai-JAP](https://github.com/Jai-JAP) made their first contribution in [#​753](jdx/usage#753) ### [`v4.0.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#400---2026-07-25) [Compare Source](jdx/usage@v3.6.0...v4.0.0) ##### 🚀 Features - **(spec)** allow effect= on flags and args by [@​jdx](https://github.com/jdx) in [#​742](jdx/usage#742) ### [`v3.6.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#360---2026-07-25) [Compare Source](jdx/usage@v3.5.7...v3.6.0) ##### 🚀 Features - **(spec)** add effect= to declare what a command does to the world by [@​jdx](https://github.com/jdx) in [#​739](jdx/usage#739) ##### 🚜 Refactor - **(spec)** make missed SpecCommand fields a compile error, and fix the four that were already missed by [@​jdx](https://github.com/jdx) in [#​740](jdx/usage#740) ### [`v3.5.7`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#357---2026-07-25) [Compare Source](jdx/usage@v3.5.6...v3.5.7) ##### 🐛 Bug Fixes - **(parse)** don't leak the mounting CLI's flags into mounted commands; scan past non-global flags by [@​jdx](https://github.com/jdx) in [#​738](jdx/usage#738) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this MR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box --- This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODguMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiLCJhdXRvbWF0aW9uOmJvdC1hdXRob3JlZCIsImRlcGVuZGVuY3ktdHlwZTo6bWFqb3IiXX0=-->
Problem
generate markdownis the only generator with no way to reach stdout —--out-fileis required and nothing else emits the document:markdown--out-dir/--out-fileonlymanpage,figjson,completion,completion-initSo the workaround is
--out-file /dev/stdout. On unix that happens to work, because it is a real device file. On Windows it is just a relative-looking path, andxx::file::writecreates the parent directory before writing — so it silently produces a realC:\dev\stdoutfile and prints nothing.This repo's own test suite does it, at
cli/tests/markdown.rs:202. On Windows that test leaves the stray file behind and fails; on unix it passes, but the snapshot it produced haswriting to /dev/stdoutbaked in as its first line, because the progress message is printed to stdout and therefore lands inside the document.Fix
generate markdown --out-fileis now optional and defaults to stdout, matchingmanpage,fig,jsonandcompletion. This is the actual fix — it removes the reason to reach for/dev/stdoutin the first place.--out-file -means stdout onmarkdown,manpageandfig, mirroring the-f -convention already used for reading a spec from stdin. It matters for callers that build the path in a variable, and it gives Windows users a portable answer. A file literally named-is spelled./-.write_or_stdouthelper placed directly belowparse_file_or_stdin, so the input and output conventions read as each other's mirror.writing to …line moved from stdout to stderr onmarkdown,manpage,figandsdk.--out-dirnow requires--multi. Without this, dropping the--out-filerequirement would have made--out-diron its own silently print to stdout and ignore the directory./dev/stdoutis deliberately not special-cased on Windows. It is a unix fd path rather than a portable filename, and emulating it honestly drags in/dev/stderr,/dev/null,/dev/fd/Nand append-vs-truncate semantics.-is the portable spelling and is now documented in the help text of all three flags.Also left alone:
generate figwraps the spec in a prescript/postscript when writing to a file but prints the bare spec object to stdout.--out-file -follows the file path — it means "the bytes a file would have received" — and bareusage g figis unchanged. Whether those two should agree is a pre-existing question, flagged in a comment rather than answered here.Compatibility
No existing valid invocation changes meaning. The one behaviour change is that
usage g md -f x.kdlwith no output flag goes from a clap error to printing markdown, which is the point.--out-filekeepseffect=writeon all three commands. The effect model is static per flag with no way to say "read when the value is-", and for a field that agents read to decide whether to ask permission, over-reporting is the safe direction.Verified
Windows 11. In an empty directory:
No
C:\devis created.cargo test -p usage-cli(markdown, manpage, clap_sort, lib) andcargo test -p usage-lib --all-featurespass;cargo clippy --all --all-features -- -D warningsandcargo fmt --all -- --checkare clean.New tests, all platform-independent so they hold the line on the Linux CI:
test_markdown_out_file_dash_writes_no_file— runs with--out-file -in a fresh empty directory and asserts nothing was written. Before the fix this leaves a file named-, which is the same bug that producedC:\dev\stdout.test_markdown_stdout_when_out_file_omitted— the two spellings of stdout produce identical bytes.test_generate_markdown_basicnow asserts the progress line is on stderr and not on stdout.test_generate_manpage_out_file_dash_is_stdout—-o -matches omitting-o.The existing snapshot test switched from
/dev/stdoutto-; the only change tomarkdown__markdown_snapshot_with_examples.snapis the removal of the straywriting to /dev/stdoutline.Generated artifacts
Regenerated from the new help text:
cli/usage.usage.kdl,docs/cli/reference/commands.json,docs/cli/reference/generate/{markdown,manpage,fig}.md,cli/assets/usage.1,cli/assets/fig.ts. The diff in each is the three help strings and nothing else. Completion scripts are untouched — they carry no flag help.Making
--out-fileoptional produces no artifact diff on its own: clap'srequired_unless_presentnever setArgSettings::Required, andlib/src/spec/flag.rs:430readsis_required_set(), so the spec already recorded the flag as not required.This pull request was generated by Claude Code.
Summary by CodeRabbit
New Features
--out-file -.Documentation
Bug Fixes