Skip to content

--path <pattern> is documented as a "pattern" but actually does literal substring containment — --path "*.py" returns 0 results, only --path ".py" works (no glob support, breaks every conventional CLI expectation) #195

Description

@Widthdom

Summary

Every cdidx command that takes --path <pattern> (search, find, definition, references, callers, callees, symbols, files, map, inspect, outline, unused, hotspots, impact, deps) implements --path as literal substring containment — the user's input is wrapped as '%' || EscapeLikeQuery(input) || '%' and matched with SQL LIKE ... ESCAPE '\\', with % and _ escaped so they cannot act as wildcards.

The CLI help and CLAUDE.md both call the parameter <pattern>. Every conventional CLI tool (rg, find, git ls-files, fd, grep --include, etc.) uses glob patterns for path filters, so a user passing --path "*.py" reasonably expects "match Python files". cdidx silently returns zero results because no path on disk literally contains *.py as a substring.

This is one of the most damaging silent-failure modes for AI consumers: the agent infers from the docs that --path "*.py" should narrow to Python files, sees zero matches, and concludes "the file/symbol doesn't exist" — even though it does.

Repro

mkdir /tmp/glob && cd /tmp/glob
cat > a.py <<'EOF'
def hello(): pass
EOF
cat > b.py <<'EOF'
def hello(): pass
EOF
/root/.local/bin/cdidx index .

Six representative inputs, each with the LIKE shape that cdidx generates:

User input SQL LIKE pattern Result on the corpus above
--path "*.py" '%*.py%' 0 matches (no path literally contains *.py)
--path "**/*.py" '%**/*.py%' 0 matches
--path "src/*.py" '%src/*.py%' 0 matches
--path "%.py" '%\%.py%' (escaped) 0 matches (% is escaped before LIKE sees it)
--path "%py%" '%\%py\%%' (escaped) 0 matches
--path ".py" '%.py%' 2 matches (works because .py is a substring of every file)
$ /root/.local/bin/cdidx find hello --path "*.py"
No matches found.
Hint: try broadening --path or adding another --path value; --path is required for find.

$ /root/.local/bin/cdidx find hello --path ".py"
a.py:1:5
     1: def hello(): pass
b.py:1:5
     1: def hello(): pass

The user has to invent a non-glob "what substring is in all my paths" rule. That works for files in subdirectories (--path "src/" to scope to a directory), but breaks immediately for anything fancier — --path "src/*.py" returns 0, you have to express it as --path "src/" --path ".py" (which is OR not AND, so it's also wrong: it returns files matching "src/" OR ".py", not src directory's Python files).

A second, related symptom: there's no way to express a glob like --path "*.test.ts" either — the user has to use --path ".test.ts" and accept mid-word collisions (e.g. manifest.test.tsx matches anything containing .test.ts).

Suspected root cause (from reading the source)

Three lines of code combine to produce this:

src/CodeIndex/Database/DbReader.cs:338-341:

internal static string EscapeLikeQuery(string input)
{
    return input.Replace("\\", "\\\\").Replace("%", "\\%").Replace("_", "\\_");
}

src/CodeIndex/Database/DbReader.cs:1764-1766:

for (int i = 0; i < pathPatterns.Count; i++)
    ors.Add($"f.path LIKE @pathPattern{i} ESCAPE '\\'");
sql += " AND (" + string.Join(" OR ", ors) + ")";

src/CodeIndex/Database/DbReader.cs:1783-1784:

for (int i = 0; i < pathPatterns.Count; i++)
    cmd.Parameters.AddWithValue($"@pathPattern{i}", $"%{EscapeLikeQuery(pathPatterns[i])}%");

The shape is LIKE '%' || EscapeLikeQuery(input) || '%' ESCAPE '\\'. Because EscapeLikeQuery neutralizes % and _, the user's input cannot contain LIKE wildcards. The leading/trailing % produce substring containment.

Suggested direction

Three reasonable fixes, listed in order of increasing scope. Pick one and document it loudly.

Option A — translate glob to LIKE (smallest user-visible change)

Detect * / ? in the input and translate to LIKE:

  • *%
  • ?_
  • everything else → EscapeLikeQuery-style escaped
  • (optional) **% (or strip — same semantics in LIKE since % already crosses /)

Then don't wrap with extra % on either side if the user already provided wildcards (otherwise --path "*.py" becomes LIKE '%%.py%' which would match a.py.txt too). Anchored interpretation: if the user provides * anywhere, treat the pattern as anchored on whichever side they didn't put a *.

This is the smallest behavior change consistent with what every other CLI tool does, and matches user/AI expectations.

Option B — true glob with Microsoft.Extensions.FileSystemGlobbing

Use the proper Matcher (already in BCL via NuGet, but adding a dependency violates the cdidx single-dependency rule — out of scope without explicit user approval). Skip unless there's a strong reason.

Option C — keep substring semantics, but rename the flag and document

If "substring containment" is the intended semantics, rename --path <pattern> to --path <substring> and document it explicitly in --help, README, CLAUDE.md, and MCP tool schemas. Then the AI consumer at least knows the rules. (This is the smallest code change but the worst usability for everyone.)

I'd recommend Option A. It satisfies AI/CLI-tool conventions with one helper function and no new dependency.

Sibling observations (related, not blocking)

Scope

  • src/CodeIndex/Database/DbReader.cs:338, 1764-1791 — implement glob→LIKE translator and adjust the wrapping logic. Probably introduce a BuildPathLikePattern(input) helper that returns the final SQL LIKE string + handles wildcard detection.
  • All command help text — clarify --path semantics.
  • README (English + Japanese) — document with examples.
  • DEVELOPER_GUIDE.md and CLAUDE.md design-decisions section — note the chosen semantics.
  • MCP tool schemas — same semantics, same docs.
  • tests/CodeIndex.Tests/QueryCommandRunnerTests.cs — add tests for --path "*.py", --path "src/*.py", --path "src/" (no wildcard, anchor implied), --path "**/*.test.ts", and the empty-string case.

Notes for reviewer

I lack a local .NET SDK in this cloud session, so I can't compile/test the fix. The change is mechanical (glob→LIKE conversion) and has clear test cases; CI is required to confirm the SQL behavior across SQLite versions.

Related

Environment

  • cdidx: v1.10.0 (install.sh).
  • Platform: linux-x64 container.
  • Filed from a cloud Claude Code session per CLOUD_BOOTSTRAP_PROMPT.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions