Skip to content

fix: improve usage spec element support in tasks - #8623

Merged
jdx merged 3 commits into
jdx:mainfrom
nkakouros:nkakouros/fix/mise-usage
Mar 21, 2026
Merged

jdx merged 3 commits into
jdx:mainfrom
nkakouros:nkakouros/fix/mise-usage

Conversation

@nkakouros

@nkakouros nkakouros commented Mar 17, 2026 •

Copy link
Copy Markdown
Contributor

The PR greatly improves support for usage directives in mise tasks. Most of the changes are new tests.

Summary

  • Fix {{ usage.* }} templates erroring when optional args or flags are not provided — they now
    default to empty string, empty array, or false as appropriate
  • Fix count flags (-vvv) rendering as [true, true, true] instead of 3 in templates
  • Add cmd subcommand support: {{ usage.cmd }} exposes the selected subcommand name,
    $usage_cmd is set as an env var, and subcommand args/flags are accessible in templates
  • Fix mise run -p <value> being silently eaten by the hidden --prefix flag instead of
    being passed to the task

Test plan

  • Unit tests for all arg/flag/cmd template rendering scenarios
  • E2E tests for count, negate, boolean defaults, optional args, arg choices (test_task_usage_map_tera)
  • E2E tests for cmd subcommand routing, aliases, file-based tasks, global flags (test_task_usage_cmd)
  • cargo fmt, cargo check, shellcheck, shfmt all pass
  • Existing tests unmodified (except test_task_usage_hashmap updated to match fixed behavior)

I split the work into 3 commits in case some of the edits are controversial.

🤖 Generated with Claude Code

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves the flexibility and robustness of mise task definitions by enhancing how usage directives are parsed and rendered. It ensures a more predictable and user-friendly experience when working with optional arguments, flags, and introduces powerful subcommand capabilities, allowing for more complex and organized task structures.

Highlights

  • Enhanced usage Template Rendering: Fixed issues where {{ usage.* }} templates would error when optional arguments or flags were not provided, now defaulting to empty strings, empty arrays, or false as appropriate. This also includes a fix for count flags (e.g., -vvv) to render as an integer (3) instead of an array of booleans ([true, true, true]).
  • Added cmd Subcommand Support: Introduced support for defining subcommands within usage directives. The selected subcommand name is now exposed via {{ usage.cmd }} in Tera templates and as the $usage_cmd environment variable. Arguments and flags specific to subcommands are also accessible in templates.
  • Resolved mise run -p Flag Conflict: Addressed a bug where mise run -p <value> was being silently consumed by a hidden --prefix flag, preventing the value from being passed to the intended task.
Changelog
  • e2e/tasks/test_task_usage_cmd
    • Added comprehensive end-to-end tests for the new cmd subcommand functionality, covering basic routing, Tera template integration, subcommand aliases, handling of optional subcommands, file-based tasks, and global flags.
  • e2e/tasks/test_task_usage_map_tera
    • Extended end-to-end tests to validate the corrected rendering of count flags as integers, proper behavior of negate flags, and accurate default values for boolean flags and optional arguments in Tera templates.
  • src/cli/run.rs
    • Removed the short alias (-p) from the internal --prefix flag to prevent conflicts with user-defined task arguments.
  • src/task/task_executor.rs
    • Modified the condition for parsing usage specifications to include subcommands, ensuring that tasks with subcommands are correctly processed.
    • Implemented logic to export the selected subcommand name into the environment as $usage_cmd for use within task scripts.
  • src/task/task_script_parser.rs
    • Updated the Tera context creation to merge default values from the usage specification with parsed argument values, ensuring all declared arguments and flags are present in the template context with appropriate fallbacks.
    • Changed the rendering of MultiBool values (used for count flags) to represent the count as an integer instead of an array of booleans.
    • Added the selected subcommand name to the Tera context as {{ usage.cmd }}.
    • Refactored the make_usage_ctx_from_spec_defaults function to recursively collect default values for arguments and flags across all commands and subcommands, providing consistent default behavior.
    • Updated the has_any_args_defined function to also consider the presence of subcommands.
    • Removed a test case that previously expected an error for an unprovided flag, aligning with the new behavior where such flags now default to false.
Activity
  • The author split the work into 3 commits, indicating a structured approach to implementing these changes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@nkakouros nkakouros changed the title fix usage template rendering and add cmd subcommand support fix: improve usage spec element support in tasks Mar 17, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant improvements to usage directive handling in mise tasks. It fixes template rendering for optional arguments and flags by providing sensible defaults, correctly renders count flags as integers, and adds comprehensive support for subcommands, including access via {{ usage.cmd }} and $usage_cmd. Additionally, it resolves a command-line argument conflict with the --prefix flag. The changes are well-implemented and accompanied by extensive unit and end-to-end tests, which is great to see. My review found one minor opportunity for improvement by refactoring a small piece of duplicated logic into a shared function.

Comment thread src/task/task_script_parser.rs Outdated
@greptile-apps

greptile-apps Bot commented Mar 17, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR substantially improves usage directive support for mise tasks by fixing template rendering failures for optional/unset args and flags, correcting count-flag representation (MultiBool → integer), and adding first-class cmd subcommand support with both Tera ({{ usage.cmd }}) and env-var ($usage_cmd) exposure. The bulk of the change is new tests (unit and e2e), and the existing tests are updated to match the corrected behavior. The implementation is sound and well-tested.

Key changes:

  • make_usage_ctx_from_spec_defaults now recurses into subcommands so every possible {{ usage.X }} key is pre-populated before the template renders, preventing Tera "undefined variable" errors for optional/unprovided args and flags.
  • make_usage_ctx overwrites spec defaults with real parsed values via usage_map.extend(…), keeping the correct merge order.
  • MultiBool (count flags) is now serialised as a Number instead of an array of bools — this is a breaking change for any existing template that iterated or called | length on a count flag's array form.
  • $usage_cmd is unconditionally exported as an empty string when the spec has subcommands, preventing set -u crashes — but the same protection is not extended to the args/flags owned by those subcommands (see inline comment).
  • subcommand_name_from_parse joins all entries after cmds[0] with a space; since only flat subcommands are supported today, this works in practice but the implementation is slightly over-engineered for the current contract.

Confidence Score: 4/5

  • Safe to merge with awareness of the set -u gap for subcommand-scoped args and the breaking MultiBool → integer change.
  • The core logic is correct and well-tested; existing tests are green or intentionally updated. The one P1 concern (subcommand-scoped env vars unset when not matched, unlike $usage_cmd) mirrors a pattern the PR itself identifies and partially fixes. The MultiBool → integer change is a necessary correctness fix but is technically breaking for any template using array operations on count flags — whether this warrants a changelog entry depends on how broadly the feature was used before.
  • src/task/task_executor.rs — the $usage_cmd guard (lines 964–966) should ideally be extended to cover all subcommand-scoped args/flags for set -u safety.

Important Files Changed

Filename Overview
src/task/task_script_parser.rs Core logic for usage spec rendering; refactors make_usage_ctx_from_spec_defaults to recurse into subcommands, fixes MultiBool → integer, adds subcommand_name_from_parse, and pre-populates defaults before extending with real parsed values. Logic is sound; one consistency gap around subcommand-scoped args not being pre-initialized in env for set -u shells.
src/task/task_executor.rs Extends the env-population path to support subcommands; adds $usage_cmd unconditional export for set -u safety; wires up subcommand_name_from_parse. Changes are minimal and correct; the $usage_cmd guard is a good defensive fix.
e2e/tasks/test_task_usage_cmd New e2e test covering subcommand routing, aliases, file-based tasks, global flags, and the no-subcommand default case. Tests are well-structured; rm -rf mise-tasks cleanup is present. Some shell variable assertions rely on bash's silent unset-to-empty expansion rather than explicit empty-string exports.
e2e/tasks/test_task_usage_map_tera Extends the existing Tera template e2e test with count flags, negate flags, boolean defaults, optional args, and arg choices. Additions are clear and targeted; no issues found.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Task invoked with args] --> B{spec has args/flags/subcommands?}
    B -- No --> C[trace: no args/flags/subcommands]
    B -- Yes --> D[usage::Parser.parse args]
    D --> E[po.as_env → insert into env BTreeMap]
    E --> F{spec has subcommands?}
    F -- Yes --> G["env.entry('usage_cmd').or_default()"]
    G --> H{subcommand selected?}
    H -- Yes --> I["env.insert('usage_cmd', subcmd_name)"]
    H -- No --> J["usage_cmd = empty string"]
    F -- No --> K[skip usage_cmd handling]

    I --> L
    J --> L
    K --> L

    subgraph TeraRender["Tera Template Rendering"]
        L["make_usage_ctx_from_spec_defaults(spec)\nRecurses all subcommands → populate defaults/fallbacks"] --> M["make_usage_ctx(parsed_output)\nActual parsed values only"]
        M --> N["usage_map = defaults.extend(real_values)\nDefaults survive for unprovided optional args"]
    end

    N --> O["tera_ctx.insert('usage', usage_map)"]
    O --> P[Render script template]
