Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
12 changes: 6 additions & 6 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -766,12 +766,12 @@ feature list is not an exhaustive audit.
compatibility baseline, and the parser-behavior semver policy. State which
builder and `ArgMatches` APIs are architectural non-goals instead of leaving
their absence implicit.
- [ ] **A release documentation audit.** Generate the limitations page from, or
check it against, the compatibility matrix; verify dependency snippets against
workspace versions; and remove stale claims after features land. Today the
Rust limitations page still says non-UTF-8 `OsString` values cannot be accepted,
and the clap integration still recommends `clap_usage = "2"` while this
workspace is on version 5.
- [x] **A release documentation audit.** The limitations page is checked against
the versioned compatibility matrix and dependency snippets consistently name
the 6.x epoch. Stale claims about non-UTF-8 values and prefix inference have
been removed; the clap integration now recommends the matching
`clap_usage = "6"`. Concrete workspace and generated-artifact versions remain
release-plz's responsibility.
- [ ] **Completion ecosystem coverage.** Decide whether general clap parity includes
every shell in `clap_complete` and every `ValueHint`. At minimum, either add
Elvish beside bash, fish, PowerShell and zsh or document it as a launch non-goal;
Expand Down
41 changes: 35 additions & 6 deletions argv/src/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,9 +625,9 @@ fn files_for(name: &str) -> Option<Files> {
}
}

fn declared_files(type_: &str, position: &Position<'_>) -> Option<Files> {
fn declared_files(type_: &str, next_arg_values: u32) -> Option<Files> {
if type_.eq_ignore_ascii_case("command_args") {
return Some(if position.next_arg_values == 0 {
return Some(if next_arg_values == 0 {
Files::Commands
} else {
Files::Any
Expand All @@ -649,7 +649,8 @@ fn declared_files_at_cursor(
return None;
}
let meta = metadata_chain_on_route(spec, position).and_then(|chain| chain.last().copied());
let at_cursor = if restarted(meta, split) {
let after_restart = restarted(meta, split);
let at_cursor = if after_restart {
meta.and_then(|m| m.args.first()).map(|m| m.arg)
} else {
position
Expand All @@ -675,7 +676,16 @@ fn declared_files_at_cursor(
(None, None)
};
complete_type
.and_then(|type_| declared_files(type_, position))
.and_then(|type_| {
declared_files(
type_,
if after_restart {
0
} else {
position.next_arg_values
},
)
})
.or_else(|| name.and_then(files_for))
}

Expand All @@ -700,7 +710,8 @@ pub fn complete<'a>(spec: &'a Spec<'a>, split: &Split) -> Completions<'a> {
// that the two halves cannot disagree. Past a restart token it is the command's *first*
// argument, whatever the words before the token filled, and everything below follows from
// that: whether paths belong, whether the set is declared, whether a separator is owed.
let at_cursor = if restarted(meta, split) {
let after_restart = restarted(meta, split);
let at_cursor = if after_restart {
meta.and_then(|m| m.args.first()).map(|m| m.arg)
} else {
position.next_arg
Expand Down Expand Up @@ -730,7 +741,16 @@ pub fn complete<'a>(spec: &'a Spec<'a>, split: &Split) -> Completions<'a> {
(None, false, None)
};
let asked_for = complete_type
.and_then(|type_| declared_files(type_, &position))
.and_then(|type_| {
declared_files(
type_,
if after_restart {
0
} else {
position.next_arg_values
},
)
})
.or_else(|| named.and_then(files_for));

// An argument that requires a separator is not fillable yet, so nothing else belongs here —
Expand Down Expand Up @@ -1925,10 +1945,12 @@ mod tests {
static META_EXEC: CommandMeta = CommandMeta {
cmd: &EXEC,
about: Some("Run something"),
restart_token: Some(":::"),
args: &[ArgMeta {
arg: &FORWARDED,
help: Some("What to run"),
choices: &["one", "two"],
complete_type: Some("command_args"),
..ArgMeta::EMPTY
}],
..CommandMeta::EMPTY
Expand Down Expand Up @@ -2893,6 +2915,13 @@ mod tests {
assert_eq!(mistyped.files, None, "a mistyped choice is still a choice");
}

#[test]
fn a_restart_makes_command_args_expect_a_command_again() {
assert_eq!(answer("mise exec ").files, Some(Files::Commands));
assert_eq!(answer("mise exec one ").files, Some(Files::Any));
assert_eq!(answer("mise exec one ::: ").files, Some(Files::Commands));
}

#[test]
fn each_shell_is_written_the_way_it_reads() {
let answer = complete(&SPEC, &at_end("mise pl"));
Expand Down
5 changes: 4 additions & 1 deletion argv/src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ _{bin}() {{
case "$__usage_files" in
any) _files && __usage_ret=0 ;;
dirs) _files -/ && __usage_ret=0 ;;
executables) _files -g '*(*)' && __usage_ret=0 ;;
executables) _files -g '*(-/,*)' && __usage_ret=0 ;;
commands) _command_names && __usage_ret=0 ;;
esac
return $__usage_ret
Expand Down Expand Up @@ -503,6 +503,9 @@ mod tests {
);
assert!(powershell.contains("} elseif ($files) {"), "{powershell}");
assert!(!powershell.contains("} else if ($files) {"), "{powershell}");

let zsh = script("mise", Shell::Zsh);
assert!(zsh.contains("_files -g '*(-/,*)'"), "{zsh}");
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions clap_usage/src/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub fn spec_with_report<S: Into<String>>(
cmd: &mut Command,
bin_name: S,
) -> (usage::Spec, FidelityReport) {
cmd.build();
Comment thread
jdx marked this conversation as resolved.
Outdated
let report = report(cmd);
(spec(cmd, bin_name), report)
}
Comment thread
jdx marked this conversation as resolved.
Expand Down
24 changes: 18 additions & 6 deletions clap_usage/tests/fidelity_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,13 @@ fn reports_nested_paths_and_leaves_supported_commands_clean() {
),
);
let (_, report) = spec_with_report(&mut nested, "ex");
assert_eq!(report.losses()[0].command, ["ex", "run"]);
assert_eq!(report.losses()[0].argument.as_deref(), Some("number"));
assert_eq!(
report.losses()[0].feature,
FidelityFeature::AllowNegativeNumbers
);
let loss = report
.losses()
.iter()
.find(|loss| loss.argument.as_deref() == Some("number"))
.expect("nested argument loss");
assert_eq!(loss.command, ["ex", "run"]);
assert_eq!(loss.feature, FidelityFeature::AllowNegativeNumbers);
}

#[test]
Expand All @@ -87,6 +88,17 @@ fn reports_delimited_arity_that_the_bridge_cannot_count() {
.any(|loss| loss.feature == FidelityFeature::ValueArity));
}

#[test]
fn builds_action_derived_arity_before_reporting() {
let mut command =
Command::new("ex").arg(Arg::new("values").long("values").action(ArgAction::Append));
let (_, report) = spec_with_report(&mut command, "ex");
assert!(report
.losses()
.iter()
.any(|loss| loss.feature == FidelityFeature::ValueArity));
}

#[test]
fn reports_positional_conflicts_declared_from_either_endpoint() {
for command in [
Expand Down
3 changes: 2 additions & 1 deletion clap_usage/tests/snapshots/simple__simple.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ usage "Usage: example [OPTIONS]"
flag --file help="some input file" {
arg <FILE>
}
flag --usage
flag --usage default="false"
flag "-h --help" help="Print help"
4 changes: 3 additions & 1 deletion cli/src/cli/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ impl CompleteWord {
tera: &ctx,
spec,
parsed: &parsed,
after_restart_token,
};
let mut has_explicit_choices = false;
// Not `available_flags`: inside a mounted command, the mounting CLI's flags stay
Expand Down Expand Up @@ -382,7 +383,7 @@ impl CompleteWord {
.keys()
.any(|bound| bound.as_ref() == next.as_ref())
});
if !command_was_bound {
if cx.after_restart_token || !command_was_bound {
return (self.complete_commands(ctoken), true);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -757,6 +758,7 @@ struct Ctx<'a> {
tera: &'a tera::Context,
spec: &'a Spec,
parsed: &'a ParseOutput,
after_restart_token: bool,
}

/// A description reduced to one line.
Expand Down
22 changes: 22 additions & 0 deletions cli/tests/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,28 @@ complete "command" type="command_args"
.stdout(contains("Cargo.toml"));
}

#[test]
fn complete_word_command_args_restarts_with_executables() {
let usage = cargo::cargo_bin!("usage");
let executable = usage.file_name().unwrap().to_string_lossy().into_owned();
let spec = r#"
name "mycli"
bin "mycli"
cmd "run" restart_token=":::" {
arg "<COMMAND>..." double_dash="automatic"
}
complete "command" type="command_args"
"#;
Command::new(usage)
.args([
"cw", "--shell", "fish", "--spec", spec, "--", "mycli", "run", "usage", ":::", "",
])
.env("PATH", usage.parent().unwrap())
.assert()
.success()
.stdout(contains(executable));
}

#[test]
fn complete_word_subcommands_without_shell() {
let mut cmd = cmd("basic.usage.kdl", None);
Expand Down
2 changes: 1 addition & 1 deletion docs/rust/args-and-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ jobs: Option<u32>,
| `overrides(…)` | Later occurrence silently overrides the named flag |
| `required_if(…)` / `required_unless(…)` | Conditional required-ness |
| `complete = my_fn` | Custom completion function ([Completions](/rust/completions)) |
| `value_hint = ValueHint::FilePath` | Ask the shell for paths or commands (see below) |
| `value_hint = ValueHint::FilePath` | Ask the shell for path completion (see below) |
| `value_name = "…"` | The placeholder shown in help (`--file <PATH>`) |
| `help = "…"` / `long_help = "…"` | Help text (doc comments are usually nicer) |
| `help_heading = "…"` | Group the entry under a heading in help output |
Expand Down
2 changes: 1 addition & 1 deletion docs/rust/completions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Completion support is opt-in: add `completion` to the root attribute and enable

```toml
[dependencies]
usage = { package = "usage-rs", version = "5", features = ["completions"] }
usage = { package = "usage-rs", version = "6", features = ["completions"] }
```

```rust
Expand Down
19 changes: 10 additions & 9 deletions docs/rust/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,16 @@ with the literal written to portable artifacts:
struct Cli;
```

The name and bin expressions return `&'static str`. They are evaluated only when the process
renders help, version output, diagnostics, or a completion script. Successful argument parsing
still reads the static tables directly and does not allocate or build a command graph. `to_kdl()`
keeps `mycli` and `6.0.0`, so generated artifacts are deterministic and do not depend on the
embedding process.

`Cli::runtime_app()` returns the borrowed view with the computed identity applied. For a caller
that already has different identity values, `Cli::app().name(...).bin(...)` provides the same
split explicitly.
The name and bin expressions return `&'static str`; a computed version implements `ToString`.
They are evaluated only when the process renders help, version output, diagnostics, or a
completion script. Successful argument parsing still reads the static tables directly and does
not allocate or build a command graph. `to_kdl()` keeps `mycli` and `6.0.0`, so generated
artifacts are deterministic and do not depend on the embedding process. `--version` formats the
computed version, while `version_spec` remains the static value exported to KDL.

`Cli::runtime_app()` returns the borrowed view with the computed name and bin applied; it does
not currently apply the computed version. For a caller that already has different identity
values, `Cli::app().name(...).bin(...).version(...)` provides the split explicitly.

## What the parser does with the spec

Expand Down
10 changes: 6 additions & 4 deletions docs/spec/integrations/clap.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

```toml
[dependencies]
clap_usage = "5"
clap_usage = "6"
```

## Quick Start
Expand Down Expand Up @@ -36,9 +36,11 @@ println!("{spec}");
```

The report includes the command path, clap argument ID, feature, and source detail
for each detectable loss. clap settings that have setters but no public getter
cannot be detected; the [compatibility matrix](/rust/clap-compatibility) lists
those as **usage-only**.
for each detectable loss. `is_lossless()` therefore means lossless for behavior
visible through clap's public getters, not for every setter clap exposes. Before
treating the generated spec as fully compatible, audit the declaration against the
[compatibility matrix](/rust/clap-compatibility), especially its **usage-only** and
**lossy** bridge rows.

## Integration Pattern

Expand Down
2 changes: 1 addition & 1 deletion go/argv/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ _{bin}() {
case "$__usage_files" in
any) _files && __usage_ret=0 ;;
dirs) _files -/ && __usage_ret=0 ;;
executables) _files -g '*(*)' && __usage_ret=0 ;;
executables) _files -g '*(-/,*)' && __usage_ret=0 ;;
commands) _command_names && __usage_ret=0 ;;
esac
return $__usage_ret
Expand Down
3 changes: 3 additions & 0 deletions go/argv/script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ func TestTheScriptsWatchForTheMarkerTheRendererWrites(t *testing.T) {
if !strings.Contains(Script("mise", Fish), `test -d "$value"; or test -x "$value"`) {
t.Error("fish filters executable-path candidates")
}
if !strings.Contains(Script("mise", Zsh), `_files -g '*(-/,*)'`) {
t.Error("zsh keeps directories beside executable-path candidates")
}
if !strings.Contains(Script("mise", PowerShell), "-CommandType Application, ExternalScript") {
t.Error("powershell filters executable-path candidates")
}
Expand Down
Loading
Loading