Skip to content
Open
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
18 changes: 9 additions & 9 deletions Cargo.lock

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

16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,20 @@ license = "MIT"
[workspace.dependencies]
clap_usage = { path = "./clap_usage", version = "5.0.0" }
usage-cli = { path = "./cli" }
usage-argv = { path = "./argv", version = "6.5.0" }
usage-config = { path = "./config", version = "6.5.0" }
usage-derive = { path = "./derive", version = "6.5.0" }
usage-argv = { path = "./argv", version = "6.6.0" }
usage-config = { path = "./config", version = "6.6.0" }
usage-derive = { path = "./derive", version = "6.6.0" }
# No features, and defaults off. A feature named here is inherited by every member that writes
# `workspace = true` and inlines into their published manifests — so `clap` and `validation`
# reached members that use neither, one of them an adopter's build script. Defaults
# are off rather than absent because cargo *ignores* a member's `default-features = false` unless
# the workspace declaration sets it too, and warns that it may become a hard error. Members name
# what they need, `docs` included.
usage-lib = { path = "./lib", version = "6.5.0", default-features = false }
usage-rs = { path = "./usage-rs", version = "6.5.0" }
usage-dynamic = { path = "./usage-dynamic", version = "6.5.0" }
usage-test = { path = "./test", version = "6.5.0" }
usage-validation = { path = "./validation", version = "6.5.0" }
usage-lib = { path = "./lib", version = "6.6.0", default-features = false }
usage-rs = { path = "./usage-rs", version = "6.6.0" }
usage-dynamic = { path = "./usage-dynamic", version = "6.6.0" }
usage-test = { path = "./test", version = "6.6.0" }
usage-validation = { path = "./validation", version = "6.6.0" }

