Skip to content

Support for multi-pattern search with -e/--pattern - #12

Merged
dannote merged 2 commits into
elixir-vibe:masterfrom
rodrigues:vr/search_many
Jul 22, 2026
Merged

Support for multi-pattern search with -e/--pattern#12
dannote merged 2 commits into
elixir-vibe:masterfrom
rodrigues:vr/search_many

Conversation

@rodrigues

Copy link
Copy Markdown
Contributor

This PR is based on #6

With this mix ex_ast.search can run many patterns in one invocation — one BEAM boot, one parse per file for the whole batch — via a repeatable -e/--pattern flag, mirroring grep -e / rg -e.

The library already supported batching (ExAST.search_many/3, which parses each file once and tags every match with a :pattern); this fills the CLI gap. Analyzers and linters that run dozens of checks over the same tree stop paying startup + re-parse per pattern.

The single-pattern path (ExAST.search/3) is untouched — the task branches early on whether any -e was given.

Invocation Behavior
'PATTERN' path/ Single pattern, exactly as before
-e A -e B path/ Batch: one boot, one parse per file, matches tagged by pattern
-e A --inside X -e B Selector filters bind to the most recent -e (grep model)
'PATTERN' -e A Error — can't mix a positional pattern with -e

Per-pattern selector filters

Selector filters (--inside, --not-inside, --parent, --contains, …) are per-pattern: each -e compiles its own Selector, and filters don't bleed across patterns.

mix ex_ast.search \
  -e 'App.Repo.get!(_, _)' --inside 'def handle_call(_, _, _) do _ end' \
  -e 'IO.inspect(_)' --not-inside 'test _ do _ end' \
  lib/ test/

OptionParser's :keep loses interleaving order, so argv is segmented manually — each -e plus the filters that follow it up to the next -e form one segment. Global flags (--count, --json, --expand-imports, --limit, --allow-broad, paths) apply to the whole batch.

Before

-e wasn't a flag — batching was only reachable from the library:

$ mix ex_ast.search -e "IO.inspect(_)" -e "dbg(_)" lib
** (Mix) Could not invoke task "ex_ast.search": 1 error found!
-e : Unknown option

After

$ mix ex_ast.search -e "IO.inspect(_)" -e "dbg(_)" lib/ex_ast/cli/json.ex
[dbg(_)] lib/ex_ast/cli/json.ex:37
  dbg(value)

1 pattern(s), 1 match(es)

--count prints a per-pattern tally plus a total:

$ mix ex_ast.search -e "IO.inspect(_)" -e "dbg(_)" -e "Enum.member?(...)" lib --count
1  IO.inspect(_)
1  dbg(_)
0  Enum.member?(...)

2 match(es) across 3 pattern(s)

--json carries the pattern field on each match (already present on search_many results).
Patterns are tagged by their raw string; duplicate -e strings raise rather than silently merging.
--count-by-file is rejected with -e (per-pattern file totals aren't meaningful across a shared batch) rather than silently ignored.

@dannote dannote left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for all the PRs, Victor — lots of good ideas here. I found a few issues with this one that need fixing before it can go in.

{head, segments} = segment_argv(args)

{global_opts, head_positional, _} =
OptionParser.parse(head, strict: @global_switches ++ SelectorOptions.switches())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Selector flags before the first -e end up in global_opts and are silently ignored. --inside X -e P file runs P unfiltered. Since the rule is that filters bind to the preceding -e, I think this should error. Could you add validation and a test for it?

Comment thread lib/mix/tasks/ex_ast.search.ex Outdated
selector = SelectorOptions.scoped_pattern(pattern, filter_opts, &validate_pattern!/1)

{[%{pattern: pattern, selector: selector} | compiled], global_opts ++ seg_globals,
positional ++ paths}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This reverses path order. One segment with [a.ex, b.ex] becomes [b.ex, a.ex] after the final reverse, which matters with --limit. Could you preserve argv order and add a two-file test?

Comment thread lib/mix/tasks/ex_ast.search.ex Outdated

defp build_named_patterns!(compiled) do
Enum.reduce(compiled, [], fn %{pattern: str, selector: selector}, acc ->
key = String.to_atom(str)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We shouldn't create atoms from CLI input. This leaks atoms in long-running VMs, and a valid pattern over 255 bytes raises SystemLimitError here. Could you use bounded internal IDs and map them back to the original strings?

Let `mix ex_ast.search` run several patterns in one invocation via a
repeatable `-e`/`--pattern` flag, reading and parsing each file once for
the whole batch through `ExAST.search_many/3` — avoiding BEAM startup and
per-file re-parsing per pattern.

- No `-e`: unchanged single-pattern path via `ExAST.search/3`.
- One or more `-e`: multi-pattern batch; positional pattern is rejected.
- Selector filters (`--inside`, `--parent`, etc.) bind to the most recent
  preceding `-e`, grep-style. argv is segmented manually since OptionParser
  loses interleaving order, then each segment compiles its own Selector via
  the new `SelectorOptions.scoped_pattern/3`.
- Global flags (`--count`, `--json`, `--expand-imports`, `--limit`,
  `--allow-broad`, paths) apply to the whole batch.
- Output tags each match by its pattern string; `--count` gives a
  per-pattern tally plus total; `--json` carries the `:pattern` field.
- Duplicate `-e` patterns raise; `--count-by-file` is rejected with `-e`.

Per-pattern path scoping is not expressible in one call (shared path list);
noted in the @moduledoc.
@rodrigues
rodrigues requested a review from dannote July 21, 2026 19:28
@dannote
dannote merged commit 6c92e74 into elixir-vibe:master Jul 22, 2026
2 checks passed
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.

2 participants