Skip to content

fix(cli): let generate markdown write to stdout - #766

Draft
JamBalaya56562 wants to merge 1 commit into
jdx:mainfrom
JamBalaya56562:fix-markdown-stdout
Draft

fix(cli): let generate markdown write to stdout#766
JamBalaya56562 wants to merge 1 commit into
jdx:mainfrom
JamBalaya56562:fix-markdown-stdout

Conversation

@JamBalaya56562

@JamBalaya56562 JamBalaya56562 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Problem

generate markdown is the only generator with no way to reach stdout — --out-file is required and nothing else emits the document:

Subcommand stdout?
markdown no--out-dir / --out-file only
manpage, fig yes, when the output flag is omitted
json, completion, completion-init stdout only

So 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, and xx::file::write creates the parent directory before writing — so it silently produces a real C:\dev\stdout file 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 has writing to /dev/stdout baked in as its first line, because the progress message is printed to stdout and therefore lands inside the document.

Fix

  • generate markdown --out-file is now optional and defaults to stdout, matching manpage, fig, json and completion. This is the actual fix — it removes the reason to reach for /dev/stdout in the first place.
  • --out-file - means stdout on markdown, manpage and fig, 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 ./-.
  • Both spellings go through one write_or_stdout helper placed directly below parse_file_or_stdin, so the input and output conventions read as each other's mirror.
  • The writing to … line moved from stdout to stderr on markdown, manpage, fig and sdk.
  • --out-dir now requires --multi. Without this, dropping the --out-file requirement would have made --out-dir on its own silently print to stdout and ignore the directory.

/dev/stdout is 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/N and append-vs-truncate semantics. - is the portable spelling and is now documented in the help text of all three flags.

Also left alone: generate fig wraps 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 bare usage g fig is 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.kdl with no output flag goes from a clap error to printing markdown, which is the point.

--out-file keeps effect=write on 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:

$ usage g md -f basic.usage.kdl --out-file - | head -3
<!-- @generated by usage-cli from usage spec -->
# `basic.usage.kdl`

$ usage g md -f basic.usage.kdl | head -3        # identical
$ ls -A | wc -l
0

No C:\dev is created. cargo test -p usage-cli (markdown, manpage, clap_sort, lib) and cargo test -p usage-lib --all-features pass; cargo clippy --all --all-features -- -D warnings and cargo fmt --all -- --check are 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 produced C:\dev\stdout.
  • test_markdown_stdout_when_out_file_omitted — the two spellings of stdout produce identical bytes.
  • test_generate_markdown_basic now 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/stdout to -; the only change to markdown__markdown_snapshot_with_examples.snap is the removal of the stray writing to /dev/stdout line.

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-file optional produces no artifact diff on its own: clap's required_unless_present never set ArgSettings::Required, and lib/src/spec/flag.rs:430 reads is_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

    • Generate Fig, manpage, and Markdown output directly to stdout using --out-file -.
    • Markdown generation now defaults to stdout when no output file is specified.
    • Output progress messages are separated from generated content and sent to stderr.
  • Documentation

    • Clarified command help and reference documentation for stdout output, defaults, and file-generation options.
  • Bug Fixes

    • Prevented stdout-generated Markdown from creating filesystem artifacts.

`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`.

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.
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Pro Plus

Run ID: fa97bb7d-fedb-4bca-bdd4-6da42c3413e8

📥 Commits

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

⛔ Files ignored due to path filters (1)
  • cli/tests/snapshots/markdown__markdown_snapshot_with_examples.snap is excluded by !**/*.snap
📒 Files selected for processing (14)
  • cli/assets/fig.ts
  • cli/assets/usage.1
  • cli/src/cli/generate/fig.rs
  • cli/src/cli/generate/manpage.rs
  • cli/src/cli/generate/markdown.rs
  • cli/src/cli/generate/mod.rs
  • cli/src/cli/generate/sdk.rs
  • cli/tests/manpage.rs
  • cli/tests/markdown.rs
  • cli/usage.usage.kdl
  • docs/cli/reference/commands.json
  • docs/cli/reference/generate/fig.md
  • docs/cli/reference/generate/manpage.md
  • docs/cli/reference/generate/markdown.md

📝 Walkthrough

Walkthrough

Fig, manpage, and Markdown generation now support shared stdout and file output handling. Markdown single-file output defaults to stdout. Progress messages use stderr, with tests and documentation updated.

Changes

Generator output handling

Layer / File(s) Summary
Shared output contract
cli/src/cli/generate/mod.rs, cli/src/cli/generate/fig.rs, cli/src/cli/generate/manpage.rs, cli/src/cli/generate/markdown.rs, cli/src/cli/generate/sdk.rs
write_or_stdout routes output to stdout or files. Generators use the shared writer, and file progress messages use stderr.
Output validation and coverage
cli/tests/manpage.rs, cli/tests/markdown.rs
Tests verify default and explicit stdout output, stderr progress messages, equivalent output, and the absence of filesystem artifacts.
CLI help and reference updates
cli/assets/*, cli/usage.usage.kdl, docs/cli/reference/*
Help text and reference documentation describe - as stdout and identify applicable defaults.

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
Loading

Suggested reviewers: jdx

Poem

A rabbit checks the output stream,
Where files and stdout meet the dream.
Progress hops to stderr’s side,
Markdown flows with nothing to hide.
- opens the burrow wide.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: allowing Markdown generation to write output to stdout.
Docstring Coverage ✅ Passed Docstring coverage is 93.33% 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.

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.

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