[workspace.metadata.release]
allow-branch = ["main"]
2 changes: 1 addition & 1 deletion argv/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "usage-argv"
description = "Zero-allocation argv parser for usage specs"
version = "6.5.0"
version = "6.6.0"
edition = "2021"
rust-version = "1.91"
homepage = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "usage-cli"
edition = "2021"
rust-version = "1.91"
version = "6.5.0"
version = "6.6.0"
description = "CLI for working with usage-based CLIs"
license = { workspace = true }
authors = { workspace = true }
Expand Down
26 changes: 20 additions & 6 deletions cli/src/cli/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,12 @@ impl CompleteWord {
.cmd
.restart_token
.as_ref()
.is_some_and(|rt| prev_token == Some(rt.as_str()));
.is_some_and(|rt| prev_token == Some(rt.as_str()))
|| parsed
.cmd
.clause
.as_ref()
.is_some_and(|clause| prev_token == Some(clause.separator.as_str()));
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
jdx marked this conversation as resolved.

let cx = Ctx {
tera: &ctx,
Expand All @@ -199,10 +204,13 @@ impl CompleteWord {
.iter()
.rev()
.take_while(|token| {
!token
.roles
.iter()
.any(|role| matches!(role, usage::parse::TokenRole::Restart))
!token.roles.iter().any(|role| {
matches!(
role,
usage::parse::TokenRole::Restart
| usage::parse::TokenRole::ClauseSeparator { .. }
)
})
})
.flat_map(|token| &token.roles)
.any(|role| match role {
Expand Down Expand Up @@ -244,7 +252,13 @@ impl CompleteWord {
// This must be checked after flag checks (to allow --flag after :::)
// but before flag_awaiting_value (since restart clears pending flag values)
let mut choices = vec![];
if let Some(arg) = parsed.cmd.args.first() {
if let Some(arg) = parsed
.cmd
.clause
.as_ref()
.and_then(|clause| clause.args.first())
.or_else(|| parsed.cmd.args.first())
{
let (found, constrained) = self.complete_positional(
&cx,
&parsed.cmd,
Expand Down
5 changes: 5 additions & 0 deletions cli/tests/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ fn complete_word_sigil_arg_strips_for_matching_and_restores_for_output() {
assert_cmd("sigil.usage.kdl", &["--", "--", "+n"]).stdout("");
}

#[test]
fn complete_word_clause_separator_restarts_at_the_first_inner_arg() {
assert_cmd("clause.usage.kdl", &["--", "lint", "--fix", ":::", "t"]).stdout("test\n");
}

#[test]
fn complete_word_choices_from_env() {
cmd("env-choices.usage.kdl", Some("fish"))
Expand Down
2 changes: 1 addition & 1 deletion cli/usage.usage.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
min_usage_version "6.5"
name usage
bin usage
version "6.5.0"
version "6.6.0"
repository "https://github.com/jdx/usage"
source_code_link_template #"""
{%- set path = path | replace(from='-', to='_') -%}
Expand Down
2 changes: 1 addition & 1 deletion config/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "usage-config"
description = "Layered configuration resolution for usage specs, with provenance"
version = "6.5.0"
version = "6.6.0"
edition = "2021"
rust-version = "1.91"
homepage = { workspace = true }
Expand Down
153 changes: 153 additions & 0 deletions conformance/tests/clause.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use usage::parse::ParseValue;
use usage::Spec;

fn spec() -> Spec {
r#"
min_usage_version "6.6"
name "clause"
bin "clause"
clause "tasks" separator=":::" {
arg "<task>"
arg "[args]..." var=#true double_dash="automatic"
}
"#
.parse()
.expect("valid clause spec")
}

fn strings<'a>(
mut instance: impl Iterator<Item = (&'a std::sync::Arc<usage::SpecArg>, &'a ParseValue)>,
name: &str,
) -> Vec<String> {
instance
.find(|(arg, _)| arg.name == name)
.map(|(_, value)| match value {
ParseValue::String(value) => vec![value.clone()],
ParseValue::MultiString(values) => values.clone(),
other => panic!("unexpected value: {other:?}"),
})
.unwrap_or_default()
}

#[test]
fn clause_instances_preserve_values_and_restart_flags() {
let parsed = usage::Parser::new(&spec())
.parse(&["clause", "lint", "--fix", ":::", "test", "--all"].map(str::to_string))
.expect("valid invocation");
let instances = &parsed.clauses["tasks"];
assert_eq!(instances.len(), 2);
assert_eq!(strings(instances[0].iter(), "task"), ["lint"]);
assert_eq!(strings(instances[0].iter(), "args"), ["--fix"]);
assert_eq!(strings(instances[1].iter(), "task"), ["test"]);
assert_eq!(strings(instances[1].iter(), "args"), ["--all"]);
assert!(parsed.args.is_empty());
}

#[test]
fn explicit_double_dash_protects_a_literal_separator() {
let parsed = usage::Parser::new(&spec())
.parse(&["clause", "lint", "--", ":::", "tail"].map(str::to_string))
.expect("valid invocation");
let instances = &parsed.clauses["tasks"];
assert_eq!(instances.len(), 1);
assert_eq!(strings(instances[0].iter(), "args"), [":::", "tail"]);
}

#[test]
fn clause_variadic_can_preserve_double_dash() {
let spec: Spec = r#"
name "clause"
bin "clause"
clause "tasks" separator=":::" {
arg "<task>"
arg "[args]..." double_dash="preserve"
}
"#
.parse()
.expect("valid clause spec");
let parsed = usage::Parser::new(&spec)
.parse(&["clause", "lint", "--", "--fix"].map(str::to_string))
.expect("preserved double dash is clause data");
assert_eq!(
strings(parsed.clauses["tasks"][0].iter(), "args"),
["--", "--fix"]
);
}

#[test]
fn clause_round_trips_through_canonical_kdl() {
let spec = spec();
let emitted = spec.to_string();
let reparsed: Spec = emitted.parse().expect("emitted clause spec reparses");
let clause = reparsed.cmd.clause.expect("clause retained");
assert_eq!(clause.name, "tasks");
assert_eq!(clause.separator, ":::");
assert_eq!(clause.args.len(), 2);
}

#[test]
fn clause_arguments_participate_in_relationship_checks() {
for (spec, argv, expected) in [
(
r#"name "clause"
bin "clause"
clause "items" separator=":::" {
arg "[output]" requires="input"
arg "[input]"
}
"#,
vec!["clause", "artifact"],
"input",
),
(
r#"name "clause"
bin "clause"
flag "--json" conflicts="task"
clause "items" separator=":::" { arg "[task]" }
"#,
vec!["clause", "--json", "lint"],
"conflicts with task",
),
(
r#"name "clause"
bin "clause"
clause "items" separator=":::" {
arg "[trigger]"
arg "[dependent]" required_if="trigger"
}
"#,
vec!["clause", "yes"],
"dependent",
),
(
r#"name "clause"
bin "clause"
clause "items" separator=":::" {
arg "[output]" requires="input"
arg "[input]"
}
"#,
vec!["clause", "first", ":::", "second", "input"],
"instance 1",
),
(
r#"name "clause"
bin "clause"
clause "items" separator=":::" {
arg "[trigger]"
arg "[dependent]" {
required_if_eq "trigger" "yes"
}
}
"#,
vec!["clause", "yes", ":::", "no", "present"],
"instance 1",
),
] {
let spec: Spec = spec.parse().expect("valid relationship spec");
let error = usage::Parser::new(&spec)
.parse(&argv.into_iter().map(str::to_string).collect::<Vec<_>>())
.unwrap_err();
assert!(format!("{error:?}").contains(expected), "{error:?}");
}
}
2 changes: 1 addition & 1 deletion derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "usage-derive"
description = "Derive macro that compiles a CLI definition into parse tables and a usage spec"
version = "6.5.0"
version = "6.6.0"
edition = "2021"
rust-version = "1.91"
homepage = { workspace = true }
Expand Down
3 changes: 2 additions & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ export default defineConfig({
link: "/spec/reference/",
items: [
{ text: "arg", link: "/spec/reference/arg" },
{ text: "sigils", link: "/spec/reference/sigils" },
{ text: "sigils", link: "/spec/reference/sigils" },
{ text: "clauses", link: "/spec/reference/clause" },
{ text: "cmd", link: "/spec/reference/cmd" },
{ text: "complete", link: "/spec/reference/complete" },
{ text: "flag", link: "/spec/reference/flag" },
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/reference/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -2014,7 +2014,7 @@
"sources": {},
"files": []
},
"version": "6.5.0",
"version": "6.6.0",
"usage": "Usage: usage <COMMAND>\n usage --completions <COMPLETIONS>\n usage --usage-spec",
"complete": {},
"source_code_link_template": "{%- set path = path | replace(from='-', to='_') -%}\n{%- if cmd.subcommands | length > 0 -%}\n{%- set path = path ~ \"/mod.rs\" -%}\n{%- elif path in [\"bash\", \"fish\", \"powershell\", \"zsh\"] -%}\n{%- set path = \"shell.rs\" -%}\n{%- else -%}\n{%- set path = path ~ \".rs\" -%}\n{%- endif -%}\nhttps://github.com/jdx/usage/blob/main/cli/src/cli/{{path}}",
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**Usage:** `usage [--completions <COMPLETIONS>] [--usage-spec] <SUBCOMMAND>`

**Version:** 6.5.0
**Version:** 6.6.0

**Repository:** https://github.com/jdx/usage

Expand Down
Loading