Skip to content
Merged
7 changes: 5 additions & 2 deletions argv/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,8 @@ fn resolve<'a>(
/// Render `error` the way a user should read it.
///
/// `argv` is what was being parsed, which is how the message finds the command to show a usage
/// line for. A [`Error::Help`] renders as nothing: it is not a failure, and a caller that has not
/// line for. [`Error::Help`] and [`Error::Version`] render as nothing: neither is a failure, and a
/// caller that has not
/// handled it before reaching here has a bug this cannot paper over.
pub fn render(
spec: &Spec<'_>,
Expand Down Expand Up @@ -621,7 +622,9 @@ pub fn render(
}
// Not a failure. A caller reaching here with one has skipped handling it, and inventing a
// message would hide that rather than help.
Error::Help { .. } => return String::new(),
// Neither is a failure, and a caller that has not handled them before reaching here
// has a bug this cannot paper over.
Error::Help { .. } | Error::Version => return String::new(),
}

if with_usage {
Expand Down
53 changes: 35 additions & 18 deletions argv/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,17 +228,22 @@ pub fn short_help(spec: &Spec<'_>, path: &[&str], meta: &CommandMeta<'_>) -> Str
let _ = writeln!(out, "{before}\n");
}

// The program, then what it is for. usage-lib prints the name when the spec gives one and
// the binary otherwise, and only when there is a version to put beside it.
if let Some(version) = spec.version {
let name = if spec.name.is_empty() {
spec.bin.unwrap_or_default()
} else {
spec.name
};
let _ = writeln!(out, "{name} {version}");
// The program, then what it is for — on the program's own page. A subcommand's page says
// what the subcommand does; see the long form for why. usage-lib prints the name when the
// spec gives one and the binary otherwise, and only when there is a version beside it.
let root = path.len() <= 1;
if root {
if let Some(version) = spec.version {
let name = if spec.name.is_empty() {
spec.bin.unwrap_or_default()
} else {
spec.name
};
let _ = writeln!(out, "{name} {version}");
}
}
if let Some(about) = spec.about {
let about = if root { spec.about } else { meta.about };
if let Some(about) = about {
let _ = writeln!(out, "{about}\n");
}
let _ = writeln!(out, "Usage: {}", usage_line(path, meta));
Expand Down Expand Up @@ -465,15 +470,27 @@ pub fn long_help(spec: &Spec<'_>, path: &[&str], meta: &CommandMeta<'_>) -> Stri
let _ = writeln!(out, "{before}\n");
}

if let Some(version) = spec.version {
let name = if spec.name.is_empty() {
spec.bin.unwrap_or_default()
} else {
spec.name
};
let _ = writeln!(out, "{name} {version}");
// The banner and the program's own description belong to the program's page. A
// subcommand's page describes the subcommand: `communique generate --help` said
// "Editorialized release notes powered by AI" and never once said what `generate` does,
// which is the question that was asked. clap prints the command's own description here.
let root = path.len() <= 1;
if root {
if let Some(version) = spec.version {
let name = if spec.name.is_empty() {
spec.bin.unwrap_or_default()
} else {
spec.name
};
let _ = writeln!(out, "{name} {version}");
}
}
if let Some(about) = spec.long_about.or(spec.about) {
let about = if root {
spec.long_about.or(spec.about)
} else {
meta.long_about.or(meta.about)
};
if let Some(about) = about {
let _ = writeln!(out, "{about}\n");
}
let _ = writeln!(out, "Usage: {}", usage_line(path, meta));
Expand Down
81 changes: 81 additions & 0 deletions argv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ pub struct Command<'a> {
/// What an unrecognized flag-like token means here. Already resolved — see
/// [`UnknownFlags`].
pub unknown_flags: UnknownFlags,
/// Whether this command answers to `--version` and `-V`.
///
/// Set on the root, and only when the CLI declares a version: clap adds the flag exactly
/// then, and a `--version` that answers with nothing is worse than one that is not there.
/// A field rather than a rule about depth, so a CLI that wants it on a subcommand — clap's
/// `propagate_version` — has somewhere to say so.
pub version: bool,
/// Caller-assigned identifier, echoed back in [`Event::Command`].
///
/// Wide enough for a derive to make these unique without coordination: two
Expand All @@ -177,6 +184,7 @@ impl Command<'_> {
subcommands: &[],
default_subcommand: ::core::option::Option::None,
unknown_flags: UnknownFlags::Value,
version: false,
key: 0,
};
}
Expand Down Expand Up @@ -428,6 +436,11 @@ pub enum Error<'t, 'v> {
/// clap has them. The caller renders — this crate does not print, because a library that
/// writes to stdout on its own is one an adopter cannot embed.
Help { cmd: &'t Command<'t>, long: bool },
/// `--version` was asked for. Not a failure either — the caller prints and leaves.
///
/// Carries nothing: the version string lives in the spec rather than the parse tables,
/// and the caller that answers this has it.
Version,
}

/// The high half of every key one declaration's items get.
Expand Down Expand Up @@ -609,6 +622,30 @@ pub static HELP_LONG: Flag<'static> = Flag {
..Flag::BOOL
};

/// See [`HELP_LONG_KEY`].
pub const VERSION_LONG_KEY: u64 = u64::MAX - 2;
/// See [`HELP_LONG_KEY`].
pub const VERSION_SHORT_KEY: u64 = u64::MAX - 3;

/// `--version`, where the CLI declared one.
///
/// In the parse table and not in the metadata, exactly as `--help` is: a spec does not declare
/// `--version`, so listing one would make the rendered page disagree with the spec it came from.
pub static VERSION_LONG: Flag<'static> = Flag {
key: VERSION_LONG_KEY,
name: "version",
longs: &["version"],
..Flag::BOOL
};

/// `-V`, which clap also supplies.
pub static VERSION_SHORT: Flag<'static> = Flag {
key: VERSION_SHORT_KEY,
name: "version",
shorts: b"V",
..Flag::BOOL
};

/// `-h`, which prints the shorter form.
pub static HELP_SHORT: Flag<'static> = Flag {
key: HELP_SHORT_KEY,
Expand All @@ -628,11 +665,43 @@ fn find_named<'t>(cmd: &'t Command<'t>, name: &[u8]) -> Option<&'t Command<'t>>
.find(|c| c.name.as_bytes() == name || c.aliases.iter().any(|a| a.as_bytes() == name))
}

/// What a caller should print for a parse failure, and what to exit with.
///
/// The one entry point a generated `parse()` reaches for, and the reason it exists here rather
/// than in the derive: whether the good rendering is available is a *feature of this crate* in
/// the adopter's dependency graph, and a `#[cfg]` written into generated code is evaluated in
/// the adopter's crate, where the feature is not theirs to see. That is how a metadata field
/// once got silently dropped; the answer is that the cfg lives beside the thing it gates.
///
/// With `diagnostics` on, this is the clap-shaped message. Without it, the error's `Debug`
/// form — which is still better than nothing and is what a parser-only build asked for.
///
/// [`Error::Help`] and [`Error::Version`] are not failures and must be handled before this.
#[cfg(feature = "diagnostics")]
pub fn render_failure(spec: &spec::Spec<'_>, argv: &[&OsStr], error: &Error<'_, '_>) -> String {
diagnostic::render(spec, argv, error, diagnostic::Style::auto())
}

/// What a caller should print for a parse failure, without the renderer that makes it readable.
///
/// See the other half. A caller that wants the clap-shaped message turns on `diagnostics`;
/// this is what a parser-only build asked for, and it still says which error it was.
#[cfg(all(feature = "spec", not(feature = "diagnostics")))]
pub fn render_failure(spec: &spec::Spec<'_>, argv: &[&OsStr], error: &Error<'_, '_>) -> String {
let _ = (spec, argv);
::std::format!("error: {error:?}\n")
}

/// Whether a flag is one of the two the parser supplies rather than the CLI declaring it.
pub fn is_help_flag(flag: &Flag<'_>) -> bool {
flag.key == HELP_LONG_KEY || flag.key == HELP_SHORT_KEY
}

/// Whether a flag is one of the two the parser supplies for `--version`.
pub fn is_version_flag(flag: &Flag<'_>) -> bool {
flag.key == VERSION_LONG_KEY || flag.key == VERSION_SHORT_KEY
}

/// Resolve a subcommand by name or alias, at compile time.
///
/// For [`Command::default_subcommand`], which names a command that a derive cannot see: the
Expand Down Expand Up @@ -1043,6 +1112,16 @@ impl<'t, 'v> Parser<'t, 'v> {
});
}

// Where the CLI declared a version, `--version` answers with it — asked after the
// command's own flags, so a CLI declaring its own keeps it.
if name == b"version" && self.cmd.version {
return Ok(Event::Flag {
flag: &VERSION_LONG,
value: None,
negated: false,
});
}

