Skip to content

fix(task): populate usage.cmd for subcommand-only tasks; share make_usage_ctx#9431

Merged
jdx merged 1 commit intomainfrom
fix/task-usage-cmd-subcommand-only
Apr 27, 2026
Merged

fix(task): populate usage.cmd for subcommand-only tasks; share make_usage_ctx#9431
jdx merged 1 commit intomainfrom
fix/task-usage-cmd-subcommand-only

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Apr 27, 2026

Summary

Follow-up to #9424 addressing two issues raised in review (Cursor Bugbot):

  1. Early-return bugparse_usage_values_from_task returned an empty map when both spec.cmd.args and spec.cmd.flags were empty, before reaching the new subcommand handling. Tasks defined with only subcommands (no top-level args/flags) that referenced {{ usage.cmd }} in deps got an empty IndexMap, so the value never landed in the Tera context. Fixed by including spec.cmd.subcommands.is_empty() in the early-return condition.

  2. Duplicated conversion logicto_tera_value and the args/flags-to-snake-case iteration in parse_usage_values_from_task duplicated TaskScriptParser::make_usage_ctx, and the two had already drifted slightly. Promoted make_usage_ctx to pub(crate) and reused it. Preserved the defensive empty-cmd insertion when subcommands are defined but none was selected.

Test plan

  • mise run format
  • mise run lint
  • cargo test --bin mise task::tests (34 tests pass)
  • mise run test:e2e e2e/tasks/test_task_dep_args (includes new regression test for subcommand-only deps via {{ usage.cmd }})
  • mise run test:e2e e2e/tasks/test_task_usage_cmd (no regression in pre-existing subcommand behavior)

🤖 Generated with Claude Code


Note

Low Risk
Low risk: small, localized change to usage-context extraction plus an added e2e regression test; behavior only differs for tasks whose usage defines subcommands without root args/flags.

Overview
Fixes dependency-template rendering for subcommand-only tasks by ensuring parse_usage_values_from_task no longer early-returns before considering subcommands, and by reusing TaskScriptParser::make_usage_ctx to build the usage map.

When a task defines subcommands but none is selected, the code now guarantees usage.cmd is present (as an empty string) so {{ usage.cmd }} references in dependency templates consistently resolve.

Adds an e2e regression test covering depends templates that branch on usage.cmd for subcommand-only usage specs.

Reviewed by Cursor Bugbot for commit 97fa3ff. Bugbot is set up for automated code reviews on this repo. Configure here.

…sage_ctx

The early-return in parse_usage_values_from_task fired when both
spec.cmd.args and spec.cmd.flags were empty, before the new subcommand
handling. Tasks that defined only subcommands and referenced
{{ usage.cmd }} in deps got an empty IndexMap, so the value never
reached the Tera context.

Fix the early-return condition to also consider subcommands, then drop
the duplicated to_tera_value/snake_case iteration by reusing
TaskScriptParser::make_usage_ctx (now pub(crate)). Preserve the
defensive empty-cmd insertion when subcommands are defined but none was
selected, matching the existing behavior callers rely on.

Add an e2e regression test exercising the subcommand-only path via
{{ usage.cmd }} in a depends template.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR fixes two issues in the task dependency template subsystem: (1) an early-return bug in parse_usage_values_from_task that caused {{ usage.cmd }} to resolve to nothing for tasks whose specs define only subcommands, and (2) dead duplication between parse_usage_values_from_task and TaskScriptParser::make_usage_ctx that had already started to drift. The fix is minimal, well-explained, and backed by a targeted regression test.

Confidence Score: 5/5

Safe to merge — targeted bug fix with no regressions and a new regression test covering the fixed path.

Both changes (early-return guard and de-duplication) are small, correct, and mirrored by existing logic in make_usage_ctx_from_spec_defaults. The e2e regression test validates the specific failure mode. No P0/P1 issues found.

No files require special attention.

Important Files Changed

Filename Overview
src/task/mod.rs Early-return guard updated to include subcommands; duplicated conversion logic replaced with reuse of TaskScriptParser::make_usage_ctx; defensive empty-cmd insertion preserved. Unused heck::ToSnakeCase import cleaned up.
src/task/task_script_parser.rs make_usage_ctx promoted from private to pub(crate) so it can be shared with mod.rs; no logic changes to the function itself.
e2e/tasks/test_task_dep_args Regression test added for subcommand-only dependency templates using {{ usage.cmd }}; covers both dispatch paths and negative assertions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[parse_usage_values_from_task] --> B{spec has args\nOR flags\nOR subcommands?}
    B -- No --> C[return empty IndexMap]
    B -- Yes --> D[parse args via usage::Parser]
    D -- parse error --> C
    D -- Ok --> E[make_usage_ctx &po\nargs + flags + cmd]
    E --> F{spec has subcommands\nAND cmd key absent?}
    F -- Yes --> G[insert cmd = empty string]
    F -- No --> H[return values]
    G --> H

    style C fill:#f9f,stroke:#333
    style H fill:#9f9,stroke:#333
Loading

Reviews (1): Last reviewed commit: "fix(task): populate usage.cmd for subcom..." | Re-trigger Greptile

@jdx jdx enabled auto-merge (squash) April 27, 2026 15:20
Copy link
Copy Markdown
Contributor

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

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 addresses a regression where usage.cmd was not correctly populated for tasks that only define subcommands, which caused issues in dependency templates. The changes refactor parse_usage_values_from_task to utilize TaskScriptParser::make_usage_ctx and ensure that usage.cmd is initialized as an empty string when subcommands are present but none are selected. Feedback was provided regarding a missing fallback in the error handling path and the need to maintain deterministic ordering of arguments and flags when converting from a HashMap to an IndexMap.

Comment thread src/task/mod.rs
Comment on lines 2082 to 2093
return Ok(IndexMap::new());
}
};
let mut values = IndexMap::new();
let to_tera_value = |val: &usage::parse::ParseValue| -> tera::Value {
use tera::Value;
use usage::parse::ParseValue::*;
match val {
MultiBool(v) => Value::Number(serde_json::Number::from(v.len())),
MultiString(v) => Value::Array(v.iter().map(|s| Value::String(s.clone())).collect()),
Bool(v) => Value::Bool(*v),
String(v) => Value::String(v.clone()),
}
};
for (arg, val) in &po.args {
values.insert(arg.name.to_snake_case(), to_tera_value(val));
}
for (flag, val) in &po.flags {
values.insert(flag.name.to_snake_case(), to_tera_value(val));
}
if !spec.cmd.subcommands.is_empty() {
let cmd = po.cmds.iter().skip(1).map(|c| c.name.clone()).join(" ");
values.insert("cmd".to_string(), tera::Value::String(cmd));
let mut values: IndexMap<String, tera::Value> =
TaskScriptParser::make_usage_ctx(&po).into_iter().collect();
// `make_usage_ctx` only inserts `cmd` when a subcommand was actually selected.
// Templates referencing `{{ usage.cmd }}` should still resolve (to "") when
// subcommands are defined in the spec but none was selected.
if !spec.cmd.subcommands.is_empty() && !values.contains_key("cmd") {
values.insert("cmd".to_string(), tera::Value::String(String::new()));
}
Ok(values)
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.

high

The current implementation misses the usage.cmd fallback in the Err case (line 2082). If a task with subcommands is called with invalid or missing arguments, usage::Parser::parse will fail, and the function will return an empty map. This causes Tera rendering errors in dependency templates that expect usage.cmd to be defined.

Additionally, reusing make_usage_ctx (which returns a HashMap) and collecting it into an IndexMap introduces non-determinism in the order of arguments/flags in the Tera context. While lookups are unaffected, any iteration over usage will be unordered, which is a regression from the previous implementation. I've added a .sorted_by_key() call with an owned key to ensure determinism and avoid lifetime issues in the closure.

Suggested change
return Ok(IndexMap::new());
}
};
let mut values = IndexMap::new();
let to_tera_value = |val: &usage::parse::ParseValue| -> tera::Value {
use tera::Value;
use usage::parse::ParseValue::*;
match val {
MultiBool(v) => Value::Number(serde_json::Number::from(v.len())),
MultiString(v) => Value::Array(v.iter().map(|s| Value::String(s.clone())).collect()),
Bool(v) => Value::Bool(*v),
String(v) => Value::String(v.clone()),
}
};
for (arg, val) in &po.args {
values.insert(arg.name.to_snake_case(), to_tera_value(val));
}
for (flag, val) in &po.flags {
values.insert(flag.name.to_snake_case(), to_tera_value(val));
}
if !spec.cmd.subcommands.is_empty() {
let cmd = po.cmds.iter().skip(1).map(|c| c.name.clone()).join(" ");
values.insert("cmd".to_string(), tera::Value::String(cmd));
let mut values: IndexMap<String, tera::Value> =
TaskScriptParser::make_usage_ctx(&po).into_iter().collect();
// `make_usage_ctx` only inserts `cmd` when a subcommand was actually selected.
// Templates referencing `{{ usage.cmd }}` should still resolve (to "") when
// subcommands are defined in the spec but none was selected.
if !spec.cmd.subcommands.is_empty() && !values.contains_key("cmd") {
values.insert("cmd".to_string(), tera::Value::String(String::new()));
}
Ok(values)
let mut values = IndexMap::new();
if !spec.cmd.subcommands.is_empty() {
values.insert("cmd".to_string(), tera::Value::String(String::new()));
}
return Ok(values);
}
};
let mut values: IndexMap<String, tera::Value> = TaskScriptParser::make_usage_ctx(&po)
.into_iter()
.sorted_by_key(|(k, _)| k.clone())
.collect();
// make_usage_ctx only inserts cmd when a subcommand was actually selected.
// Templates referencing {{ usage.cmd }} should still resolve (to "") when
// subcommands are defined in the spec but none was selected.
if !spec.cmd.subcommands.is_empty() && !values.contains_key("cmd") {
values.insert("cmd".to_string(), tera::Value::String(String::new()));
}
Ok(values)
References
  1. The key returned by closures in iteration or sorting methods should be an owned value to prevent lifetime errors where the key needs to outlive the closure call.

@github-actions
Copy link
Copy Markdown

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.24 x -- echo 21.9 ± 0.4 21.2 24.2 1.00
mise x -- echo 22.6 ± 0.5 21.5 24.4 1.03 ± 0.03

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.24 env 21.7 ± 0.6 20.8 27.2 1.00
mise env 22.1 ± 0.3 21.4 23.7 1.02 ± 0.03

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.24 hook-env 22.5 ± 0.5 21.4 26.5 1.00
mise hook-env 23.6 ± 0.6 22.0 26.1 1.05 ± 0.04

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.24 ls 23.6 ± 0.5 22.4 25.7 1.00
mise ls 23.6 ± 0.6 22.4 25.8 1.00 ± 0.03

xtasks/test/perf

Command mise-2026.4.24 mise Variance
install (cached) 156ms 160ms -2%
ls (cached) 79ms 82ms -3%
bin-paths (cached) 81ms 82ms -1%
task-ls (cached) 827ms 805ms +2%

@jdx jdx merged commit b157a15 into main Apr 27, 2026
38 checks passed
@jdx jdx deleted the fix/task-usage-cmd-subcommand-only branch April 27, 2026 15:53
mise-en-dev added a commit that referenced this pull request Apr 28, 2026
### 🚀 Features

- **(task)** add --name-only flag to mise tasks ls by @jdx in
[#9435](#9435)

### 🐛 Bug Fixes

- **(Dockerfile)** install copr-cli via dnf for better dependency
management by @bestagi in [#9421](#9421)
- **(aqua)** drop empty-releases fallback to tags by @jdx in
[#9443](#9443)
- **(docs)** fix theme flicker on docs by @vhespanha in
[#9427](#9427)
- **(lockfile)** update global lockfile on upgrade by @jdx in
[#9442](#9442)
- **(ls-remote)** omit rolling/prerelease from JSON when false by @jdx
in [#9439](#9439)
- **(task)** support usage refs in dependency template tags by @jdx in
[#9424](#9424)
- **(task)** populate usage.cmd for subcommand-only tasks; share
make_usage_ctx by @jdx in [#9431](#9431)
- **(task)** resolve sandbox allow_read/allow_write against task dir by
@jdx in [#9428](#9428)

### 📚 Documentation

- **(site)** add self-hosted page tracker via Cloudflare Worker, drop
GoatCounter by @jdx in [#9430](#9430)

### New Contributors

- @vhespanha made their first contribution in
[#9427](#9427)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
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.

1 participant