Loading

Last reviewed commit: "Merge branch 'main' ..."

Comment thread e2e/tasks/test_task_usage_cmd
Comment thread src/task/task_executor.rs Outdated
Comment thread src/task/task_script_parser.rs
Comment thread src/task/task_executor.rs
@nkakouros
nkakouros marked this pull request as draft March 17, 2026 10:19
@nkakouros
nkakouros force-pushed the nkakouros/fix/mise-usage branch 2 times, most recently from 622fb6e to 2b4f3ad Compare March 17, 2026 11:18
- Seed `{{ usage.* }}` context with spec defaults before overlaying
  parsed values, so optional args/flags don't error when not provided.
- Render count flags as integers (e.g. `-vvv` → `3`, not `[true, true, true]`).
- Fix count flag default from `[]` to `0`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nkakouros
nkakouros force-pushed the nkakouros/fix/mise-usage branch from cb0aa8a to 4cf96d3 Compare March 17, 2026 21:47
@nkakouros
nkakouros marked this pull request as ready for review March 17, 2026 23:03
@nkakouros

Copy link
Copy Markdown
Contributor Author

Failing test seems unrelated.

@iamkroot

Copy link
Copy Markdown
Contributor

Fix mise run -p being silently eaten by the hidden --prefix flag instead of

What did prefix do?
Isn't this the wrong outcome? How would we provide args to mise run?

@nkakouros
nkakouros force-pushed the nkakouros/fix/mise-usage branch from cdcd72d to 9b53d6d Compare March 21, 2026 01:23
Comment thread src/task/task_script_parser.rs
Comment thread src/task/task_script_parser.rs
@nkakouros

Copy link
Copy Markdown
Contributor Author

Fix mise run -p being silently eaten by the hidden --prefix flag instead of

What did prefix do? Isn't this the wrong outcome? How would we provide args to mise run?

I removed the commit that removed -p and opened #8669. See the description there for why -p and some other hidden flags should and is ok to remove.

- Expose selected subcommand as `{{ usage.cmd }}` in Tera templates
  and `$usage_cmd` in env vars.
- Always export `$usage_cmd` (empty string) when the spec has
  subcommands, so `set -u` scripts don't fail when none is chosen.
- Extract `subcommand_name_from_parse` helper shared between
  task_script_parser and task_executor.
- Recurse into subcommand specs when building default template context,
  so `{{ usage.X }}` works for args/flags defined inside `cmd` blocks.
  (SpecCommand::subcommands is an IndexMap, so iteration order is
  deterministic.)
- Include `spec.cmd.subcommands` in the `has_any_args_defined` check
  so tasks with only subcommands trigger parsing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@nkakouros
