Repository navigation
fix: improve usage spec element support in tasks #8623
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| #!/usr/bin/env bash | ||
|
|
||
| # Test usage `cmd` (subcommand) support in task definitions | ||
|
|
||
| # Basic subcommand routing with env vars ($usage_cmd, $usage_*) | ||
| cat <<'EOF' >mise.toml | ||
| [tasks.deploy] | ||
| description = "Deploy task with subcommands" | ||
| usage = ''' | ||
| cmd "staging" { | ||
| arg "<version>" | ||
| flag "--dry-run" | ||
| } | ||
| cmd "production" { | ||
| arg "<version>" | ||
| flag "--approve" | ||
| } | ||
| ''' | ||
| run = ''' | ||
| echo "cmd=$usage_cmd version=$usage_version" | ||
| ''' | ||
| EOF | ||
|
|
||
| assert "mise run deploy staging v1.0" "cmd=staging version=v1.0" | ||
| assert "mise run deploy production v2.0" "cmd=production version=v2.0" | ||
|
|
||
| # Subcommand with Tera templates ({{ usage.cmd }}, {{ usage.* }}) | ||
| cat <<'EOF' >mise.toml | ||
| [tasks.manage] | ||
| description = "Manage resources" | ||
| usage = ''' | ||
| cmd "create" { | ||
| arg "<name>" | ||
| flag "--type <type>" default="default" | ||
| } | ||
| cmd "delete" { | ||
| arg "<name>" | ||
| flag "--force" | ||
| } | ||
| ''' | ||
| run = ''' | ||
| echo "action={{ usage.cmd }} name={{ usage.name }} type={{ usage.type }}" | ||
| {% if usage.force %} | ||
| echo "force=true" | ||
| {% endif %} | ||
| ''' | ||
| EOF | ||
|
|
||
| assert "mise run manage create myresource" "action=create name=myresource type=default" | ||
| assert "mise run manage create myresource --type custom" "action=create name=myresource type=custom" | ||
| assert "mise run manage delete myresource --force" "\ | ||
| action=delete name=myresource type=default | ||
| force=true" | ||
|
|
||
| # Subcommand with alias | ||
| cat <<'EOF' >mise.toml | ||
| [tasks.db] | ||
| description = "Database operations" | ||
| usage = ''' | ||
| cmd "migrate" { | ||
| alias "m" | ||
| flag "--seed" | ||
| } | ||
| ''' | ||
| run = ''' | ||
| echo "cmd=$usage_cmd" | ||
| ''' | ||
| EOF | ||
|
|
||
| assert "mise run db migrate" "cmd=migrate" | ||
| assert "mise run db m" "cmd=migrate" | ||
|
|
||
| # No subcommand selected β usage.cmd defaults to empty | ||
| cat <<'EOF' >mise.toml | ||
| [tasks.tool] | ||
| description = "Tool with optional subcommand" | ||
| usage = ''' | ||
| arg "<input>" | ||
| cmd "check" { | ||
| arg "<target>" | ||
| } | ||
| ''' | ||
| run = ''' | ||
| echo "cmd={{ usage.cmd }} input={{ usage.input }}" | ||
| ''' | ||
| EOF | ||
|
|
||
| assert "mise run tool hello" "cmd= input=hello" | ||
|
|
||
| # Mixed: root-level arg + subcommand with its own arg | ||
| cat <<'EOF' >mise.toml | ||
| [tasks.mixed] | ||
| description = "Root args and subcommands together" | ||
| usage = ''' | ||
| arg "<env>" | ||
| cmd "deploy" { | ||
| arg "<version>" | ||
| } | ||
| cmd "rollback" { | ||
| arg "<version>" | ||
| } | ||
| ''' | ||
| run = ''' | ||
| echo "cmd=$usage_cmd env=$usage_env version=$usage_version" | ||
| ''' | ||
| EOF | ||
|
|
||
| assert "mise run mixed staging" "cmd= env=staging version=" | ||
| assert "mise run mixed deploy v1.0" "cmd=deploy env= version=v1.0" | ||
|
|
||
| # File-based task with #USAGE cmd | ||
| mkdir -p mise-tasks | ||
| cat <<'SCRIPT' >mise-tasks/runner | ||
| #!/usr/bin/env bash | ||
| #USAGE cmd "lint" { | ||
| #USAGE arg "<path>" | ||
| #USAGE } | ||
| #USAGE cmd "test" { | ||
| #USAGE arg "<path>" | ||
| #USAGE flag "--coverage" | ||
| #USAGE } | ||
|
|
||
| echo "cmd=$usage_cmd path=$usage_path" | ||
| SCRIPT | ||
| chmod +x mise-tasks/runner | ||
|
|
||
| assert "mise run runner lint src/" "cmd=lint path=src/" | ||
| assert "mise run runner test src/ --coverage" "cmd=test path=src/" | ||
|
|
||
| rm -rf mise-tasks | ||
|
|
||
| # Root-level flags with subcommands require global=#true | ||
| cat <<'EOF' >mise.toml | ||
| [tasks.build] | ||
| description = "Build with parallelism" | ||
| usage = ''' | ||
| flag "-p --parallel <parallel>" default="1" global=#true | ||
| cmd "debug" { | ||
| arg "<target>" | ||
| } | ||
| cmd "release" { | ||
| arg "<target>" | ||
| } | ||
| ''' | ||
| run = 'echo "cmd=$usage_cmd target=$usage_target parallel=$usage_parallel"' | ||
| EOF | ||
|
|
||
| assert "mise run build debug myapp --parallel 4" "cmd=debug target=myapp parallel=4" | ||
| assert "mise run build debug myapp -p 4" "cmd=debug target=myapp parallel=4" | ||
| assert "mise run build debug myapp -p4" "cmd=debug target=myapp parallel=4" | ||
| assert "mise run build -p 4 release myapp" "cmd=release target=myapp parallel=4" | ||
| assert "mise run build debug myapp" "cmd=debug target=myapp parallel=1" | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -207,3 +207,91 @@ dirname=/path/to | |||||
| basename=file.txt | ||||||
| extname=txt | ||||||
| file_stem=file" | ||||||
|
|
||||||
| # Count flag renders as integer (e.g. -vvv -> 3) | ||||||
| cat <<'EOF' >mise.toml | ||||||
| [tasks.usage-tera-count] | ||||||
| description = "Test count flag renders as integer" | ||||||
| usage = ''' | ||||||
| flag "-v --verbose" count=#true | ||||||
| ''' | ||||||
| run = ''' | ||||||
| echo "verbosity={{ usage.verbose }}" | ||||||
| ''' | ||||||
| EOF | ||||||
|
|
||||||
| assert "mise run usage-tera-count" "verbosity=0" | ||||||
| assert "mise run usage-tera-count -v" "verbosity=1" | ||||||
| assert "mise run usage-tera-count -vvv" "verbosity=3" | ||||||
|
|
||||||
| # Negate flag (--no-color sets to false, default is true) | ||||||
| cat <<'EOF' >mise.toml | ||||||
| [tasks.usage-tera-negate] | ||||||
| description = "Test negate flag" | ||||||
| usage = ''' | ||||||
| flag "--color" negate="--no-color" default=#true | ||||||
| ''' | ||||||
| run = ''' | ||||||
| echo "color={{ usage.color }}" | ||||||
| ''' | ||||||
|
greptile-apps[bot] marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Using
Suggested change
|
||||||
| EOF | ||||||
|
|
||||||
| assert "mise run usage-tera-negate" "color=true" | ||||||
| assert "mise run usage-tera-negate --color" "color=true" | ||||||
| assert "mise run usage-tera-negate --no-color" "color=false" | ||||||
|
|
||||||
| # Boolean flag defaults to false when not provided (unprovided flag in template) | ||||||
| cat <<'EOF' >mise.toml | ||||||
| [tasks.usage-tera-bool-default] | ||||||
| description = "Test boolean flag defaults to false" | ||||||
| usage = ''' | ||||||
| flag "-f --force" | ||||||
| ''' | ||||||
| run = ''' | ||||||
| {% if usage.force %} | ||||||
| echo "forced" | ||||||
| {% else %} | ||||||
| echo "not forced" | ||||||
| {% endif %} | ||||||
| ''' | ||||||
| EOF | ||||||
|
|
||||||
| assert "mise run usage-tera-bool-default" "not forced" | ||||||
| assert "mise run usage-tera-bool-default --force" "forced" | ||||||
|
|
||||||
| # Optional arg defaults to empty string when not provided | ||||||
| cat <<'EOF' >mise.toml | ||||||
| [tasks.usage-tera-opt-arg] | ||||||
| description = "Test optional arg defaults to empty" | ||||||
| usage = ''' | ||||||
| arg "[name]" | ||||||
| ''' | ||||||
| run = ''' | ||||||
| {% if usage.name %} | ||||||
| echo "hello {{ usage.name }}" | ||||||
| {% else %} | ||||||
| echo "hello world" | ||||||
| {% endif %} | ||||||
| ''' | ||||||
| EOF | ||||||
|
|
||||||
| assert "mise run usage-tera-opt-arg" "hello world" | ||||||
| assert "mise run usage-tera-opt-arg -- Alice" "hello Alice" | ||||||
|
|
||||||
| # Arg choices restrict allowed values | ||||||
| cat <<'EOF' >mise.toml | ||||||
| [tasks.usage-tera-arg-choices] | ||||||
| description = "Test arg choices" | ||||||
| usage = ''' | ||||||
| arg "<shell>" { | ||||||
| choices "bash" "zsh" "fish" | ||||||
| } | ||||||
| ''' | ||||||
| run = ''' | ||||||
| echo "shell={{ usage.shell }}" | ||||||
| ''' | ||||||
| EOF | ||||||
|
|
||||||
| assert "mise run usage-tera-arg-choices bash" "shell=bash" | ||||||
| assert "mise run usage-tera-arg-choices zsh" "shell=zsh" | ||||||
| assert_fail "mise run usage-tera-arg-choices powershell" | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ use crate::task::task_context_builder::TaskContextBuilder; | |
| use crate::task::task_list::split_task_spec; | ||
| use crate::task::task_output::{TaskOutput, trunc}; | ||
| use crate::task::task_output_handler::OutputHandler; | ||
| use crate::task::task_script_parser::subcommand_name_from_parse; | ||
| use crate::task::task_source_checker::{save_checksum, sources_are_fresh, task_cwd}; | ||
| use crate::task::{Deps, FailedTasks, GetMatchingExt, Task}; | ||
| use crate::toolset::env_cache::CachedEnv; | ||
|
|
@@ -941,7 +942,10 @@ impl TaskExecutor { | |
| let (spec, _) = task | ||
| .parse_usage_spec_with_vars(config, self.cd.clone(), env, extra_vars) | ||
| .await?; | ||
| if !spec.cmd.args.is_empty() || !spec.cmd.flags.is_empty() { | ||
| if !spec.cmd.args.is_empty() | ||
| || !spec.cmd.flags.is_empty() | ||
| || !spec.cmd.subcommands.is_empty() | ||
| { | ||
| let args: Vec<String> = get_args(); | ||
| trace!("Parsing usage spec for {:?}", args); | ||
| // Pass env vars to Parser so it can resolve env= defaults in usage specs | ||
|
|
@@ -955,8 +959,17 @@ impl TaskExecutor { | |
| trace!("Adding key {} value {} in env", k, v); | ||
| env.insert(k, v); | ||
| } | ||
| // always export $usage_cmd when spec has subcommands so | ||
| // shell scripts with `set -u` don't fail when none is chosen | ||
| if !spec.cmd.subcommands.is_empty() { | ||
| env.entry("usage_cmd".to_string()).or_default(); | ||
| } | ||
| if let Some(subcmd) = subcommand_name_from_parse(&po.cmds) { | ||
| trace!("Adding key usage_cmd value {} in env", subcmd); | ||
| env.insert("usage_cmd".to_string(), subcmd); | ||
| } | ||
|
Comment on lines
+962
to
+970
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The fix at lines 964β966 ensures Consider the
Any task script that uses A defensive fix (similar to the |
||
| } else { | ||
| trace!("Usage spec has no args or flags"); | ||
| trace!("Usage spec has no args, flags, or subcommands"); | ||
| } | ||
|
nkakouros marked this conversation as resolved.
|
||
|
|
||
| Ok(()) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.