// Every CLI answers to `--help`, and none of them declares it. Asked *after* the
// command's own flags, so a CLI that declares its own `--help` keeps it.
if name == b"help" {
Expand Down Expand Up @@ -1295,6 +1374,8 @@ impl<'t, 'v> Parser<'t, 'v> {
// declared a `-h` of its own.
.or(if byte == b'h' {
Some(&HELP_SHORT)
} else if byte == b'V' && self.cmd.version {
Some(&VERSION_SHORT)
} else {
None
})
Expand Down
11 changes: 11 additions & 0 deletions argv/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ pub struct Spec<'a> {
/// The binary as invoked, when it differs from `name`.
pub bin: Option<&'a str>,
pub version: Option<&'a str>,
/// The oldest `usage` that can read this spec, when the CLI says.
///
/// Written first, before anything a `usage` too old to understand would choke on — which is
/// the whole point of it, and why it is declared rather than worked out here: it is the
/// CLI's claim about which consumers it means to keep working.
pub min_usage_version: Option<&'a str>,
pub about: Option<&'a str>,
pub long_about: Option<&'a str>,
/// Which command the root falls back to when a word matches no subcommand.
Expand All @@ -284,6 +290,7 @@ impl Spec<'_> {
name: "",
bin: None,
version: None,
min_usage_version: None,
about: None,
long_about: None,
default_subcommand: None,
Expand Down Expand Up @@ -598,6 +605,10 @@ impl Spec<'_> {
}

fn write_kdl(&self, out: &mut String) -> core::fmt::Result {
// First, so a `usage` too old to read the rest sees it before whatever it would choke on.
if let Some(min) = self.min_usage_version {
prop(out, "min_usage_version", min)?;
}
prop(out, "name", self.name)?;
prop(out, "bin", self.bin.unwrap_or(self.name))?;
if let Some(version) = self.version {
Expand Down
3 changes: 3 additions & 0 deletions conformance/src/argv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,9 @@ fn build(
// Filled in by the caller for the root, which is the only place a spec declares one.
default_subcommand: None,
unknown_flags,
// The corpus describes argv parsing; `--version` is a question the *caller* answers,
// so no vector turns on it and the harness leaves it off.
version: false,
key: 0,
}))
}
Expand Down
15 changes: 8 additions & 7 deletions conformance/tests/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ fn a_required_collection_is_required_at_parse_time_too() {
let no_files = argv(["--tag", "t"]);
let err = MustHaveSome::parse_from(&no_files).expect_err("no files given");
assert!(
format!("{err:?}").contains("files"),
format!("{err:?}").contains("FILES"),
"should name the missing collection: {err:?}"
);
let no_tags = argv(["f"]);
Expand Down Expand Up @@ -365,10 +365,11 @@ fn the_spec_is_valid_and_says_what_was_declared() {
let secret = spec.cmd.flags.iter().find(|f| f.name == "secret").unwrap();
assert!(secret.hide);

// A `String` positional must be filled; a `Vec<String>` need not be.
assert_eq!(spec.cmd.args[0].name, "file");
// A `String` positional must be filled; a `Vec<String>` need not be. Shouted, which is
// what clap prints — `<FILE> [REST]…` — and a positional's name is its placeholder.
assert_eq!(spec.cmd.args[0].name, "FILE");
assert!(spec.cmd.args[0].required);
assert_eq!(spec.cmd.args[1].name, "rest");
assert_eq!(spec.cmd.args[1].name, "REST");
assert!(spec.cmd.args[1].var);
assert!(!spec.cmd.args[1].required);
}
Expand Down Expand Up @@ -415,7 +416,7 @@ struct ManyDefaulted {
fn a_defaulted_argument_reads_as_optional_in_both_renderers() {
// The two sides have to agree, and this is the case where they did not. usage-lib clears
// `required` while *parsing* a spec that declares a default, then renders from `required`
// alone — so it writes `[dir]`. usage-argv kept the two separate and read `required` on its
// alone — so it writes `[DIR]`. usage-argv kept the two separate and read `required` on its
// own, writing `<dir>` for an argument the parser is perfectly happy to omit.
//
// mise's own spec does not catch this: every defaulted positional in it is written
Expand All @@ -427,7 +428,7 @@ fn a_defaulted_argument_reads_as_optional_in_both_renderers() {
let ours = usage_argv::help::usage_line(&["ex"], Defaulted::spec().root);
assert_eq!(ours, theirs, "the two renderers disagree");
assert!(
ours.contains("[dir]"),
ours.contains("[DIR]"),
"a defaulted argument is optional: {ours}"
);
// A flag says it the same way, and had the same bug: usage-lib clears `required` on a flag
Expand All @@ -445,7 +446,7 @@ fn a_defaulted_argument_reads_as_optional_in_both_renderers() {
plain_line,
format!("ex {}", plain.cmd.usage()).trim().to_string()
);
assert!(plain_line.contains("<file>"), "{plain_line}");
assert!(plain_line.contains("<FILE>"), "{plain_line}");

// Past the point where the line collapses them into one placeholder, the same signal
// decides whether it reads `<ARGS>…` or `[ARGS]…` — and all of these are omittable.
Expand Down
Loading
Loading