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
22 changes: 22 additions & 0 deletions e2e/tasks/test_task_wildcard_no_self_match
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

# Test that task glob pattern "test:*" does not match the "test" task itself.
# This prevents circular invocation when a parent task delegates to its children.
# See: https://github.com/jdx/mise/discussions/8138

cat <<'EOF' >mise.toml
[tasks."test:foo"]
run = 'echo foo'

[tasks."test:bar"]
run = 'echo bar'

[tasks.test]
run = [
{ task = "test:*" },
]
EOF

# "mise run test" should run test:foo and test:bar without circular dependency
assert_contains "mise run test" "foo"
assert_contains "mise run test" "bar"
33 changes: 31 additions & 2 deletions src/task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1335,6 +1335,7 @@ where
// Split pattern into path and task parts
// Pattern format: //path/...:task* or //path:task*
let parts: Vec<&str> = normalized_pat.splitn(2, ':').collect();
let has_explicit_task_glob = parts.len() > 1;
let (path_pattern, task_pattern) = match parts.as_slice() {
[path, task] => (*path, *task),
[path] => (*path, "*"),
Expand Down Expand Up @@ -1415,8 +1416,13 @@ where
};

// Match task part with asterisk support and extension stripping
// When the pattern explicitly uses a wildcard after `:` (e.g., "test:*"),
// require the key to actually have a task part (i.e., contain a `:`
// separator). This prevents "test" from matching "test:*", which would
// cause circular dependencies. Implicit wildcards (bare names like "test")
// should still match the exact task.
let task_matches = if task_glob == "*" {
true
!has_explicit_task_glob || !key_task.is_empty()
} else if let Some(ref matcher) = task_matcher {
// Check exact match OR match without extension
matcher.is_match(key_task) || matcher.is_match(strip_extension(key_task))
Expand All @@ -1441,7 +1447,7 @@ where
};

let rel_task_matches = if task_glob == "*" {
true
!has_explicit_task_glob || !stripped_task.is_empty()
} else if let Some(ref matcher) = rel_task_matcher {
// Check exact match OR match without extension
matcher.is_match(stripped_task)
Expand Down Expand Up @@ -2341,4 +2347,27 @@ echo "test"
assert_eq!(task.tools.get("git-cliff").unwrap(), "1.0");
assert_eq!(task.tools.get("1password-cli").unwrap(), "2.0");
}

#[test]
fn test_get_matching_wildcard_does_not_match_parent() {
use std::collections::BTreeMap;

use super::GetMatchingExt;

let mut tasks: BTreeMap<String, String> = BTreeMap::new();
tasks.insert("test".to_string(), "test".to_string());
tasks.insert("test:foo".to_string(), "test:foo".to_string());
tasks.insert("test:bar".to_string(), "test:bar".to_string());

// "test:*" should match "test:foo" and "test:bar" but NOT "test" itself
let matches = tasks.get_matching("test:*").unwrap();
assert_eq!(
matches,
vec![&"test:bar".to_string(), &"test:foo".to_string()]
);

// Bare name "test" should still match the "test" task (implicit wildcard)
let matches = tasks.get_matching("test").unwrap();
assert!(matches.contains(&&"test".to_string()));
}
}
Loading