nkakouros force-pushed the nkakouros/fix/mise-usage branch from 12614fd to 6709132 Compare March 21, 2026 02:08
Comment on lines +823 to +826
// expose selected subcommand as {{ usage.cmd }}
if let Some(subcmd) = subcommand_name_from_parse(&usage.cmds) {
usage_ctx.insert("cmd".to_string(), tera::Value::String(subcmd));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 "cmd" key silently overwrites an arg named cmd

make_usage_ctx unconditionally inserts "cmd" into the Tera context after the arg/flag loop. If a spec has an arg literally named <cmd> and also defines subcommands, the arg's parsed value is silently overwritten by the subcommand name:

for (arg, val) in &usage.args {
    usage_ctx.insert(arg.name.to_snake_case(), tera_val); // may insert "cmd"
}
// …
if let Some(subcmd) = subcommand_name_from_parse(&usage.cmds) {
    usage_ctx.insert("cmd".to_string(), …);  // silently clobbers it
}

The same clobbering happens in make_usage_ctx_from_spec_defaults (after collect_cmd_defaults the explicit "cmd" = "" insertion overwrites any <cmd> arg default). The same potential collision also exists for a flag named --cmd.

Since "cmd" is effectively a reserved key in the {{ usage.* }} namespace when subcommands are present, consider either:

  • Documenting this reserved-key restriction so authors know not to name args/flags cmd when using subcommands, or
  • Emitting a warning/error if the spec contains an arg or flag whose snake_case name is "cmd" and subcommands are also defined.

Comment on lines 800 to +803
use tera::Value;
use usage::parse::ParseValue::*;
match val {
MultiBool(v) => Value::Array(v.iter().map(|b| Value::Bool(*b)).collect()),
MultiBool(v) => Value::Number(serde_json::Number::from(v.len())),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 MultiBool → integer is a breaking change for existing templates

Before this PR, a count flag like -vvv rendered as [true, true, true] in Tera, so authors who wrote {{ usage.verbose | length }} or {% for _ in usage.verbose %} were technically relying on the array representation. Changing MultiBool to emit a Number is the correct fix, but any existing templates using array filters (length, join, iteration) on a count flag will now fail with a Tera type error at runtime.

If there are any docs or a migration guide, a note about this behavioral change would help existing users upgrade. Otherwise consider whether a semver bump or changelog entry is warranted.

Comment thread e2e/tasks/test_task_usage_map_tera
Comment thread src/task/task_executor.rs
Comment on lines +962 to +970
// 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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 $usage_cmd guard not extended to subcommand-scoped args/flags

The fix at lines 964–966 ensures $usage_cmd is always exported as an empty string when the spec has subcommands (protecting shell scripts that use set -u). However, the same protection is not applied to the args and flags that belong to those subcommands.

Consider the mixed task in the e2e test (lines 90–115 of test_task_usage_cmd):

  • Running mise run mixed staging selects the root arg <env> but no subcommand, so $usage_version (a subcommand arg) is never exported by po.as_env().
  • Running mise run mixed deploy v1.0 selects the deploy subcommand, so $usage_env (the root-level arg) is never exported.

Any task script that uses set -u (or set -euo pipefail) will crash with "unbound variable" in these cases. The e2e assertions like "cmd= env=staging version=" pass today only because bash silently expands unset variables to empty without set -u.

A defensive fix (similar to the usage_cmd guard) would iterate over the spec's args/flags across all subcommands and ensure they are pre-inserted with empty defaults before po.as_env() runs — or at a minimum document that set -u is not supported when mixing root-level args with subcommands.

Comment on lines +904 to 913
/// Extract the selected subcommand name from parsed commands.
/// `cmds[0]` is the root command; subsequent entries are subcommands.
pub fn subcommand_name_from_parse(cmds: &[usage::SpecCommand]) -> Option<String> {
if cmds.len() > 1 {
let names: Vec<String> = cmds.iter().skip(1).map(|c| c.name.clone()).collect();
Some(names.join(" "))
} else {
None
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 subcommand_name_from_parse collects all depths unnecessarily

The function collects and joins all entries after cmds[0] with a space. Given that mise tasks only support flat (one-level) subcommands today, cmds will have at most two entries and the names.join(" ") degenerates to a single-element join. The slice-construction and collect are unnecessary for the current use-case, and the implementation is slightly misleading about what values could appear.

A simpler and more explicit form that pins the contract to exactly one subcommand level would be:

Suggested change
/// Extract the selected subcommand name from parsed commands.
/// `cmds[0]` is the root command; subsequent entries are subcommands.
pub fn subcommand_name_from_parse(cmds: &[usage::SpecCommand]) -> Option<String> {
if cmds.len() > 1 {
let names: Vec<String> = cmds.iter().skip(1).map(|c| c.name.clone()).collect();
Some(names.join(" "))
} else {
None
}
}
pub fn subcommand_name_from_parse(cmds: &[usage::SpecCommand]) -> Option<String> {
// cmds[0] is the root command; cmds[1] is the selected subcommand (flat only).
cmds.get(1).map(|c| c.name.clone())
}

This also avoids returning a space-containing string if the usage library ever gains nested-subcommand support, which would silently break case "$usage_cmd" in patterns in shell tasks.

'''
run = '''
echo "color={{ usage.color }}"
'''

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 -- Alice double-dash separator is unnecessary here

The -- end-of-options separator is only needed when a positional value could be misinterpreted as a flag (e.g. a value starting with -). Alice is a plain string that would match [name] directly without the separator.

Using -- Alice here may mislead task authors into thinking the separator is always required for optional args, when the corresponding unit test in test_usage_arg_defaults_for_unprovided already passes &["x"] without one.

Suggested change
'''
assert "mise run usage-tera-opt-arg Alice" "hello Alice"

@jdx
jdx merged commit 6069e60 into jdx:main Mar 21, 2026
34 checks passed
mise-en-dev added a commit that referenced this pull request Mar 22, 2026
### 🚀 Features

- **(github)** read tokens from gh CLI hosts.yml config by @jdx in
[#8692](#8692)
- **(task)** support optional `args` and `env` fields in `run` entries
by @jdx in [#8687](#8687)
- **(task)** add --skip-tools flag to mise run by @jdx in
[#8699](#8699)
- **(vfox)** add try_get, try_head, try_download_file to Lua HTTP module
by @jdx in [#8697](#8697)

### 🐛 Bug Fixes

- **(config)** recognize SSH and other non-HTTPS URLs in get_repo_url by
@modestman in [#8666](#8666)
- **(docs)** add dark mode support to favicon by @jdx in
[#8678](#8678)
- **(env)** support multiple --env/-E flags by @jdx in
[#8686](#8686)
- **(github)** rename_exe renames correct binary when archive contains
multiple executables by @jdx in
[#8700](#8700)
- **(implode)** include system data dir in implode cleanup by @jdx in
[#8696](#8696)
- **(install)** skip GitHub API calls for aqua tools in --locked mode by
@jdx in [#8679](#8679)
- **(install)** skip redundant provenance verification when lockfile has
integrity data by @jdx in [#8688](#8688)
- **(lock)** respect existing platforms in lockfile when running `mise
lock` by @jdx in [#8708](#8708)
- **(lock)** skip global config lockfile by default by @jdx in
[#8707](#8707)
- **(node)** expand tilde in default_packages_file path by @jdx in
[#8709](#8709)
- **(shell)** error when no version specified instead of silent no-op by
@jdx in [#8693](#8693)
- **(shim)** detect shims by checking shims directory instead of binary
name by @jdx in [#8694](#8694)
- **(task)** inherit task_config.dir for included TOML and file tasks by
@jdx in [#8689](#8689)
- **(task)** strip inline args when validating run.tasks references by
@jdx in [#8701](#8701)
- **(task)** include idiomatic version files in monorepo task toolset by
@jdx in [#8702](#8702)
- **(task)** improve error message when task files are not executable by
@jdx in [#8705](#8705)
- **(test)** update vfox provenance test for checksum-backed skip by
@jdx in [#8703](#8703)
- improve usage spec element support in tasks by @nkakouros in
[#8623](#8623)
- make env plugin (Module) vars available in Tera template context by
@victor-founder in [#8682](#8682)
- respect MISE_COLOR=0 for color_eyre error output by @jdx in
[#8690](#8690)
- add windows support for usage tool registry by @jdx in
[#8713](#8713)

### 📚 Documentation

- **(task)** clarify interactive task blocking behavior by @jdx in
[#8685](#8685)
- improve visibility of install_before setting by @jdx in
[#8712](#8712)

### 📦 Registry

- add rtk ([github:rtk-ai/rtk](https://github.com/rtk-ai/rtk)) by
@bricelalu in [#8683](#8683)

### New Contributors

- @victor-founder made their first contribution in
[#8682](#8682)
- @modestman made their first contribution in
[#8666](#8666)
- @bricelalu made their first contribution in
[#8683](#8683)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants