Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
matrix:
include:
- version: "1.91"
crates: usage-argv usage-derive usage-config
crates: usage-argv usage-derive usage-config usage-rs
- version: "1.95"
crates: usage-lib usage-config-build clap_usage usage-cli
steps:
Expand Down
59 changes: 45 additions & 14 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"config",
"config-build",
"derive",
"usage-rs",
"clap_usage",
"cli",
"conformance",
Expand Down Expand Up @@ -33,6 +34,7 @@ usage-argv = { path = "./argv", version = "5.1.0" }
usage-config = { path = "./config", version = "5.1.0" }
usage-derive = { path = "./derive", version = "5.1.0" }
usage-lib = { path = "./lib", version = "5.1.0", features = ["clap"] }
usage-rs = { path = "./usage-rs", version = "5.1.0" }

[workspace.metadata.release]
allow-branch = ["main"]
15 changes: 11 additions & 4 deletions argv/src/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,19 +428,26 @@ pub fn complete<'a>(spec: &'a Spec<'a>, split: &Split) -> Completions<'a> {

// The name a value here would have, which is what says whether paths belong, and whether
// that value declares its own set.
let (named, declares_choices) = if let Some(flag) = position.awaiting_value {
let (named, declares_choices, complete_type) = if let Some(flag) = position.awaiting_value {
let meta = flag_meta(spec.root, flag);
(
meta.and_then(|m| m.value_name).or(Some(flag.name)),
meta.is_some_and(|m| !m.choices.is_empty()),
meta.and_then(|m| m.complete_type),
)
} else if let Some(arg) = at_cursor {
let meta = arg_meta(spec.root, arg);
(Some(arg.name), meta.is_some_and(|m| !m.choices.is_empty()))
(
Some(arg.name),
meta.is_some_and(|m| !m.choices.is_empty()),
meta.and_then(|m| m.complete_type),
)
} else {
(None, false)
(None, false, None)
};
let asked_for = named.and_then(files_for);
let asked_for = complete_type
.and_then(files_for)
.or_else(|| named.and_then(files_for));

// An argument that requires a separator is not fillable yet, so nothing else belongs here —
// not even a path, which the parser would reject exactly as it rejects a value.
Expand Down
16 changes: 16 additions & 0 deletions argv/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,13 @@ pub fn render(
);
}
Error::MissingSubcommand => {
// A bare command that can do nothing on its own is a request for orientation.
// clap prints the command's help page here, including the available subcommands,
// while keeping exit 2; an error plus only `<SUBCOMMAND>` tells the reader what is
// missing and withholds the list they need to fix it.
if let Some(help) = crate::help::render_at(spec, &taken, false) {
return help;
}
with_usage = true;
let _ = writeln!(
out,
Expand Down Expand Up @@ -811,6 +818,15 @@ mod tests {
assert_eq!(line, crate::help::usage_line(&["ex", "use"], &USE_META));
}

#[test]
fn a_missing_subcommand_prints_the_choices() {
let message = rendered(&[], Error::MissingSubcommand);
assert!(message.contains("Commands:"), "{message}");
assert!(message.contains("use"), "{message}");
assert!(message.contains("user"), "{message}");
assert!(!message.contains("requires a subcommand"), "{message}");
}

#[test]
fn a_missing_value_names_what_it_wanted() {
// No usage block: the shape of the command line was right, one value was missing — which
Expand Down
15 changes: 15 additions & 0 deletions argv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@

use std::ffi::{OsStr, OsString};

/// A value's filesystem completion class for `#[usage(value_hint = ...)]`.
///
/// This lives in the runtime crate so a declaration never needs clap merely to describe what
/// kind of path a shell should offer. It is metadata only and adds no work to a successful
/// parse.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ValueHint {
/// A path to a file.
FilePath,
/// A path to either a file or a directory.
AnyPath,
/// A path to a directory.
DirPath,
}

#[cfg(feature = "complete")]
pub mod complete;
#[cfg(feature = "diagnostics")]
Expand Down
44 changes: 44 additions & 0 deletions argv/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ pub struct Spec<'a> {
pub min_usage_version: Option<&'a str>,
pub about: Option<&'a str>,
pub long_about: Option<&'a str>,
/// An exact usage synopsis, including the `Usage:` prefix, when the generated
/// shape needs alternatives that cannot be inferred from one command grammar.
pub usage: Option<&'a str>,
/// Which command the root falls back to when a word matches no subcommand.
/// mise uses this so `mise foo` completes as `mise run foo`.
pub default_subcommand: Option<&'a str>,
Expand All @@ -293,6 +296,7 @@ impl Spec<'_> {
min_usage_version: None,
about: None,
long_about: None,
usage: None,
default_subcommand: None,
root: &CommandMeta::EMPTY,
};
Expand Down Expand Up @@ -485,6 +489,8 @@ pub struct FlagMeta<'a> {
/// that asks *this binary*, so a spec stays complete for every other consumer while the
/// binary answers itself.
pub complete: Option<Completer>,
/// A built-in completion class such as `path` or `dir`.
pub complete_type: Option<&'a str>,
/// Whether the flag may be given more than once. Distinct from
/// [`Flag::variadic`], which is one occurrence taking several values.
pub repeatable: bool,
Expand Down Expand Up @@ -518,6 +524,7 @@ impl FlagMeta<'_> {
/// Metadata for a flag with nothing declared, for struct update syntax.
pub const EMPTY: FlagMeta<'static> = FlagMeta {
complete: None,
complete_type: None,
flag: &Flag::BOOL,
help: None,
long_help: None,
Expand Down Expand Up @@ -561,12 +568,15 @@ pub struct ArgMeta<'a> {
pub help_heading: Option<&'a str>,
/// What answers for this argument when a shell asks. See [`FlagMeta::complete`].
pub complete: Option<Completer>,
/// A built-in completion class such as `path` or `dir`.
pub complete_type: Option<&'a str>,
}

impl ArgMeta<'_> {
/// Metadata for an argument with nothing declared, for struct update syntax.
pub const EMPTY: ArgMeta<'static> = ArgMeta {
complete: None,
complete_type: None,
arg: &Arg::REQUIRED,
help: None,
long_help: None,
Expand Down Expand Up @@ -666,6 +676,9 @@ impl Spec<'_> {
if let Some(long_about) = self.long_about.or(self.root.long_about) {
prop(out, "long_about", long_about)?;
}
if let Some(usage) = self.usage {
prop(out, "usage", usage)?;
}
// Written only when it is not the default, so an ordinary spec stays quiet
// about it.
if self.root.cmd.unknown_flags == Some(UnknownFlags::Error) {
Expand Down Expand Up @@ -785,6 +798,7 @@ fn write_body(
);
write_arg(out, arg, depth)?;
}
write_completion_types(out, meta, depth)?;
#[cfg(feature = "complete")]
write_completers(out, meta, bin, depth)?;
for sub in meta.subcommands {
Expand All @@ -793,6 +807,36 @@ fn write_body(
Ok(())
}

/// Built-in completion types declared by this command, written in the spec's vocabulary.
fn write_completion_types(
out: &mut String,
meta: &CommandMeta<'_>,
depth: usize,
) -> core::fmt::Result {
for arg in meta.args {
if let Some(type_) = arg.complete_type {
indent(out, depth)?;
writeln!(
out,
"complete {} type={}",
quoted(&arg.arg.name.to_ascii_lowercase()),
quoted(type_)
)?;
}
}
for flag in meta.flags {
if let Some(type_) = flag.complete_type {
let name = flag
.value_name
.unwrap_or(flag.flag.name)
.to_ascii_lowercase();
indent(out, depth)?;
writeln!(out, "complete {} type={}", quoted(&name), quoted(type_))?;
}
}
Ok(())
}

fn write_command(
out: &mut String,
meta: &CommandMeta<'_>,
Expand Down
7 changes: 4 additions & 3 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ name = "usage_cli"
path = "src/lib.rs"

[dependencies]
clap = { version = "4", features = ["derive", "string", "env"] }
clap_usage = { workspace = true }
env_logger = "0.11"
indexmap = "2"
itertools = "0.15"
Expand All @@ -43,6 +41,10 @@ serde_with = "3"
tera = "2"
thiserror = "2"
tokio = { version = "1", features = ["rt", "macros", "io-std"] }
# The CLI is the facade's first adopter: `usage` parses its own command line with
# the parser it ships. `diagnostics` includes spec emission for `--usage-spec` and
# the errors a person needs when a command line does not parse.
usage-rs = { workspace = true, features = ["diagnostics"] }
usage-lib = { workspace = true, features = ["clap", "docs", "unstable_choices_env"] }
xx = "2"

Expand All @@ -51,7 +53,6 @@ exec = "0.3"

[dev-dependencies]
assert_cmd = { version = "2", features = ["color-auto"] }
clap-sort = "1"
ctor = "1"
insta = "1"
predicates = "3"
Expand Down
Loading