Skip to content
Merged
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
79 changes: 79 additions & 0 deletions conformance/tests/program_identity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! What a program calls itself, and what version it reports.
//!
//! Two papercuts the communique port hit, both of which made an adopter write something twice
//! or get an answer they did not mean.

use usage::Spec as LibSpec;
use usage_derive::Cli;

/// A tool named after its binary
///
/// `bin` is given and `name` is not — which is the ordinary case, and used to banner itself
/// `cli 0.0.0` after the struct.
#[derive(Cli)]
#[usage(bin = "communique", version)]
struct Cli_ {
#[usage(long)]
plain: bool,
}

/// A tool that names itself something else
#[derive(Cli)]
#[usage(name = "the-tool", bin = "tool", version = "9.9")]
struct Renamed {
#[usage(long)]
plain: bool,
}

#[test]
fn a_program_is_called_what_its_binary_is_called() {
// The name defaults to the struct's, and a struct is usually called `Cli`. So `bin` alone
// produced `name cli`, the banner read `cli 1.3.1`, and communique had to declare the same
// word twice to avoid it.
let spec: LibSpec = Cli_::to_kdl().parse().expect("valid spec");
assert_eq!(spec.name, "communique");
assert_eq!(spec.bin, "communique");

let page = usage_argv::help::render(Cli_::spec(), Cli_::spec().root.cmd, false).expect("page");
assert!(page.starts_with("communique "), "{page}");
}

#[test]
fn a_declared_name_still_wins() {
// The default is only a default: a CLI whose program name differs from its binary says so
// and is believed.
let spec: LibSpec = Renamed::to_kdl().parse().expect("valid spec");
assert_eq!(spec.name, "the-tool");
assert_eq!(spec.bin, "tool");
}

#[test]
fn a_bare_version_is_the_packages_own() {
// `#[usage(version)]` with no value, as clap spells it. The expansion has to reach for
// `CARGO_PKG_VERSION` in the *adopter's* crate — usage-derive has a version of its own and
// it is not the answer — so this is the conformance crate's version, whatever it is.
let spec: LibSpec = Cli_::to_kdl().parse().expect("valid spec");
assert_eq!(spec.version.as_deref(), Some(env!("CARGO_PKG_VERSION")));

// And it reaches the flag, which is the point of declaring one at all.
use std::ffi::OsStr;
let argv = [OsStr::new("--version")];
assert!(matches!(
Cli_::parse_from(&argv),
Err(usage_argv::Error::Version)
));
}

#[test]
fn a_written_version_is_taken_as_written() {
let spec: LibSpec = Renamed::to_kdl().parse().expect("valid spec");
assert_eq!(spec.version.as_deref(), Some("9.9"));
}

#[test]
fn the_fields_are_bound() {
use std::ffi::OsStr;
let argv = [OsStr::new("--plain")];
assert!(Cli_::parse_from(&argv).expect("should parse").plain);
assert!(Renamed::parse_from(&argv).expect("should parse").plain);
}
5 changes: 4 additions & 1 deletion derive/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ pub fn emit(cli: &Cli) -> TokenStream {

let name = &cli.name;
let bin = option_str(cli.bin.as_deref());
let version = option_str(cli.version.as_deref());
let version = match &cli.version {
Some(tokens) => quote!(::core::option::Option::Some(#tokens)),
None => quote!(::core::option::Option::None),
};
let about = option_str(cli.about.as_deref());
let long_about = option_str(cli.long_about.as_deref());

Expand Down
34 changes: 31 additions & 3 deletions derive/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ pub struct Cli {
pub fingerprint: String,
pub name: String,
pub bin: Option<String>,
pub version: Option<String>,
/// The version the CLI reports, as the tokens for an `Option<&str>`.
///
/// Tokens rather than a string because bare `version` means "the package's", and
/// `env!("CARGO_PKG_VERSION")` has to be expanded in the *adopter's* crate — this one has
/// its own version and it is not the answer. clap's bare `#[command(version)]` reads it the
/// same way.
pub version: Option<proc_macro2::TokenStream>,
/// Whether this CLI answers a completion request.
///
/// Opt in rather than supplied like `--help`: it is a hidden command a binary carries and a
Expand Down Expand Up @@ -314,6 +320,7 @@ impl Cli {
));
}

let mut name_given = false;
let (about, long_about) = doc_comment(&input.attrs)?;
let mut cli = Cli {
ident: input.ident.clone(),
Expand Down Expand Up @@ -349,7 +356,10 @@ impl Cli {
for meta in nested(attr)? {
let path = meta.path().clone();
match ident_of(&path).as_str() {
"name" => cli.name = string_value(&meta)?,
"name" => {
cli.name = string_value(&meta)?;
name_given = true;
}
"bin" => cli.bin = Some(string_value(&meta)?),
// Through the same helper as `global` and `var`, so `completion = false`
// means false rather than being read as the bare word with something
Expand All @@ -358,7 +368,16 @@ impl Cli {
"settings" => cli.settings = flag_value(&meta)?,
"effect" => cli.effect = Some(effect_value(&meta)?),
"min_usage_version" => cli.min_usage_version = Some(string_value(&meta)?),
"version" => cli.version = Some(string_value(&meta)?),
"version" => {
cli.version = Some(match &meta {
// `version` on its own: whatever the adopter's package says.
Meta::Path(_) => quote::quote!(env!("CARGO_PKG_VERSION")),
_ => {
let literal = string_value(&meta)?;
quote::quote!(#literal)
}
})
}
// A doc comment's long form always contains its short one — the short form
// *is* the comment's first paragraph. A spec keeps `about` and `about_long`
// independent, and mise's differ entirely: "Dev tools, env vars, and tasks
Expand Down Expand Up @@ -419,6 +438,15 @@ impl Cli {
for field in &named.named {
cli.fields.push(Field::from_field(field)?);
}
// A program is called what its binary is called, unless it says otherwise. The name
// defaults to the struct's, and a struct is usually called `Cli` — so `bin =
// "communique"` with no `name` bannered itself as `cli 1.3.1`, and every adopter had
// to declare the same word twice to avoid it.
if !name_given {
if let Some(bin) = &cli.bin {
cli.name = bin.clone();
}
}
cli.check()?;
Ok(cli)
}
Expand Down