denoland/deno · error · anyhow::Error

task name filter ' ' uses an exclusion group '(!...)' but…

Error message

task name filter '{}' uses an exclusion group '(!...)' but has no wildcard '*' to exclude from

What it means

deno task accepts name filters with a trailing exclusion group, e.g. `test:*(!e2e|interactive)` — each capture made by a `*` is checked against the exclusion list (arg_to_task_name_filter, cli/tools/task.rs:1564-1594). An exclusion group is only meaningful when the pattern part contains a `*`; inputs like `build(!dev)` have nothing to exclude from, so the filter is rejected before any task runs.

Solutions

  1. Attach the exclusion to a wildcard family: `build:*(!dev)` excludes tasks whose capture equals dev.
  2. For "everything except", use the bare wildcard: `*(!dev)` matches every task except exactly `dev`.
  3. If you meant one exact task, drop the group entirely: `deno task build`.

Example fix

# before
$ deno task 'build(!dev)'
error: task name filter 'build(!dev)' uses an exclusion group '(!...)' but has no wildcard '*' to exclude from

# after — exclude one capture from a wildcard family
deno task 'build:*(!dev)'
# or run every task except `dev`
deno task '*(!dev)'
Defensive patterns

Strategy: validation

Validate before calling

// Validate a deno task filter before shelling out
function validateFilter(f: string): void {
  const m = f.match(/^(.*)\(!([^)]*)\)$/);
  if (!m) return; // no exclusion group
  const pattern = m[1];
  if (!pattern.includes("*")) {
    throw new Error(`exclusion group '(!...)' requires a '*' in the pattern: ${f}`);
  }
}

Type guard

const isExclusionWithWildcard = (f: string) =>
  !/\(![^)]*\)$/.test(f) || f.slice(0, f.lastIndexOf("(!")).includes("*");

Prevention

When it happens

Trigger: deno task 'build(!dev)', deno task 'check(!lint)' — any filter whose text before `(!` contains no wildcard; usually written by someone expecting npm-style negation ("run everything except").

Common situations: Assuming `(!x)` alone means "all tasks except x"; shell quoting that drops a `*` (unquoted globs get expanded by the shell); filters copy-pasted from docs without their wildcard prefix.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/abf61646b6fd132a. Report an issue: GitHub.

Appendix: source

Thrown at cli/tools/task.rs:1581

fn arg_to_task_name_filter(
  input: &str,
) -> Result<TaskNameFilter<'_>, AnyError> {
  // Parse an optional trailing exclusion group of the form `(!a|b|c)`.
  // Exclusion values are matched against what each `*` in the pattern
  // captures, e.g. `test:*(!e2e|interactive)` excludes `test:e2e` and
  // `test:interactive` but still matches `test:unit`.
  let (pattern, exclusions): (&str, Vec<&str>) = match input.rfind("(!") {
    Some(open) if input.ends_with(')') => {
      let inner = &input[open + 2..input.len() - 1];
      (&input[..open], inner.split('|').collect())
    }
    _ => (input, Vec::new()),
  };

  if !pattern.contains('*') {
    if !exclusions.is_empty() {
      return Err(anyhow!(
        "task name filter '{}' uses an exclusion group '(!...)' but has no wildcard '*' to exclude from",
        input
      ));
    }
    return Ok(TaskNameFilter::Exact(input));
  }

  let mut regex_str = regex::escape(pattern);
  regex_str = regex_str.replace("\\*", "(.*)");
  regex_str = format!("^{}", regex_str);
  let re = Regex::new(&regex_str)?;
  let exclusions = exclusions.into_iter().map(String::from).collect();
  Ok(TaskNameFilter::Regex { re, exclusions })
}

#[derive(Debug)]
enum TaskNameFilter<'s> {
  Exact(&'s str),

View on GitHub (pinned to 9ad36f7a2c)