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
31 changes: 31 additions & 0 deletions e2e/tasks/test_task_completion_global_cd
Original file line number Diff line number Diff line change
@@ -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 "<profile>" { 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 <dir> 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"
20 changes: 10 additions & 10 deletions mise.usage.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -873,25 +873,25 @@ 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<<EOF\n #!/usr/bin/env bash\n npm run build\n EOF\n $ mise run build"
after_long_help "Examples:\n\n # Runs the \"lint\" tasks. This needs to either be defined in mise.toml\n # or as a standalone script. See the project README for more information.\n $ mise run lint\n\n # Forces the \"build\" tasks to run even if its sources are up-to-date.\n $ mise run --force build\n\n # Run \"test\" with stdin/stdout/stderr all connected to the current terminal.\n # This forces `--jobs=1` to prevent interleaving of output.\n $ mise run --raw test\n\n # Runs the \"lint\", \"test\", and \"check\" tasks in parallel.\n $ mise run lint ::: test ::: check\n\n # Execute multiple tasks each with their own arguments.\n $ mise run cmd1 arg1 arg2 ::: cmd2 arg1 arg2\n"
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" {
flag "-C --cd" help="Change to this directory before executing the command" global=#true {
arg <CD>
}
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 <JOBS>
}
flag "-n --dry-run" help="Don't actually run the task(s), just print them in order of execution"
flag "-o --output" help="Change how tasks information is output when running 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 <OUTPUT>
}
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 <SHELL>
}
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 <TOOL@VERSION>
}
Expand Down Expand Up @@ -1206,25 +1206,25 @@ 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<<EOF\n #!/usr/bin/env bash\n npm run build\n EOF\n $ mise run build"
after_long_help "Examples:\n\n # Runs the \"lint\" tasks. This needs to either be defined in mise.toml\n # or as a standalone script. See the project README for more information.\n $ mise run lint\n\n # Forces the \"build\" tasks to run even if its sources are up-to-date.\n $ mise run --force build\n\n # Run \"test\" with stdin/stdout/stderr all connected to the current terminal.\n # This forces `--jobs=1` to prevent interleaving of output.\n $ mise run --raw test\n\n # Runs the \"lint\", \"test\", and \"check\" tasks in parallel.\n $ mise run lint ::: test ::: check\n\n # Execute multiple tasks each with their own arguments.\n $ mise run cmd1 arg1 arg2 ::: cmd2 arg1 arg2\n"
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" {
flag "-C --cd" help="Change to this directory before executing the command" global=#true {
arg <CD>
}
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 <JOBS>
}
flag "-n --dry-run" help="Don't actually run the task(s), just print them in order of execution"
flag "-o --output" help="Change how tasks information is output when running 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 <OUTPUT>
}
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 <SHELL>
}
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 <TOOL@VERSION>
}
Expand Down
73 changes: 60 additions & 13 deletions src/cli/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand All @@ -18,20 +19,66 @@ 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());
// 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 <dir> run
// <task> ...` 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<char> = spec
.cmd
.flags
.iter()
.filter(|f| f.global)
.flat_map(|f| f.short.iter().copied())
.collect();
let global_longs: HashSet<String> = 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;
}
}
};

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());
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();
Expand Down
Loading