targrep 0.7.2

Search inside compressed log archives (.tgz, .tar.zst, .tar.xz) with ripgrep's flags and per-member line numbers
Documentation
# targrep

`targrep` — run as `trg` — searches inside compressed log archives (`.tgz`,
`.tar.zst` and `.tar.xz`) with ripgrep's flags, per-member line numbers, and without
trashing the page cache of the server you are searching on.

```console
$ trg -i 'timeout' /var/log/archive/
/var/log/archive/2026-08-24.tgz:logs/vhost3.error.log:1182:[...] upstream timeout
/var/log/archive/2026-08-24.tgz:logs/vhost7.access.log.1.gz:40211:[...] 504 timeout
```

## Why

`zgrep -a` and `rg -za` both treat an archive as one long stream of text, so a
match reports a line number into the whole tarball, not into the log file it
came from. Both also pipe through the `gzip` binary, which inflates several
times slower than zlib, and both leave every archive they read resident in the
page cache — memory the production workload on that box needed.

`trg` reads the tar structure, so it reports `archive:member:line` with line
numbers counted per member and binary detection per member. It inflates
in-process with zlib-ng and drops archive pages from the cache as it goes.

Measured on 610 MB compressed / 11.5 GB raw of real Apache logs (Linux
aarch64):

| | wall | CPU | page cache left |
|---|---:|---:|---:|
| `zgrep -a` | 33.96 s | 31.84 s | 639 MB |
| `trg -j1` | 3.95 s | 3.81 s | 0 MB |
| `trg --turbo` | 0.84 s | — | 0 MB |

`--turbo` still uses less total CPU than `rg -za` on 12 cores.

On x86-64 (a 4-core AMD EPYC GitHub runner, 1.16 GB compressed of synthetic
logs built to match the real ones' compression, via
`.github/workflows/bench.yml`):

| | wall | CPU | page cache left |
|---|---:|---:|---:|
| `zgrep -a` | 52.74 s | 56.78 s | 1162 MB |
| `rg -za` (all cores) | 14.55 s | 57.66 s | 1162 MB |
| `trg -j1` | 6.08 s | 6.07 s | 0 MB |
| `trg --turbo` | 2.76 s | 7.84 s | 0 MB |

9.4× less CPU than `zgrep`. Ratios move with the CPU — the same corpus gives
6.1× on a GitHub arm64 runner (Neoverse-N2) — so treat each table as a
measurement of that machine.

## Features

- `archive:member/path:line:` output, line numbers correct per member.
- Compression detected from content, not the filename: gzip, zstd and xz,
  each unwrapped one level, so a logrotate'd `access.log.1.gz`, `.zst` or `.xz`
  *inside* an archive is searched as text.
- ripgrep's flag names wherever the job is the same (`-i`, `-w`, `-F`, `-v`,
  `-C`, `-o`, `-c`, `-l`, `-q`, `--json`, …). `-g` filters *members* inside
  archives, `--archive-glob` filters the archives a directory walk finds, and
  both take ripgrep's `!` exclusions (`-g '!*.error.log'`).
- Polite by default: `nice 10`, idle I/O priority, one archive at a time, and
  no page-cache footprint. `--turbo` lifts all of that when you need the answer
  now.
- An exit code you can trust (below).
- Safe to read on a terminal: log lines and member names are written by
  whoever sent the request, so on a tty their control bytes are shown as text
  (`\x1b[2J`) instead of being acted on — no retitled windows, cleared
  screens, clipboard writes or hidden lines. trg's own colours still work.
  Piped output stays byte-exact; `--escape always|never` overrides.

## Install

With Homebrew (builds from source):

```sh
brew install codedeviate/cli/targrep
```

From [crates.io](https://crates.io/crates/targrep) — the crate is `targrep`,
the binary it installs is `trg`:

```sh
cargo install targrep
```

Either way this needs Rust 1.88 or newer and, for the default zlib-ng backend,
`cmake` and a C compiler. From a checkout:

```sh
cargo install --path .
```

Without cmake, build the pure-Rust backend instead — slower, but still far
cheaper than the `gzip` binary:

```sh
cargo install --path . --no-default-features
```

### A Linux binary to copy onto a server

Every [GitHub release](https://github.com/codedeviate/trg/releases) has
prebuilt binaries for x86_64 and aarch64, each with a `.sha256`:

```sh
curl -LO https://github.com/codedeviate/trg/releases/download/v0.5.0/trg-0.5.0-x86_64-linux.tar.gz
tar -xzf trg-0.5.0-x86_64-linux.tar.gz   # trg-0.5.0-x86_64-linux/trg
```

To build one yourself, from macOS or any other host, with `cargo-zigbuild`,
`zig` and `cmake` installed:

```sh
scripts/build-linux.sh            # dist/trg-<version>-x86_64-linux
scripts/build-linux.sh aarch64    # dist/trg-<version>-aarch64-linux
```

The binary depends on nothing but glibc 2.17 or newer, so it runs on any
current Debian, Ubuntu or RHEL. Copy it over with `scp` and put it on the
`PATH` as `trg`.

## Usage

```sh
trg [OPTIONS] PATTERN PATH...
trg [OPTIONS] -e PATTERN... PATH...
trg [OPTIONS] -T PATH...            # list members, no search
```

At least one `PATH` is required: `trg` never searches the current directory on
its own.

```sh
trg 'POST /checkout' /var/log/archive/*.tgz
trg -g '*.error.log' -C 2 'segfault' /var/log/archive/
trg --archive-glob '2026-08-*' ' 500 ' /var/log/archive/   # August only, any depth
trg -T 2026-08-24.tar.zst
trg --turbo -c ' 500 ' /var/log/archive/
```

Run `trg --help` for every option, including `-j`, `--nice`,
`--no-drop-cache` and `--load-limit`.

### Exit codes

| code | meaning |
|---|---|
| `0` | matches found, everything was read |
| `1` | no matches, everything was read |
| `2` | something went unread — an error, a truncated archive, no `PATH`, or an `--archive-glob` that matched nothing |

A `1` is a claim that the logs are clean, so `trg` only gives one when it read
everything. Never trust a `1` you did not get.

## Development

```sh
cargo test
```

The page-cache residency test runs on Linux only. `bench/` holds the corpus
builder and the comparison against `zgrep` and `rg`.

- [CHANGELOG.md]CHANGELOG.md — what changed in each version
- [CONTRIBUTING.md]CONTRIBUTING.md — commit conventions and versioning
- [OUT-OF-SCOPE.md]OUT-OF-SCOPE.md — what `trg` will not do, and the wishlist

## License

MIT — see [LICENSE](LICENSE).