From 09be421ea612f6cda45b97a9b6cd6673db8ea7b6 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Sun, 31 May 2026 13:46:28 +0900 Subject: [PATCH 1/2] fix(completion): keep global -C/--cd usable in task argument completion (#10069) The `run`/`tasks run` subcommands redeclare `-C`/`--cd` as their own non-global flags (cli::run::Run::cd, used for the task execution dir). In the generated usage completion spec this shadowed the root-level global flag, so the usage parser dropped it when descending into a mounted task subcommand and mis-parsed a leading `mise -C run ` as the task's positional `choices` argument, erroring with "Invalid choice for arg ...". Promote completion-spec flags that collide with a root-level global flag back to global on the mounted `run`/`tasks run` subcommands. Completion-spec only; clap runtime parsing, `mise run` behavior, and `--help` are unchanged. This fixes the symptom for the currently released `usage` parser; the underlying parser bug is tracked upstream in jdx/usage. Co-Authored-By: Claude Opus 4.8 (1M context) --- e2e/tasks/test_task_completion_global_cd | 31 ++++++++++++++++ src/cli/usage.rs | 46 ++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 e2e/tasks/test_task_completion_global_cd diff --git a/e2e/tasks/test_task_completion_global_cd b/e2e/tasks/test_task_completion_global_cd new file mode 100644 index 0000000000..52546bf5cc --- /dev/null +++ b/e2e/tasks/test_task_completion_global_cd @@ -0,0 +1,31 @@ +#!/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. + +cat <<'EOF' >mise.toml +[tools] +"usage" = { version = "latest", os = ["linux", "macos"] } + +[tasks."sample:run"] +usage = 'arg "" { choices "alpha" "beta" "gamma" }' +run = 'printf "%s" "$usage_profile"' +EOF + +mise usage >./mise.usage.kdl + +# baseline: completion of the profile arg works without the global flag +assert "mise exec -- usage complete-word --shell zsh -f ./mise.usage.kdl -- mise run sample:run -- ''" "alpha +beta +gamma" + +# regression: with the global -C flag before `run`, completion must still +# offer the choices (previously errored: Invalid choice for arg profile: -C) +assert "mise exec -- usage complete-word --shell zsh -f ./mise.usage.kdl -- mise -C . run sample:run -- ''" "alpha +beta +gamma" + +# also cover the `tasks run` path, which shares the same flags/mount +assert "mise exec -- usage complete-word --shell zsh -f ./mise.usage.kdl -- mise -C . tasks run sample:run -- ''" "alpha +beta +gamma" diff --git a/src/cli/usage.rs b/src/cli/usage.rs index bbf1b86c93..7d4cc8207f 100644 --- a/src/cli/usage.rs +++ b/src/cli/usage.rs @@ -2,6 +2,7 @@ use crate::cli::Cli; use clap::CommandFactory; use clap::builder::Resettable; use eyre::Result; +use std::collections::HashSet; /// Generate a usage CLI spec /// @@ -33,6 +34,51 @@ impl Usage { }); tasks_run.restart_token = Some(":::".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` 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. + let global_shorts: HashSet = spec + .cmd + .flags + .iter() + .filter(|f| f.global) + .flat_map(|f| f.short.iter().copied()) + .collect(); + let global_longs: HashSet = spec + .cmd + .flags + .iter() + .filter(|f| f.global) + .flat_map(|f| f.long.iter().cloned()) + .collect(); + let promote = |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)) + { + f.global = true; + } + } + }; + promote(spec.cmd.subcommands.get_mut("run").unwrap()); + promote( + spec.cmd + .subcommands + .get_mut("tasks") + .unwrap() + .subcommands + .get_mut("run") + .unwrap(), + ); + let min_version = r#"min_usage_version "2.11""#; let extra = include_str!("../assets/mise-extra.usage.kdl").trim(); println!("{min_version}\n{}\n{extra}", spec.to_string().trim()); From e6cd96501fe51f742d59520b5330cd786ee6c0c1 Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Sun, 31 May 2026 13:46:28 +0900 Subject: [PATCH 2/2] refactor(completion): use if let for subcommand lookups in usage spec Address review feedback: replace the .unwrap() subcommand lookups in the usage spec builder with graceful `if let` handling, and reuse the existing run/tasks run mutable bindings to promote colliding global flags (no extra lookups). No behavior change for the normal case; the spec generation now skips gracefully if a subcommand is ever absent instead of panicking. Co-Authored-By: Claude Opus 4.8 (1M context) --- mise.usage.kdl | 20 +++++++++---------- src/cli/usage.rs | 51 ++++++++++++++++++++++++------------------------ 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/mise.usage.kdl b/mise.usage.kdl index f172d4ef1d..e930f77c2e 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -873,11 +873,11 @@ cmd run restart_token=::: help="Run task(s)" { long_help "Run task(s)\n\nThis command will run a task, or multiple tasks in parallel.\nTasks may have dependencies on other tasks or on source files.\nIf source is configured on a task, it will only run if the source\nfiles have changed.\n\nTasks can be defined in mise.toml or as standalone scripts.\nIn mise.toml, tasks take this form:\n\n [tasks.build]\n run = \"npm run build\"\n sources = [\"src/**/*.ts\"]\n outputs = [\"dist/**/*.js\"]\n\nAlternatively, tasks can be defined as standalone scripts.\nThese must be located in `mise-tasks`, `.mise-tasks`, `.mise/tasks`, `mise/tasks` or\n`.config/mise/tasks`.\nThe name of the script will be the name of the tasks.\n\n $ cat .mise/tasks/build< } flag "-f --force" help="Force the tasks to run even if outputs are up to date" - flag "-j --jobs" help="Number of tasks to run in parallel\n[default: 4]\nConfigure with `jobs` config or `MISE_JOBS` env var" { + flag "-j --jobs" help="Number of tasks to run in parallel\n[default: 4]\nConfigure 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" @@ -885,13 +885,13 @@ cmd run restart_token=::: help="Run task(s)" { long_help "Change how tasks information is output when running tasks\n\n- `prefix` - Print stdout/stderr by line, prefixed with the task's label\n- `interleave` - Print directly to stdout/stderr instead of by line\n- `replacing` - Stdout is replaced each time, stderr is printed as is\n- `timed` - Only show stdout lines if they are displayed for more than 1 second\n- `keep-order` - Print stdout/stderr by line, prefixed with the task's label, but keep the order of the output\n- `quiet` - Don't show extra output\n- `silent` - Don't show any output including stdout and stderr from the task except for errors" arg } - flag "-q --quiet" help="Don't show extra output" - flag "-r --raw" help="Read/write directly to stdin/stdout/stderr instead of by line\nRedactions are not applied with this option\nConfigure with `raw` config or `MISE_RAW` env var" + flag "-q --quiet" help="Don't show extra output" global=#true + flag "-r --raw" help="Read/write directly to stdin/stdout/stderr instead of by line\nRedactions are not applied with this option\nConfigure with `raw` config or `MISE_RAW` env var" global=#true flag "-s --shell" help="Shell to use to run toml tasks" { long_help "Shell to use to run toml tasks\n\nDefaults to `sh -c -o errexit -o pipefail` on unix, and `cmd /c` on Windows\nCan also be set with the setting `MISE_UNIX_DEFAULT_INLINE_SHELL_ARGS` or `MISE_WINDOWS_DEFAULT_INLINE_SHELL_ARGS`\nOr it can be overridden with the `shell` property on a task." arg } - flag "-S --silent" help="Don't show any output except for errors" + flag "-S --silent" help="Don't show any output except for errors" global=#true flag "-t --tool" help="Tool(s) to run in addition to what is in mise.toml files e.g.: node@20 python@3.10" var=#true { arg } @@ -1206,11 +1206,11 @@ cmd tasks help="Manage tasks" { long_help "Run task(s)\n\nThis command will run a task, or multiple tasks in parallel.\nTasks may have dependencies on other tasks or on source files.\nIf source is configured on a task, it will only run if the source\nfiles have changed.\n\nTasks can be defined in mise.toml or as standalone scripts.\nIn mise.toml, tasks take this form:\n\n [tasks.build]\n run = \"npm run build\"\n sources = [\"src/**/*.ts\"]\n outputs = [\"dist/**/*.js\"]\n\nAlternatively, tasks can be defined as standalone scripts.\nThese must be located in `mise-tasks`, `.mise-tasks`, `.mise/tasks`, `mise/tasks` or\n`.config/mise/tasks`.\nThe name of the script will be the name of the tasks.\n\n $ cat .mise/tasks/build< } flag "-f --force" help="Force the tasks to run even if outputs are up to date" - flag "-j --jobs" help="Number of tasks to run in parallel\n[default: 4]\nConfigure with `jobs` config or `MISE_JOBS` env var" { + flag "-j --jobs" help="Number of tasks to run in parallel\n[default: 4]\nConfigure 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" @@ -1218,13 +1218,13 @@ cmd tasks help="Manage tasks" { long_help "Change how tasks information is output when running tasks\n\n- `prefix` - Print stdout/stderr by line, prefixed with the task's label\n- `interleave` - Print directly to stdout/stderr instead of by line\n- `replacing` - Stdout is replaced each time, stderr is printed as is\n- `timed` - Only show stdout lines if they are displayed for more than 1 second\n- `keep-order` - Print stdout/stderr by line, prefixed with the task's label, but keep the order of the output\n- `quiet` - Don't show extra output\n- `silent` - Don't show any output including stdout and stderr from the task except for errors" arg } - flag "-q --quiet" help="Don't show extra output" - flag "-r --raw" help="Read/write directly to stdin/stdout/stderr instead of by line\nRedactions are not applied with this option\nConfigure with `raw` config or `MISE_RAW` env var" + flag "-q --quiet" help="Don't show extra output" global=#true + flag "-r --raw" help="Read/write directly to stdin/stdout/stderr instead of by line\nRedactions are not applied with this option\nConfigure with `raw` config or `MISE_RAW` env var" global=#true flag "-s --shell" help="Shell to use to run toml tasks" { long_help "Shell to use to run toml tasks\n\nDefaults to `sh -c -o errexit -o pipefail` on unix, and `cmd /c` on Windows\nCan also be set with the setting `MISE_UNIX_DEFAULT_INLINE_SHELL_ARGS` or `MISE_WINDOWS_DEFAULT_INLINE_SHELL_ARGS`\nOr it can be overridden with the `shell` property on a task." arg } - flag "-S --silent" help="Don't show any output except for errors" + flag "-S --silent" help="Don't show any output except for errors" global=#true flag "-t --tool" help="Tool(s) to run in addition to what is in mise.toml files e.g.: node@20 python@3.10" var=#true { arg } diff --git a/src/cli/usage.rs b/src/cli/usage.rs index 7d4cc8207f..c96878e77e 100644 --- a/src/cli/usage.rs +++ b/src/cli/usage.rs @@ -19,21 +19,6 @@ impl Usage { // Enable "naked" task completions: `mise foo` completes like `mise run foo` spec.default_subcommand = Some("run".to_string()); - let run = spec.cmd.subcommands.get_mut("run").unwrap(); - run.args = vec![]; - run.mounts.push(usage::SpecMount { - run: "mise tasks --usage".to_string(), - }); - // Enable completions after ::: separator for multi-task invocations - run.restart_token = Some(":::".to_string()); - - let tasks = spec.cmd.subcommands.get_mut("tasks").unwrap(); - let tasks_run = tasks.subcommands.get_mut("run").unwrap(); - tasks_run.mounts.push(usage::SpecMount { - run: "mise tasks --usage".to_string(), - }); - tasks_run.restart_token = Some(":::".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. // @@ -45,6 +30,9 @@ impl Usage { // ...` 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. + // + // Collect the root global flag identifiers up front so the immutable borrow + // of `spec.cmd.flags` is released before the subcommands are borrowed mutably. let global_shorts: HashSet = spec .cmd .flags @@ -68,16 +56,29 @@ impl Usage { } } }; - promote(spec.cmd.subcommands.get_mut("run").unwrap()); - promote( - spec.cmd - .subcommands - .get_mut("tasks") - .unwrap() - .subcommands - .get_mut("run") - .unwrap(), - ); + + if let Some(run) = spec.cmd.subcommands.get_mut("run") { + run.args = vec![]; + run.mounts.push(usage::SpecMount { + run: "mise tasks --usage".to_string(), + }); + // Enable completions after ::: separator for multi-task invocations + run.restart_token = Some(":::".to_string()); + promote(run); + } + + if let Some(tasks_run) = spec + .cmd + .subcommands + .get_mut("tasks") + .and_then(|tasks| tasks.subcommands.get_mut("run")) + { + tasks_run.mounts.push(usage::SpecMount { + run: "mise tasks --usage".to_string(), + }); + tasks_run.restart_token = Some(":::".to_string()); + promote(tasks_run); + } let min_version = r#"min_usage_version "2.11""#; let extra = include_str!("../assets/mise-extra.usage.kdl").trim();