Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cli/assets/fig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ const completionSpec: Fig.Spec = {
{
name: "--out-file",
description:
"File path where the generated Fig spec will be saved",
'File path where the generated Fig spec will be saved, or "-" for stdout',
isRepeatable: false,
args: {
name: "out_file",
Expand Down Expand Up @@ -380,7 +380,7 @@ const completionSpec: Fig.Spec = {
},
{
name: ["-o", "--out-file"],
description: "Output file path (defaults to stdout)",
description: 'Output file path, or "-" for stdout (default)',
isRepeatable: false,
args: {
name: "out_file",
Expand Down Expand Up @@ -434,7 +434,7 @@ const completionSpec: Fig.Spec = {
{
name: "--out-file",
description:
"Output file path for single-file markdown generation",
'Output file path for single-file markdown generation, or "-" for stdout (default)',
isRepeatable: false,
args: {
name: "out_file",
Expand Down
6 changes: 3 additions & 3 deletions cli/assets/usage.1
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ Generate Fig completion spec for Amazon Q / Fig
A usage spec taken in as a file, use "\-" to read from stdin
.TP
\fB\-\-out\-file\fR \fI<OUT_FILE>\fR
File path where the generated Fig spec will be saved
File path where the generated Fig spec will be saved, or "\-" for stdout
.TP
\fB\-\-spec\fR \fI<SPEC>\fR
Raw string spec input
Expand All @@ -285,7 +285,7 @@ raw string spec input
A usage spec taken in as a file, use "\-" to read from stdin
.TP
\fB\-o, \-\-out\-file\fR \fI<OUT_FILE>\fR
Output file path (defaults to stdout)
Output file path, or "\-" for stdout (default)
.TP
\fB\-s, \-\-section\fR \fI<SECTION>\fR
Manual section number (default: 1)
Expand Down Expand Up @@ -315,7 +315,7 @@ Escape HTML in markdown
Output markdown files to this directory (required when using \-\-multi)
.TP
\fB\-\-out\-file\fR \fI<OUT_FILE>\fR
Output file path for single\-file markdown generation
Output file path for single\-file markdown generation, or "\-" for stdout (default)
.TP
\fB\-\-replace\-pre\-with\-code\-fences\fR
Replace `<pre>` tags with markdown code fences
Expand Down
9 changes: 6 additions & 3 deletions cli/src/cli/generate/fig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub struct Fig {
#[clap(short, long)]
file: Option<PathBuf>,

/// File path where the generated Fig spec will be saved
/// File path where the generated Fig spec will be saved, or "-" for stdout
#[clap(long, value_hint = clap::ValueHint::FilePath)]
out_file: Option<PathBuf>,

Expand Down Expand Up @@ -355,8 +355,7 @@ impl FigCommand {
impl Fig {
pub fn run(&self) -> miette::Result<()> {
let write = |path: &PathBuf, md: &str| -> miette::Result<()> {
println!("writing to {}", path.display());
xx::file::write(path, format!("{}\n", md.trim()))?;
generate::write_or_stdout(Some(path), &format!("{}\n", md.trim()))?;
Ok(())
};
let spec = generate::file_or_spec(&self.file, &self.spec)?;
Expand Down Expand Up @@ -394,6 +393,10 @@ impl Fig {
)
});

// Only the file path wraps the spec in the prescript/postscript that make it usable
// on its own; bare `usage g fig` prints the spec object alone. `--out-file -` follows
// the file path, since it means "the bytes a file would have received". Whether the
// two should agree is a separate question, left alone here.
if let Some(path) = &self.out_file {
let prescript = if let Some(source_file) = &self.file {
let source_label = if source_file.as_os_str() == "-" {
Expand Down
11 changes: 3 additions & 8 deletions cli/src/cli/generate/manpage.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::path::PathBuf;

use super::parse_file_or_stdin;
use super::{parse_file_or_stdin, write_or_stdout};
use clap::Args;
use usage::docs::manpage::ManpageRenderer;

Expand All @@ -11,7 +11,7 @@ pub struct Manpage {
#[clap(short, long)]
file: PathBuf,

/// Output file path (defaults to stdout)
/// Output file path, or "-" for stdout (default)
#[clap(short, long, value_hint = clap::ValueHint::FilePath)]
out_file: Option<PathBuf>,

Expand All @@ -32,12 +32,7 @@ impl Manpage {
let renderer = ManpageRenderer::new(spec).with_section(self.section);
let manpage = renderer.render()?;

if let Some(out_file) = &self.out_file {
println!("writing to {}", out_file.display());
xx::file::write(out_file, &manpage)?;
} else {
print!("{}", manpage);
}
write_or_stdout(self.out_file.as_deref(), &manpage)?;

Ok(())
}
Expand Down
31 changes: 17 additions & 14 deletions cli/src/cli/generate/markdown.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::path::PathBuf;

use super::parse_file_or_stdin;
use super::{parse_file_or_stdin, write_or_stdout};
use clap::Args;
use usage::docs::markdown::MarkdownRenderer;

Expand All @@ -23,11 +23,11 @@ pub struct Markdown {
html_encode: bool,

/// Output markdown files to this directory (required when using --multi)
#[clap(long, value_hint = clap::ValueHint::DirPath)]
#[clap(long, value_hint = clap::ValueHint::DirPath, requires = "multi")]
out_dir: Option<PathBuf>,

/// Output file path for single-file markdown generation
#[clap(long, value_hint = clap::ValueHint::FilePath, required_unless_present = "multi")]
/// Output file path for single-file markdown generation, or "-" for stdout (default)
#[clap(long, value_hint = clap::ValueHint::FilePath)]
out_file: Option<PathBuf>,

/// Replace `<pre>` tags with markdown code fences
Expand All @@ -41,15 +41,19 @@ pub struct Markdown {

impl Markdown {
pub fn run(&self) -> miette::Result<()> {
// The banner belongs to every generated document, so build it in one place rather
// than once per output path.
let render = |md: &str| {
format!(
"<!-- @generated by usage-cli from usage spec -->\n{}\n",
md.trim()
)
};
// File-only, deliberately: every path this receives is a join onto `--out-dir`, so a
// `-` meaning stdout cannot turn up here.
let write = |path: &PathBuf, md: &str| -> miette::Result<()> {
println!("writing to {}", path.display());
xx::file::write(
path,
format!(
"<!-- @generated by usage-cli from usage spec -->\n{}\n",
md.trim()
),
)?;
eprintln!("writing to {}", path.display());
xx::file::write(path, render(md))?;
Ok(())
};
let spec = parse_file_or_stdin(&self.file)?;
Expand Down Expand Up @@ -84,8 +88,7 @@ impl Markdown {
write(&path_idx, &md_idx)?;
} else {
let md = ctx.render_spec()?;
let path = self.out_file.as_ref().unwrap();
write(path, &md)?;
write_or_stdout(self.out_file.as_deref(), &render(&md))?;
}
Ok(())
}
Expand Down
41 changes: 40 additions & 1 deletion cli/src/cli/generate/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::io::Read;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use usage::error::UsageErr;

Expand Down Expand Up @@ -65,6 +65,45 @@ pub fn parse_file_or_stdin(file: &Path) -> Result<Spec, UsageErr> {
}
}

/// The mirror of [`parse_file_or_stdin`]: `-` means stdout, and so does no path at all.
///
/// Both spellings are collapsed here so a caller never has to ask which of the two it is
/// looking at. To write to a file actually named `-`, spell it `./-`.
///
/// The progress line goes to stderr. It used to go to stdout, where it ended up inside the
/// document whenever the document itself was going to stdout.
pub fn write_or_stdout(out_file: Option<&Path>, contents: &str) -> Result<(), UsageErr> {
match out_file {
Some(path) if path.as_os_str() != "-" => {
eprintln!("writing to {}", path.display());
xx::file::write(path, contents)?;
}
_ => write_stdout(contents)?,
}
Ok(())
}

/// Write a generated document to stdout, reporting a failed write rather than panicking.
///
/// `print!` panics if the write fails, and these documents are big enough to outlast a pipe
/// buffer: `usage g markdown -f mise.usage.kdl --out-file - | head -1` produces 100 KB and
/// used to end in `failed printing to stdout … (os error 109)` and exit 101.
///
/// A reader that closed early is not a failure to report, though. Rust ignores `SIGPIPE`, so
/// what would end the process silently in C arrives here as an ordinary write error; treating
/// it as success is what makes `| head` behave the way it does everywhere else.
fn write_stdout(contents: &str) -> Result<(), UsageErr> {
let mut stdout = std::io::stdout().lock();
let wrote = stdout.write_all(contents.as_bytes()).and_then(|_| {
// Explicitly, because the flush at process exit discards whatever it hits.
stdout.flush()
});
match wrote {
Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
other => Ok(other?),
}
}

fn read_spec_from_stdin() -> Result<Spec, UsageErr> {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input)?;
Expand Down
2 changes: 1 addition & 1 deletion cli/src/cli/generate/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl Sdk {

for file in &output.files {
let path = self.output.join(&file.path);
println!("writing to {}", path.display());
eprintln!("writing to {}", path.display());
xx::file::write(&path, &file.content)?;
}

Expand Down
15 changes: 15 additions & 0 deletions cli/tests/manpage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,21 @@ fn test_generate_manpage_output_to_file() {
std::fs::remove_file(&out_file).unwrap();
}

#[test]
fn test_generate_manpage_out_file_dash_is_stdout() {
let run = |args: &[&str]| {
let mut cmd = usage_cmd();
cmd.args(["generate", "manpage", "-f"]);
cmd.arg(example_path("basic.usage.kdl"));
cmd.args(args);
let output = cmd.output().unwrap();
assert!(output.status.success());
String::from_utf8(output.stdout).unwrap()
};

assert_eq!(run(&[]), run(&["-o", "-"]));
}

#[test]
fn test_manpage_output_first_50_lines() {
// Test first 50 lines of mise manpage to avoid huge snapshot
Expand Down
55 changes: 48 additions & 7 deletions cli/tests/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ fn test_generate_markdown_basic() {
out_file.to_str().unwrap(),
]);

cmd.assert().success();
let output = cmd.output().unwrap();
assert!(output.status.success());

// Verify file was created
assert!(out_file.exists());
Expand All @@ -186,25 +187,65 @@ fn test_generate_markdown_basic() {
let content = fs::read_to_string(&out_file).unwrap();
assert!(content.contains("# `basic.usage.kdl`"));

// The progress line belongs on stderr; on stdout it lands inside the document whenever
// the document is going to stdout.
let stdout = String::from_utf8(output.stdout).unwrap();
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(!stdout.contains("writing to"), "stdout was: {stdout:?}");
assert!(stderr.contains("writing to"), "stderr was: {stderr:?}");

// Clean up
std::fs::remove_file(&out_file).unwrap();
}

fn markdown_stdout(args: &[&str]) -> String {
let mut cmd = usage_cmd();
cmd.args(["generate", "markdown", "-f"]);
cmd.arg(example_path("with-examples.usage.kdl"));
cmd.args(args);

let output = cmd.output().unwrap();
assert!(output.status.success());
String::from_utf8(output.stdout).unwrap()
}

#[test]
fn test_markdown_snapshot_with_examples() {
insta::assert_snapshot!(markdown_stdout(&["--out-file", "-"]));
}

#[test]
fn test_markdown_stdout_when_out_file_omitted() {
// The two spellings of "stdout" have to agree; `--out-file -` exists for callers that
// build the path in a variable.
assert_eq!(markdown_stdout(&[]), markdown_stdout(&["--out-file", "-"]));
}

#[test]
fn test_markdown_out_file_dash_writes_no_file() {
// The bug this guards: `--out-file` was resolved as a path unconditionally, and
// `xx::file::write` creates the parent directory before writing. `/dev/stdout` therefore
// produced a real `C:\dev\stdout` on Windows; `-` would likewise leave a file named `-`.
let dir = std::env::temp_dir().join(format!("usage_md_dash_{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();

let mut cmd = usage_cmd();
cmd.args([
cmd.current_dir(&dir).args([
"generate",
"markdown",
"-f",
&example_path("with-examples.usage.kdl"),
"--out-file",
"/dev/stdout",
"-",
]);
cmd.assert().success();

let output = cmd.output().unwrap();
assert!(output.status.success());
let leftovers: Vec<_> = fs::read_dir(&dir)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
assert!(leftovers.is_empty(), "wrote files: {leftovers:?}");

let stdout = String::from_utf8(output.stdout).unwrap();
insta::assert_snapshot!(stdout);
fs::remove_dir_all(&dir).unwrap();
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
---
source: cli/tests/markdown.rs
expression: stdout
expression: "markdown_stdout(&[\"--out-file\", \"-\"])"
---
writing to /dev/stdout
<!-- @generated by usage-cli from usage spec -->
# `demo`

Expand Down
6 changes: 3 additions & 3 deletions cli/usage.usage.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ You may need to set this if you have a different bin named "usage"
flag "-f --file" help="A usage spec taken in as a file, use \"-\" to read from stdin" {
arg <FILE>
}
flag --out-file help="File path where the generated Fig spec will be saved" effect=write {
flag --out-file help="File path where the generated Fig spec will be saved, or \"-\" for stdout" effect=write {
arg <OUT_FILE>
}
flag --spec help="Raw string spec input" {
Expand All @@ -144,7 +144,7 @@ You may need to set this if you have a different bin named "usage"
flag "-f --file" help="A usage spec taken in as a file, use \"-\" to read from stdin" required=#true {
arg <FILE>
}
flag "-o --out-file" help="Output file path (defaults to stdout)" effect=write {
flag "-o --out-file" help="Output file path, or \"-\" for stdout (default)" effect=write {
arg <OUT_FILE>
}
flag "-s --section" help="Manual section number (default: 1)" default="1" {
Expand All @@ -166,7 +166,7 @@ Common sections: - 1: User commands - 5: File formats - 7: Miscellaneous - 8: Sy
flag --out-dir help="Output markdown files to this directory (required when using --multi)" effect=write {
arg <OUT_DIR>
}
flag --out-file help="Output file path for single-file markdown generation" effect=write {
flag --out-file help="Output file path for single-file markdown generation, or \"-\" for stdout (default)" effect=write {
arg <OUT_FILE>
}
flag --replace-pre-with-code-fences help="Replace `<pre>` tags with markdown code fences"
Expand Down
Loading