Issue #537: pdfcraft-cli --help prints the full usage (#540)

The per-command options lived only in main.rs's doc header, so a packaged
binary had no way to show them. They move into a USAGE constant (one copy,
no drift) that `--help`, `-h`, `help` and `<command> --help` print to stdout,
exiting 0. No command prints the usage to stderr and fails; an unknown
command is named and points to --help.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
LloydNicholson 2026-10-09 18:19:31 +02:00 committed by GitHub
parent 36af4afb19
commit 5deb42199c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 90 additions and 38 deletions

View File

@ -1,23 +1,6 @@
//! pdfcraft-cli — headless PdfCraft.
//!
//! ```text
//! pdfcraft-cli info <file.pdf> [--password PW] document summary as JSON
//! pdfcraft-cli render <file.pdf> --page N [--dpi 96] --out x.png (.png, .jpg, .tif or .pam)
//! pdfcraft-cli text <file.pdf> [--page N] extracted text (pages separated by form feeds)
//! pdfcraft-cli edit <in.pdf> --out out.pdf [--rotate 1,3:90] [--delete 2,4] [--move 5:1]
//! [--insert-blank 1] [--title T] [--author A] [--full]
//! pdfcraft-cli combine <a.pdf> <b.pdf> … --out combined.pdf
//! pdfcraft-cli extract <in.pdf> --pages 1,3,5 --out out.pdf
//! pdfcraft-cli split <in.pdf> (--every N | --before 3,7) [--out-dir DIR]
//! pdfcraft-cli check <files or dirs…> [--timeout 20] [--dpi 36] [--json out.json]
//! pdfcraft-cli tools automation tools and their JSON Schemas
//! pdfcraft-cli run <tool> [key=value …] [--root DIR] [--out image.png]
//! pdfcraft-cli run --script steps.json [--root DIR] [{"tool": "doc_open", "args": {…}}, …]
//! pdfcraft-cli mcp [--root DIR] [--compact] MCP server on stdin/stdout (opt-in)
//! --compact lists a core set of tools plus tool_search and tool_call
//! pdfcraft-cli ui --control FILE <method> [key=value …] [--out shot.png]
//! drive a running app started with --control FILE
//! ```
//! Usage: `pdfcraft-cli --help`, which prints `USAGE` below.
//!
//! `run` and `mcp` drive the same tool table (`pdfcraft-automation`). In `run`, values parse as
//! JSON when they can (`pages=[1,3]`, `degrees=90`) and are strings otherwise. A script runs its
@ -43,28 +26,53 @@ use std::time::{Duration, Instant};
use pdfcraft_engine::export::ImageFormat;
use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest, RequestKind, inspect};
/// The full usage, printed by `--help` (also after a command, e.g. `render --help`).
const USAGE: &str = "\
pdfcraft-cli info <file.pdf> [--password PW] document summary as JSON
pdfcraft-cli render <file.pdf> --page N [--dpi 96] --out x.png (.png, .jpg, .tif or .pam)
pdfcraft-cli text <file.pdf> [--page N] extracted text (pages separated by form feeds)
pdfcraft-cli edit <in.pdf> --out out.pdf [--rotate 1,3:90] [--delete 2,4] [--move 5:1]
[--insert-blank 1] [--title T] [--author A] [--full]
pdfcraft-cli combine <a.pdf> <b.pdf> … --out combined.pdf
pdfcraft-cli extract <in.pdf> --pages 1,3,5 --out out.pdf
pdfcraft-cli split <in.pdf> (--every N | --before 3,7) [--out-dir DIR]
pdfcraft-cli check <files or dirs…> [--timeout 20] [--dpi 36] [--json out.json]
pdfcraft-cli tools automation tools and their JSON Schemas
pdfcraft-cli run <tool> [key=value …] [--root DIR] [--out image.png]
pdfcraft-cli run --script steps.json [--root DIR] [{\"tool\": \"doc_open\", \"args\": {…}}, …]
pdfcraft-cli mcp [--root DIR] [--compact] MCP server on stdin/stdout (opt-in)
--compact lists a core set of tools plus tool_search and tool_call
pdfcraft-cli ui --control FILE <method> [key=value …] [--out shot.png]
drive a running app started with --control FILE
help and feedback: https://discord.gg/artcraft";
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let result =
match args.first().map(String::as_str) {
Some("info") => info(&args[1..]),
Some("render") => render(&args[1..]),
Some("text") => text(&args[1..]),
Some("edit") => edit(&args[1..]),
Some("combine") => combine(&args[1..]),
Some("extract") => extract(&args[1..]),
Some("split") => split(&args[1..]),
Some("check") => check(&args[1..]),
Some("check-one") => check_one(&args[1..]),
Some("tools") => tools(),
Some("run") => run(&args[1..]),
Some("ui") => ui(&args[1..]),
#[cfg(feature = "mcp")]
Some("mcp") => mcp(&args[1..]),
Some("--version") => version(),
_ => Err("usage: pdfcraft-cli <info|render|text|edit|combine|extract|split|check|tools|run|mcp|ui> … (see source header for options)\nhelp and feedback: https://discord.gg/artcraft"
.into()),
};
// `<command> --help` too; only right after the command, so a later `-h` (a value) still reaches it.
let command = match args.get(1).map(String::as_str) {
Some("--help" | "-h") => Some("--help"),
_ => args.first().map(String::as_str),
};
let result = match command {
Some("--help" | "-h" | "help") => stdout_line(format_args!("{USAGE}")),
None => Err(format!("usage:\n{USAGE}").into()),
Some("info") => info(&args[1..]),
Some("render") => render(&args[1..]),
Some("text") => text(&args[1..]),
Some("edit") => edit(&args[1..]),
Some("combine") => combine(&args[1..]),
Some("extract") => extract(&args[1..]),
Some("split") => split(&args[1..]),
Some("check") => check(&args[1..]),
Some("check-one") => check_one(&args[1..]),
Some("tools") => tools(),
Some("run") => run(&args[1..]),
Some("ui") => ui(&args[1..]),
#[cfg(feature = "mcp")]
Some("mcp") => mcp(&args[1..]),
Some("--version") => version(),
Some(other) => Err(format!("unknown command '{other}'; see pdfcraft-cli --help").into()),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(CliError::Stdout(e)) if e.kind() == std::io::ErrorKind::BrokenPipe => ExitCode::SUCCESS,

View File

@ -0,0 +1,44 @@
//! `--help` prints the full usage to stdout and succeeds; a missing or unknown command fails with
//! the usage (or a pointer to `--help`) on stderr.
use std::process::{Command, Output};
const BIN: &str = env!("CARGO_BIN_EXE_pdfcraft-cli");
fn run(args: &[&str]) -> Output {
Command::new(BIN).args(args).output().unwrap()
}
fn text(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).into_owned()
}
#[test]
fn help_flags_print_full_usage_to_stdout() {
for args in [&["--help"][..], &["-h"], &["help"], &["render", "--help"], &["check", "-h"]] {
let out = run(args);
assert!(out.status.success(), "{args:?} exited {:?}", out.status);
let stdout = text(&out.stdout);
// Per-command options, not just the command list.
assert!(stdout.contains("pdfcraft-cli render <file.pdf> --page N"), "{args:?}: {stdout}");
assert!(stdout.contains("--every N"), "{args:?}: {stdout}");
assert!(out.stderr.is_empty(), "{args:?}: {}", text(&out.stderr));
}
}
#[test]
fn no_command_fails_with_usage_on_stderr() {
let out = run(&[]);
assert!(!out.status.success());
assert!(out.stdout.is_empty());
assert!(text(&out.stderr).contains("pdfcraft-cli render <file.pdf> --page N"));
}
#[test]
fn unknown_command_names_it_and_points_to_help() {
let out = run(&["frobnicate"]);
assert!(!out.status.success());
let stderr = text(&out.stderr);
assert!(stderr.contains("unknown command 'frobnicate'"), "{stderr}");
assert!(stderr.contains("pdfcraft-cli --help"), "{stderr}");
}