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
22 changes: 14 additions & 8 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,13 +417,17 @@ Groups are the opposite case: `Command::get_groups`, `ArgGroup::get_args` and
usage-argv, generated Go, and the clap bridge. It suppresses delimiter
splitting only after `--` or once a `double_dash="automatic"`
positional begins; the same argument still splits ordinary values.
- [ ] **Fixed arity and distinct value names** — clap can say
`num_args(2)` with `<START> <END>`. `var_min` / `var_max` express the bound,
- [x] **Fixed arity and distinct value names** — clap can say
`num_args(2)` with `<START> <END>`. Nested value `var_min` / `var_max`
express the per-occurrence bound without becoming limits on repeatable flag
occurrences,
and the clap bridge now preserves it for positionals and non-repeatable value
flags. Optional-value ranges beginning at zero and repeatable `Append` ranges
remain bridge-lossy because their per-occurrence semantics cannot be represented
as one accumulated bound. The derive also has one display name for the collection,
so distinct `<START> <END>` labels are still absent.
flags. Repeatable `Append` ranges are enforced per occurrence. Optional-value ranges
beginning at zero remain bridge-lossy when clap exposes no `default_missing_value`.
Distinct names beside a non-fixed range are reported as lossy and reduced to the first
label so the emitted KDL remains valid. `#[arg(num_args = 2, value_names = ["START", "END"])]`
now preserves the exact bound and each display label through the derive, KDL,
clap bridge, Rust and Go parsers, help, and generated tables.
- [x] **`allow_hyphen_values` on the derive path** — the spec said it and
usage-lib honoured it (`lib/src/parse.rs`); usage-argv now has the same
bit on `Flag`, so a detached value that looks like a flag binds when
Expand Down Expand Up @@ -652,8 +656,10 @@ feature list is not an exhaustive audit.
can remain on the command. Hidden `alias` / `aliases` retain their parse-only
visibility through KDL, Rust and Go tables, help, completion, and the clap
bridge. Non-default `rename_all` / `rename_all_env` casing and bare `env`
now migrate in place; `num_args` and `value_parser` produce targeted
migration diagnostics instead of a generic unknown-option error.
now migrate in place. `num_args` maps to portable bounds (including fixed
arity with distinct `value_names`); optional-value shapes that need
per-occurrence presence semantics and arbitrary `value_parser` callbacks
produce targeted migration diagnostics instead of a generic unknown-option error.
- [x] **Command-with-arguments completion hints.** `ExecutablePath`,
`CommandName`, `CommandString`, and `CommandWithArguments` lower to
shell-native completion types. A forwarded argv vector offers commands for
Expand Down
63 changes: 56 additions & 7 deletions argv/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,16 +253,38 @@ fn flag_usage_masked(meta: &FlagMeta<'_>, show: &Shown) -> String {
out.push('…');
}
if flag.takes_value {
let name = meta.value_name.unwrap_or(flag.name);
// Angled where the value must be given, squared where it need not — the same brackets
// an argument uses, and for the same reason. pitchfork's `--bump` is the fleet's case.
let (open, close) = if meta.value_optional {
('[', ']')
} else {
('<', '>')
};
let _ = write!(out, " {open}{name}{close}");
if flag.variadic {
let exact = exact_arity(meta.value_var_min, meta.value_var_max);
if meta.value_names.len() <= 1 && exact.is_some_and(|n| n > 1) {
let name = meta
.value_names
.first()
.copied()
.or(meta.value_name)
.unwrap_or(flag.name);
for _ in 0..exact.unwrap() {
let _ = write!(out, " {open}{name}{close}");
}
} else if meta.value_names.len() <= 1 {
let name = meta
.value_names
.first()
.copied()
.or(meta.value_name)
.unwrap_or(flag.name);
let _ = write!(out, " {open}{name}{close}");
Comment thread
jdx marked this conversation as resolved.
} else {
for name in meta.value_names {
let _ = write!(out, " {open}{name}{close}");
}
}
if flag.variadic && meta.value_names.len() <= 1 && exact.is_none() {
out.push('…');
}
}
Expand Down Expand Up @@ -308,17 +330,44 @@ pub(crate) fn arg_usage(meta: &ArgMeta<'_>) -> String {
// value without it does not reach this argument at all — and the brackets go *outside*
// it, as usage-lib writes it: `[-- COMMAND]…`, one optional thing rather than a literal
// `--` followed by an optional word.
if arg.double_dash == DoubleDash::Required {
let _ = write!(out, "{open}-- {}{close}", arg.name);
let exact = exact_arity(meta.var_min, meta.var_max);
if meta.value_names.len() <= 1 && exact.is_some_and(|n| n > 1) {
for index in 0..exact.unwrap() {
if index > 0 {
out.push(' ');
}
let _ = write!(out, "{open}{}{close}", arg.name);
}
Comment thread
cursor[bot] marked this conversation as resolved.
} else if meta.value_names.len() <= 1 {
if arg.double_dash == DoubleDash::Required {
let _ = write!(out, "{open}-- {}{close}", arg.name);
} else {
let _ = write!(out, "{open}{}{close}", arg.name);
}
} else {
let _ = write!(out, "{open}{}{close}", arg.name);
if arg.double_dash == DoubleDash::Required {
out.push_str("-- ");
}
for (index, name) in meta.value_names.iter().enumerate() {
if index > 0 {
out.push(' ');
}
let _ = write!(out, "{open}{name}{close}");
}
}
if arg.var {
if arg.var && meta.value_names.len() <= 1 && exact.is_none() {
out.push('…');
}
out
}

fn exact_arity(min: Option<usize>, max: Option<usize>) -> Option<usize> {
match (min, max) {
(Some(min), Some(max)) if min == max => Some(min),
_ => None,
}
}

/// Everything `-h` prints.
///
/// The short form: one line per entry, its help beside it. `--help` renders the same content
Expand Down
86 changes: 80 additions & 6 deletions argv/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,8 @@ pub struct FlagMeta<'a> {
pub long_help: Option<&'a str>,
/// The placeholder for the flag's value, such as `n` in `--jobs <n>`.
pub value_name: Option<&'a str>,
/// Ordered placeholders for one fixed-arity occurrence.
pub value_names: &'a [&'a str],
pub env: Option<&'a str>,
pub default: &'a [&'a str],
/// Canonical choices plus aliases accepted by the value type.
Expand Down Expand Up @@ -807,6 +809,10 @@ pub struct FlagMeta<'a> {
pub repeatable: bool,
pub var_min: Option<usize>,
pub var_max: Option<usize>,
/// Bounds on values consumed by one occurrence, distinct from the
/// flag-level occurrence bounds above.
pub value_var_min: Option<usize>,
pub value_var_max: Option<usize>,
/// Flags this one displaces when both are given.
pub overrides: &'a [&'a str],
/// Flags that cannot be given alongside this one.
Expand Down Expand Up @@ -854,6 +860,7 @@ impl FlagMeta<'_> {
help: None,
long_help: None,
value_name: None,
value_names: &[],
env: None,
default: &[],
accepted_choices: &[],
Expand All @@ -870,6 +877,8 @@ impl FlagMeta<'_> {
repeatable: false,
var_min: None,
var_max: None,
value_var_min: None,
value_var_max: None,
overrides: &[],
conflicts: &[],
delimiter: None,
Expand Down Expand Up @@ -906,6 +915,8 @@ pub struct DefaultIf<'a> {
#[derive(Debug, Clone, Copy)]
pub struct ArgMeta<'a> {
pub arg: &'a Arg<'a>,
/// Ordered placeholders for a fixed-arity positional.
pub value_names: &'a [&'a str],
pub help: Option<&'a str>,
pub long_help: Option<&'a str>,
pub env: Option<&'a str>,
Expand Down Expand Up @@ -947,6 +958,7 @@ impl ArgMeta<'_> {
complete: None,
complete_type: None,
arg: &Arg::REQUIRED,
value_names: &[],
help: None,
long_help: None,
env: None,
Expand Down Expand Up @@ -1598,12 +1610,40 @@ fn write_flag(out: &mut String, meta: &FlagMeta<'_>, depth: usize) -> core::fmt:
write_many_list(out, "required_unless", meta.required_unless, inner)?;
if meta.flag.takes_value {
indent(out, inner)?;
let name = meta.value_name.unwrap_or(meta.flag.name);
write!(
out,
"arg {}",
quoted(&placeholder(name, meta.flag.variadic, meta.value_optional))
)?;
let exact = exact_arity(meta.value_var_min, meta.value_var_max);
let rendered = if meta.value_names.len() <= 1 && exact.is_some_and(|n| n > 1) {
let name = meta
.value_names
.first()
.copied()
.or(meta.value_name)
.unwrap_or(meta.flag.name);
(0..exact.unwrap())
.map(|_| placeholder(name, false, meta.value_optional))
.collect::<Vec<_>>()
.join(" ")
} else if meta.value_names.len() <= 1 {
let name = meta
.value_names
.first()
.copied()
.or(meta.value_name)
.unwrap_or(meta.flag.name);
placeholder(name, meta.flag.variadic, meta.value_optional)
} else {
meta.value_names
.iter()
.map(|name| placeholder(name, false, meta.value_optional))
.collect::<Vec<_>>()
.join(" ")
};
write!(out, "arg {}", quoted(&rendered))?;
Comment thread
cursor[bot] marked this conversation as resolved.
if let Some(min) = meta.value_var_min {
write!(out, " var_min={min}")?;
}
if let Some(max) = meta.value_var_max {
write!(out, " var_max={max}")?;
}
// Square brackets alone would round-trip as required, since usage-lib reads the
// brackets *and* the attribute: `[BUMP]` without it comes back `required=#true`.
if meta.value_optional {
Expand Down Expand Up @@ -1990,8 +2030,42 @@ fn placeholder(name: &str, variadic: bool, optional: bool) -> String {
format!("{open}{name}{close}{ellipsis}")
}

fn exact_arity(min: Option<usize>, max: Option<usize>) -> Option<usize> {
match (min, max) {
(Some(min), Some(max)) if min == max => Some(min),
_ => None,
}
}

/// A positional's placeholder: angle brackets when required, square when not.
fn arg_placeholder(name: &str, meta: &ArgMeta<'_>) -> String {
if let Some(arity) =
exact_arity(meta.var_min, meta.var_max).filter(|n| *n > 1 && meta.value_names.len() <= 1)
{
let values = (0..arity)
.map(|_| placeholder(name, false, !meta.required))
.collect::<Vec<_>>()
.join(" ");
return values;
}
Comment thread
cursor[bot] marked this conversation as resolved.
if meta.value_names.len() > 1 {
let (open, close) = if meta.required {
('<', '>')
} else {
('[', ']')
};
let values = meta
.value_names
.iter()
.map(|name| format!("{open}{name}{close}"))
.collect::<Vec<_>>()
.join(" ");
return if meta.arg.double_dash == DoubleDash::Required {
format!("-- {values}")
} else {
values
};
}
let ellipsis = if meta.arg.var { "..." } else { "" };
if meta.required {
format!("<{name}>{ellipsis}")
Expand Down
24 changes: 16 additions & 8 deletions clap_usage/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,18 +202,11 @@ fn argument_losses(arg: &Arg, path: &[String], losses: &mut BTreeSet<FidelityLos
break;
}
}
if let Some(names) = arg.get_value_names().filter(|names| names.len() > 1) {
add(
FidelityFeature::DistinctValueNames,
format!("value_names={names:?}"),
);
}
if let Some(range) = arg.get_num_args() {
let takes_values = matches!(arg.get_action(), ArgAction::Set | ArgAction::Append);
let lost = takes_values
&& (arg.get_value_delimiter().is_some()
|| !arg.is_positional()
&& (range.min_values() == 0 || matches!(arg.get_action(), ArgAction::Append)));
|| !arg.is_positional() && range.min_values() == 0);
if lost {
add(
FidelityFeature::ValueArity,
Expand All @@ -226,6 +219,21 @@ fn argument_losses(arg: &Arg, path: &[String], losses: &mut BTreeSet<FidelityLos
);
}
}
let value_names = arg.get_value_names().unwrap_or_default();
if value_names.len() > 1
&& (arg.get_value_delimiter().is_some()
|| arg.get_num_args().is_some_and(|range| {
range.min_values() != value_names.len() || range.max_values() != value_names.len()
}))
{
add(
FidelityFeature::DistinctValueNames,
format!(
"{} value names with ranged or delimiter-split arity",
value_names.len()
),
);
}
let mut hidden = Vec::new();
if arg.is_hide_default_value_set() {
hidden.push("default_value");
Expand Down
Loading