Files
wehub-resource-sync bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

329 lines
13 KiB
Python

from collections.abc import (AsyncIterator, Awaitable, Callable, Mapping,
Sequence)
from dataclasses import dataclass
from functools import partial
from mirage.accessor.base import Accessor
from mirage.cache.index import IndexCacheStore
from mirage.cache.read_through import (cache_aware_read_bytes,
cache_aware_read_stream)
from mirage.commands.builtin.grep_helper import ( # yapf: disable
compile_pattern, count_exit_stream, count_records_have_matches,
grep_files_only, grep_lines, grep_recursive, grep_stream, prefix_lines,
resolve_pattern)
from mirage.commands.builtin.utils.lines import split_lines
from mirage.commands.builtin.utils.output import (format_optional_records,
format_records)
from mirage.commands.builtin.utils.stream import _resolve_source
from mirage.commands.builtin.utils.wrap import (call_read_bytes, call_readdir,
call_stat)
from mirage.commands.errors import UsageError
from mirage.commands.spec import SPECS
from mirage.commands.spec.types import FlagView
from mirage.io.stream import exit_on_empty, quiet_match
from mirage.io.types import ByteSource, IOResult
from mirage.types import FileStat, FileType, PathSpec
from mirage.utils.key_prefix import mount_prefix_of
from mirage.utils.path import rebase_raw
@dataclass(frozen=True, slots=True)
class GrepFlags:
"""Parsed grep flags (TS FlagSet parity); the complete set grep honors."""
ignore_case: bool
invert: bool
line_numbers: bool
count_only: bool
files_only: bool
whole_word: bool
fixed_string: bool
only_matching: bool
quiet: bool
recursive: bool
with_filename: bool
no_filename: bool
max_count: int | None
after_context: int
before_context: int
def parse_flags(fl: FlagView, never_match: bool) -> GrepFlags:
"""Convert the raw flag bag into GrepFlags, the only string-keyed reads.
Args:
fl (FlagView): spec-validated view over the raw flag kwargs.
never_match (bool): zero-pattern sentinel from resolve_pattern; it is
a regex, so it suppresses -F.
"""
a_ctx = fl.int("A")
b_ctx = fl.int("B")
c_ctx = fl.int("C")
return GrepFlags(
ignore_case=fl.bool("i"),
invert=fl.bool("v"),
line_numbers=fl.bool("n"),
count_only=fl.bool("c"),
files_only=fl.bool("args_l"),
whole_word=fl.bool("w"),
fixed_string=fl.bool("F") and not never_match,
only_matching=fl.bool("o"),
quiet=fl.bool("q"),
recursive=fl.bool("r") or fl.bool("R"),
with_filename=fl.bool("H"),
no_filename=fl.bool("h"),
max_count=fl.int("m"),
after_context=a_ctx if a_ctx is not None else (c_ctx or 0),
before_context=b_ctx if b_ctx is not None else (c_ctx or 0),
)
async def grep(
paths: list[PathSpec],
texts: Sequence[str] = (),
flags: Mapping[str, object] | None = None,
*,
readdir: Callable[..., Awaitable[list[str]]],
stat: Callable[[PathSpec], Awaitable[FileStat]],
read_bytes: Callable[..., Awaitable[bytes]],
read_stream: Callable[..., AsyncIterator[bytes]] | None,
accessor: Accessor | None = None,
stdin: AsyncIterator[bytes] | bytes | None = None,
index: IndexCacheStore | None = None,
) -> tuple[ByteSource | None, IOResult]:
"""Run grep-style fallback search over backend paths or stdin.
Interprets the raw flag kwargs itself (TS grepGeneric parity), so
backend wrappers only wire paths, texts, flags, and backend I/O.
Args:
paths (list[PathSpec]): Backend paths to search. Empty paths consume
stdin.
texts (Sequence[str]): positional TEXT operands (the pattern unless
-e/-f supplied it).
flags (Mapping[str, object] | None): raw flag kwargs from the
dispatcher (e, f, i, v, n, c, args_l, w, F, o, q, r, R, m,
A, B, C).
readdir (Callable[..., Awaitable[list[str]]]): Directory reader.
stat (Callable[[PathSpec], Awaitable[FileStat]]): Backend stat reader.
read_bytes (Callable[..., Awaitable[bytes]]): Whole-file reader.
read_stream (Callable[..., AsyncIterator[bytes]] | None): Optional
stream reader.
accessor (Accessor | None): Backend accessor passed through wrapper
helpers.
stdin (AsyncIterator[bytes] | bytes | None): Input used when paths is
empty.
index (IndexCacheStore | None): Optional cache index for wrapped
backend calls.
Returns:
tuple[ByteSource | None, IOResult]: Output stream and exit metadata.
"""
read_bytes = cache_aware_read_bytes(read_bytes)
if read_stream is not None:
read_stream = cache_aware_read_stream(read_stream)
fl = FlagView(flags, spec=SPECS["grep"])
pattern, never_match = await resolve_pattern(
texts, fl, read_bytes, accessor, index,
"grep: usage: grep [flags] pattern [path]")
f = parse_flags(fl, never_match)
if paths:
mount_prefix = mount_prefix_of(paths[0].virtual,
paths[0].resource_path)
rd = partial(call_readdir,
readdir,
accessor,
index=index,
prefix=mount_prefix)
st = partial(call_stat,
stat,
accessor,
index=index,
prefix=mount_prefix)
rb = partial(call_read_bytes,
read_bytes,
accessor,
index=index,
prefix=mount_prefix)
if f.files_only:
warnings: list[str] = []
results: list[str] = []
for p in paths:
hits = await grep_files_only(
rd,
st,
rb,
p.virtual,
pattern,
recursive=f.recursive,
ignore_case=f.ignore_case,
invert=f.invert,
line_numbers=f.line_numbers,
count_only=f.count_only,
fixed_string=f.fixed_string,
only_matching=f.only_matching,
max_count=f.max_count,
whole_word=f.whole_word,
warnings=warnings,
read_stream_fn=None,
)
results.extend(rebase_raw(hits, p.virtual, p.raw_path))
stderr = format_optional_records(warnings)
if not results:
return b"", IOResult(exit_code=1, stderr=stderr)
return format_records(results), IOResult(stderr=stderr)
if f.recursive:
pat = compile_pattern(pattern, f.ignore_case, f.fixed_string,
f.whole_word)
# OPTIMIZATION (see #207): this buffers every match into
# all_results and returns it materialized, so
# `grep -r PATTERN dir | head -n 3`
# still scans the whole tree before head sees a line. For plain
# line output (not -c/-l, which must aggregate) this could instead
# yield prefixed matches lazily per file as an async generator
# wrapped in exit_on_empty, letting an early-exiting consumer
# (head, grep -m, grep -q) abort the walk after enough matches.
all_results: list[str] = []
warnings = []
for p in paths:
s = await st(p.virtual)
if s.type == FileType.DIRECTORY:
res = await grep_recursive(
rd,
st,
rb,
p.virtual,
pat,
invert=f.invert,
line_numbers=f.line_numbers,
count_only=f.count_only,
files_only=False,
only_matching=f.only_matching,
max_count=f.max_count,
warnings=warnings,
read_stream_fn=None,
)
all_results.extend(rebase_raw(res, p.virtual, p.raw_path))
else:
data = split_lines(
(await rb(p.virtual)).decode(errors="replace"))
hits = grep_lines(p.raw_path, data, pat, f.invert,
f.line_numbers, f.count_only,
f.files_only, f.only_matching,
f.max_count)
label = "" if f.no_filename else f"{p.raw_path}:"
if f.count_only and hits:
all_results.append(f"{label}{hits[0]}")
else:
all_results.extend(f"{label}{rl}" for rl in hits)
stderr = format_optional_records(warnings)
if not all_results:
return b"", IOResult(exit_code=1, stderr=stderr)
if f.count_only and not count_records_have_matches(all_results):
return format_records(all_results), IOResult(exit_code=1,
stderr=stderr)
return format_records(all_results), IOResult(stderr=stderr)
pat = compile_pattern(pattern, f.ignore_case, f.fixed_string,
f.whole_word)
if len(paths) > 1:
all_results = []
multi_warnings: list[str] = []
for p in paths:
try:
s = await st(p.virtual)
except FileNotFoundError:
multi_warnings.append(
f"grep: {p.raw_path}: No such file or directory")
continue
if s.type == FileType.DIRECTORY:
multi_warnings.append(
f"grep: {p.raw_path}: Is a directory")
continue
data = split_lines((await
rb(p.virtual)).decode(errors="replace"))
hits = grep_lines(p.raw_path, data, pat, f.invert,
f.line_numbers, f.count_only, f.files_only,
f.only_matching, f.max_count)
label = "" if f.no_filename else f"{p.raw_path}:"
if f.count_only:
if hits:
all_results.append(f"{label}{hits[0]}")
elif f.files_only:
all_results.extend(hits)
else:
all_results.extend(f"{label}{r}" for r in hits)
stderr = format_optional_records(multi_warnings)
if not all_results:
return b"", IOResult(exit_code=1, stderr=stderr)
if f.count_only and not count_records_have_matches(all_results):
return format_records(all_results), IOResult(exit_code=1,
stderr=stderr)
return format_records(all_results), IOResult(stderr=stderr)
first_stat = await st(paths[0].virtual)
if first_stat.type == FileType.DIRECTORY:
stderr = f"grep: {paths[0].raw_path}: Is a directory\n".encode()
return b"", IOResult(exit_code=1, stderr=stderr)
if read_stream is not None:
source: AsyncIterator[bytes] = read_stream(accessor, paths[0])
else:
data = await rb(paths[0].virtual)
source = _wrap_bytes(data)
stream = grep_stream(
source,
pat,
invert=f.invert,
line_numbers=f.line_numbers,
only_matching=f.only_matching,
max_count=f.max_count,
count_only=f.count_only,
after_context=f.after_context,
before_context=f.before_context,
)
if f.quiet:
io = IOResult(exit_code=1)
return quiet_match(stream, io), io
io = IOResult()
out = (count_exit_stream(stream, io)
if f.count_only else exit_on_empty(stream, io))
if f.with_filename and not (f.after_context or f.before_context):
# GNU labels context lines with `-` instead of `:`, which the
# uniform prefix cannot reproduce, so -H skips context output.
out = prefix_lines(out, f"{paths[0].raw_path}:")
return out, io
source = _resolve_source(stdin,
"grep: usage: grep [flags] pattern [path]",
error_cls=UsageError)
pat = compile_pattern(pattern, f.ignore_case, f.fixed_string, f.whole_word)
stream = grep_stream(
source,
pat,
invert=f.invert,
line_numbers=f.line_numbers,
only_matching=f.only_matching,
max_count=f.max_count,
count_only=f.count_only,
after_context=f.after_context,
before_context=f.before_context,
)
if f.quiet:
io = IOResult(exit_code=1)
return quiet_match(stream, io), io
io = IOResult()
if f.count_only:
return count_exit_stream(stream, io), io
return exit_on_empty(stream, io), io
async def _wrap_bytes(data: bytes) -> AsyncIterator[bytes]:
yield data
__all__ = ["grep"]