can1357/oh-my-pi · error · ToolError

Line-range selector requires a single file, not a glob: ${en

Error message

Line-range selector requires a single file, not a glob: ${entry}

What it means

A line-range selector like ':50-100' narrows matches to specific lines, which only makes sense for exactly one file. If the path part of the entry contains glob characters (* ? [ ] etc.), grep refuses to combine the two.

Source

Thrown at packages/coding-agent/src/tools/grep.ts:194

			specs.push({ original: entry, clean: internalSplit.path, ranges: selectorLineRanges(internalSplit.sel) });
			continue;
		}
		// Prefer a literal filesystem match when one exists — a real file named
		// `test:1-2` outranks the `:1-2` selector interpretation (issue #4618).
		const strictSplit = splitPathAndSel(entry);
		const split = await splitPathAndSelPreferringLiteral(entry, cwd);
		const literalFilesystemMatch = strictSplit.sel !== undefined && split.sel === undefined;
		let clean = literalFilesystemMatch ? resolveReadPath(entry, cwd) : entry;
		let ranges: [LineRange, ...LineRange[]] | undefined;
		if (!literalFilesystemMatch && split.sel) {
			const parsed = parseLineRanges(split.sel);
			if (!parsed) {
				throw new ToolError(
					`path entry "${entry}" — only line-range selectors like ":50-100" are supported (no ":raw"/":conflicts")`,
				);
			}
			if (hasGlobPathChars(split.path)) {
				throw new ToolError(`Line-range selector requires a single file, not a glob: ${entry}`);
			}
			clean = split.path;
			ranges = parsed;
		}
		specs.push({
			original: entry,
			clean,
			literalFilesystemMatch,
			ranges,
		});
	}
	return specs;
}

function mergeRangesInto(map: Map<string, LineRange[]>, absKey: string, ranges: readonly LineRange[]): void {
	// Concat-without-merge is correct: `isLineInRanges` scans linearly, so
	// duplicates/overlaps only cost a few extra comparisons per match.
	const existing = map.get(absKey);

View on GitHub (pinned to 9690622007)

Solutions

  1. Remove the line-range selector and grep the glob without it, or
  2. Run separate grep calls per file with the ':N-M' selector
  3. Filter matched lines yourself after a glob-wide search

Example fix

// before
await grepTool.execute({ pattern: "foo", path: "src/**/*.ts:10-20" })
// after
await grepTool.execute({ pattern: "foo", path: "src/specific.ts:10-20" })
Defensive patterns

Strategy: validation

Validate before calling

function validateLineRangeEntry(entry: string): string {
  const idx = entry.lastIndexOf(":");
  if (idx > 0 && /^\d+(-\d+)?$/.test(entry.slice(idx + 1))) {
    const p = entry.slice(0, idx);
    if (/[*?\[]/.test(p)) throw new Error(`Line-range selector requires a single file: ${entry}`);
  }
  return entry;
}

Type guard

function isGlobPath(p: string): boolean {
  return /[*?\[]/.test(p);
}

Try / catch

try {
  return await grepTool.execute({ pattern, path: entry });
} catch (err) {
  if (err instanceof ToolError && err.message.startsWith("Line-range selector requires a single file")) {
    return await grepTool.execute({ pattern, path: entry.split(":").slice(0, -1).join(":") });
  }
  throw err;
}

Prevention

When it happens

Trigger: Entries like 'src/**/*.ts:10-20' — a glob pattern with a line-range suffix.

Common situations: Trying to grep a line window across many files at once; appending ranges to an existing glob search string.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/7fb9dbc0c9f3fdeb. Report an issue: GitHub.