diff --git a/e2e/tasks/test_task_completion_global_cd b/e2e/tasks/test_task_completion_global_cd index dd5f926605..a971f11649 100644 --- a/e2e/tasks/test_task_completion_global_cd +++ b/e2e/tasks/test_task_completion_global_cd @@ -1,7 +1,12 @@ #!/usr/bin/env bash # Regression test for https://github.com/jdx/mise/discussions/10069 -# Completion must work when the global -C/--cd flag precedes `run` and the -# task has a positional arg with choices. +# Completion must work when a root global flag (e.g. -C/--cd) precedes `run`, or +# a `run` flag precedes the task, when the task has a positional arg with +# choices. -C/--cd, -j/--jobs and -q/--quiet are handled by the upstream usage +# parser fix (jdx/usage#649). -r/--raw and -S/--silent still need mise's +# completion-spec promotion: their root globals are long-only, so the parser +# would drop the -r/-S shorts that only exist on the non-global `run` +# redeclarations. cat <<'EOF' >mise.toml [tools] @@ -29,3 +34,21 @@ gamma gamma" assert "mise exec -- usage complete-word --shell zsh -f ./mise.usage.kdl -- mise -C . tasks run sample:run -- ''" "alpha alpha beta beta gamma gamma" + +# orphan-short flags: -r/--raw and -S/--silent have no short on the root global, +# so usage#649 alone would drop the short. The completion-spec promotion keeps +# them recognized before the mounted task (previously errored: unexpected word). +assert "mise exec -- usage complete-word --shell zsh -f ./mise.usage.kdl -- mise run -r sample:run -- ''" "alpha alpha +beta beta +gamma gamma" +assert "mise exec -- usage complete-word --shell zsh -f ./mise.usage.kdl -- mise run -S sample:run -- ''" "alpha alpha +beta beta +gamma gamma" + +# and the same two over the `tasks run` path +assert "mise exec -- usage complete-word --shell zsh -f ./mise.usage.kdl -- mise tasks run -r sample:run -- ''" "alpha alpha +beta beta +gamma gamma" +assert "mise exec -- usage complete-word --shell zsh -f ./mise.usage.kdl -- mise tasks run -S sample:run -- ''" "alpha alpha +beta beta +gamma gamma" diff --git a/mise.usage.kdl b/mise.usage.kdl index 13b7d5ec74..108f348b54 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -1,4 +1,4 @@ -min_usage_version "2.11" +min_usage_version "3.4" name mise bin mise about "Dev tools, env vars, and tasks in one CLI" @@ -2279,7 +2279,7 @@ Examples: """# flag "-c --continue-on-error" help="Continue running tasks even if one fails" - flag "-C --cd" help="Change to this directory before executing the command" global=#true { + flag "-C --cd" help="Change to this directory before executing the command" { arg } flag "-f --force" help="Force the tasks to run even if outputs are up to date" @@ -2287,7 +2287,7 @@ Examples: Number of tasks to run in parallel [default: 4] Configure with `jobs` config or `MISE_JOBS` env var -"""# global=#true { +"""# { arg } flag "-n --dry-run" help="Don't actually run the task(s), just print them in order of execution" @@ -2305,7 +2305,7 @@ Change how tasks information is output when running tasks """# arg } - flag "-q --quiet" help="Don't show extra output" global=#true + flag "-q --quiet" help="Don't show extra output" flag "-r --raw" help=#""" Read/write directly to stdin/stdout/stderr instead of by line Redactions are not applied with this option @@ -3074,7 +3074,7 @@ Examples: """# flag "-c --continue-on-error" help="Continue running tasks even if one fails" - flag "-C --cd" help="Change to this directory before executing the command" global=#true { + flag "-C --cd" help="Change to this directory before executing the command" { arg } flag "-f --force" help="Force the tasks to run even if outputs are up to date" @@ -3082,7 +3082,7 @@ Examples: Number of tasks to run in parallel [default: 4] Configure with `jobs` config or `MISE_JOBS` env var -"""# global=#true { +"""# { arg } flag "-n --dry-run" help="Don't actually run the task(s), just print them in order of execution" @@ -3100,7 +3100,7 @@ Change how tasks information is output when running tasks """# arg } - flag "-q --quiet" help="Don't show extra output" global=#true + flag "-q --quiet" help="Don't show extra output" flag "-r --raw" help=#""" Read/write directly to stdin/stdout/stderr instead of by line Redactions are not applied with this option diff --git a/src/cli/usage.rs b/src/cli/usage.rs index c96878e77e..05bd08f36f 100644 --- a/src/cli/usage.rs +++ b/src/cli/usage.rs @@ -19,20 +19,24 @@ impl Usage { // Enable "naked" task completions: `mise foo` completes like `mise run foo` spec.default_subcommand = Some("run".to_string()); - // Promote completion-spec flags that collide with a root-level global flag - // (e.g. `-C`/`--cd`) to global on the mounted `run`/`tasks run` subcommands. + // The `run`/`tasks run` subcommands redeclare some root globals as their own + // non-global flags (see `cli::run::Run`). When the usage parser descends into + // a mounted task subcommand it keeps only inherited global flags, so a leading + // `mise -C run ` (or `mise run `) used to mis-parse + // the redeclared flag during completion. See mise#10069. // - // The `run` subcommand redeclares some root globals as its own non-global - // flags (notably `-C`/`--cd`, see `cli::run::Run::cd`). When the usage parser - // descends into a mounted task subcommand it keeps only `global` flags - // (`available_flags.retain(|_, f| f.global)`), so the non-global redeclaration - // causes the inherited global to be dropped. A leading `mise -C run - // ...` then mis-parses `-C` as the task's positional arg during - // completion. Marking the colliding flags global here (completion-spec only, - // no effect on clap runtime parsing) keeps them recognized. See mise#10069. + // jdx/usage#649 fixes the common case in the parser: when a subcommand + // redeclares an inherited global as non-global, the inherited global (with all + // its aliases) is now preserved. That covers `-C`/`--cd`, `-j`/`--jobs`, and + // `-q`/`--quiet`, whose short *is* a root global short, so no spec workaround + // is needed for them anymore. // - // Collect the root global flag identifiers up front so the immutable borrow - // of `spec.cmd.flags` is released before the subcommands are borrowed mutably. + // It cannot cover `-r`/`--raw` and `-S`/`--silent`: the root globals are + // long-only (`--raw`/`--silent` have no short, see `cli::Cli`), and the `-r`/ + // `-S` shorts live only on the non-global `run` redeclarations. The parser + // preserves the short-less root global and drops the redeclaration, losing the + // short. So we still promote just those "orphan short" flags to global in the + // completion spec (spec-only; clap runtime parsing is unchanged). let global_shorts: HashSet = spec .cmd .flags @@ -47,11 +51,13 @@ impl Usage { .filter(|f| f.global) .flat_map(|f| f.long.iter().cloned()) .collect(); - let promote = |cmd: &mut usage::SpecCommand| { + // Promote a redeclared flag only when it carries a short alias that the + // matching root global lacks (so jdx/usage#649 would otherwise drop it). + let promote_orphan_shorts = |cmd: &mut usage::SpecCommand| { for f in cmd.flags.iter_mut() { - if f.short.iter().any(|c| global_shorts.contains(c)) - || f.long.iter().any(|l| global_longs.contains(l)) - { + let long_is_root_global = f.long.iter().any(|l| global_longs.contains(l)); + let has_orphan_short = f.short.iter().any(|c| !global_shorts.contains(c)); + if long_is_root_global && has_orphan_short { f.global = true; } } @@ -64,7 +70,7 @@ impl Usage { }); // Enable completions after ::: separator for multi-task invocations run.restart_token = Some(":::".to_string()); - promote(run); + promote_orphan_shorts(run); } if let Some(tasks_run) = spec @@ -77,10 +83,13 @@ impl Usage { run: "mise tasks --usage".to_string(), }); tasks_run.restart_token = Some(":::".to_string()); - promote(tasks_run); + promote_orphan_shorts(tasks_run); } - let min_version = r#"min_usage_version "2.11""#; + // Require usage >= 3.4, the release that ships the jdx/usage#649 parser fix + // (see jdx/usage#652). This guards old `usage` CLIs from silently + // re-triggering the mise#10069 mis-parse without the fix. + let min_version = r#"min_usage_version "3.4""#; let extra = include_str!("../assets/mise-extra.usage.kdl").trim(); println!("{min_version}\n{}\n{extra}", spec.to_string().trim()); Ok(())