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
196 lines
6.5 KiB
Python
196 lines
6.5 KiB
Python
import gzip as gziplib
|
|
import re
|
|
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.commands.builtin.grep_helper import (build_pattern_str,
|
|
resolve_pattern)
|
|
from mirage.commands.builtin.utils.lines import split_lines
|
|
from mirage.commands.builtin.utils.stream import _read_stdin_async
|
|
from mirage.commands.spec import SPECS
|
|
from mirage.commands.spec.types import FlagView
|
|
from mirage.io.types import ByteSource, IOResult
|
|
from mirage.types import PathSpec
|
|
|
|
|
|
async def _read_plain(
|
|
read_bytes: Callable[..., Awaitable[bytes]],
|
|
accessor: Accessor,
|
|
path: PathSpec,
|
|
index: object = None,
|
|
) -> bytes:
|
|
return await read_bytes(accessor, path)
|
|
|
|
|
|
def _zgrep_search(
|
|
data: bytes,
|
|
pattern: str,
|
|
ignore_case: bool,
|
|
invert: bool,
|
|
count: bool,
|
|
line_numbers: bool,
|
|
filename: str | None,
|
|
only_matching: bool,
|
|
max_count: int | None,
|
|
) -> tuple[list[str], bool]:
|
|
text = data.decode(errors="replace")
|
|
lines = split_lines(text)
|
|
flags = re.IGNORECASE if ignore_case else 0
|
|
matched: list[tuple[int, str]] = []
|
|
for idx, line in enumerate(lines, 1):
|
|
if only_matching and not invert:
|
|
hits = list(re.finditer(pattern, line, flags))
|
|
if hits:
|
|
for m in hits:
|
|
matched.append((idx, m.group()))
|
|
if max_count is not None and len(matched) >= max_count:
|
|
break
|
|
elif invert:
|
|
matched.append((idx, line))
|
|
else:
|
|
hit = bool(re.search(pattern, line, flags))
|
|
if invert:
|
|
hit = not hit
|
|
if hit:
|
|
matched.append((idx, line))
|
|
if max_count is not None and len(matched) >= max_count:
|
|
break
|
|
if count:
|
|
value = str(len(matched))
|
|
if filename:
|
|
value = f"{filename}:{value}"
|
|
return [value], len(matched) > 0
|
|
result: list[str] = []
|
|
for idx, line in matched:
|
|
prefix = ""
|
|
if filename:
|
|
prefix = filename + ":"
|
|
if line_numbers:
|
|
prefix += str(idx) + ":"
|
|
result.append(prefix + line)
|
|
return result, len(matched) > 0
|
|
|
|
|
|
def _files_only_match(data: bytes, pattern: str, ignore_case: bool,
|
|
invert: bool) -> bool:
|
|
text = data.decode(errors="replace")
|
|
flags = re.IGNORECASE if ignore_case else 0
|
|
for line in split_lines(text):
|
|
hit = bool(re.search(pattern, line, flags))
|
|
if invert:
|
|
hit = not hit
|
|
if hit:
|
|
return True
|
|
return False
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ZgrepFlags:
|
|
"""Parsed zgrep flags; the complete set zgrep honors."""
|
|
ignore_case: bool
|
|
invert: bool
|
|
count: bool
|
|
files_only: bool
|
|
line_numbers: bool
|
|
fixed: bool
|
|
force_filename: bool
|
|
suppress_filename: bool
|
|
only_matching: bool
|
|
quiet: bool
|
|
whole_word: bool
|
|
max_count: int | None
|
|
|
|
|
|
def parse_flags(fl: FlagView, never_match: bool) -> ZgrepFlags:
|
|
"""Convert the raw flag bag into ZgrepFlags, 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.
|
|
"""
|
|
return ZgrepFlags(
|
|
ignore_case=fl.bool("i"),
|
|
invert=fl.bool("v"),
|
|
count=fl.bool("c"),
|
|
files_only=fl.bool("args_l"),
|
|
line_numbers=fl.bool("n"),
|
|
fixed=fl.bool("F") and not never_match,
|
|
force_filename=fl.bool("H"),
|
|
suppress_filename=fl.bool("h"),
|
|
only_matching=fl.bool("o"),
|
|
quiet=fl.bool("q"),
|
|
whole_word=fl.bool("w"),
|
|
max_count=fl.int("m"),
|
|
)
|
|
|
|
|
|
async def zgrep(
|
|
paths: list[PathSpec],
|
|
texts: Sequence[str] = (),
|
|
flags: Mapping[str, object] | None = None,
|
|
*,
|
|
read_bytes: Callable[..., Awaitable[bytes]],
|
|
accessor: Accessor | None = None,
|
|
stdin: AsyncIterator[bytes] | bytes | None = None,
|
|
index: object = None,
|
|
) -> tuple[ByteSource | None, IOResult]:
|
|
fl = FlagView(flags, spec=SPECS["zgrep"])
|
|
pattern, never_match = await resolve_pattern(
|
|
texts, fl, partial(_read_plain, read_bytes), accessor, index,
|
|
"zgrep: usage: zgrep [flags] pattern [path]")
|
|
f = parse_flags(fl, never_match)
|
|
compiled = build_pattern_str(pattern, f.fixed, f.whole_word)
|
|
multi = len(paths) > 1
|
|
show_filename = f.force_filename or (multi and not f.suppress_filename)
|
|
any_match = False
|
|
all_results: list[str] = []
|
|
|
|
if paths:
|
|
for p in paths:
|
|
raw = await read_bytes(accessor, p)
|
|
data = gziplib.decompress(raw)
|
|
fname = p.virtual if show_filename else None
|
|
if f.files_only:
|
|
if _files_only_match(data, compiled, f.ignore_case, f.invert):
|
|
all_results.append(p.virtual)
|
|
any_match = True
|
|
else:
|
|
result, had_match = _zgrep_search(data, compiled,
|
|
f.ignore_case, f.invert,
|
|
f.count, f.line_numbers,
|
|
fname, f.only_matching,
|
|
f.max_count)
|
|
if had_match:
|
|
any_match = True
|
|
all_results.extend(result)
|
|
else:
|
|
raw = await _read_stdin_async(stdin)
|
|
data = gziplib.decompress(raw) if raw else b""
|
|
if f.files_only:
|
|
if _files_only_match(data, compiled, f.ignore_case, f.invert):
|
|
all_results.append("(standard input)")
|
|
any_match = True
|
|
else:
|
|
result, had_match = _zgrep_search(data, compiled, f.ignore_case,
|
|
f.invert, f.count,
|
|
f.line_numbers, None,
|
|
f.only_matching, f.max_count)
|
|
if had_match:
|
|
any_match = True
|
|
all_results.extend(result)
|
|
|
|
if f.quiet:
|
|
return None, IOResult(exit_code=0 if any_match else 1)
|
|
exit_code = 0 if any_match else 1
|
|
if not all_results:
|
|
return None, IOResult(exit_code=exit_code)
|
|
return ("\n".join(all_results) +
|
|
"\n").encode(), IOResult(exit_code=exit_code)
|
|
|
|
|
|
__all__ = ["zgrep"]
|