rusty-pdfgrep 0.2.0

Grep through PDF files — a Rust port of Hans-Peter Deifel's `pdfgrep(1)` with lopdf-backed text extraction, regex + fancy-regex pluggable engines, --password retry for encrypted PDFs, GNU-grep-compatible color output, recursive walking with fnmatch include/exclude, and a typed library API.
Documentation
# rusty-pdfgrep

Grep through PDF text content. Rust port of [Hans-Peter Deifel's `pdfgrep(1)`](https://pdfgrep.org) 2.2.0.

[![crates.io](https://img.shields.io/crates/v/rusty-pdfgrep.svg)](https://crates.io/crates/rusty-pdfgrep)
[![docs.rs](https://docs.rs/rusty-pdfgrep/badge.svg)](https://docs.rs/rusty-pdfgrep)
[![CI](https://github.com/jsh562/rusty-pdfgrep/actions/workflows/ci.yml/badge.svg)](https://github.com/jsh562/rusty-pdfgrep/actions/workflows/ci.yml)
[![MSRV](https://img.shields.io/badge/MSRV-1.85-blue.svg)](#msrv)
[![license: MIT OR Apache-2.0](https://img.shields.io/crates/l/rusty-pdfgrep.svg)](#license)

`lopdf`-backed text extraction, pluggable regex engines (`regex` default, `fancy-regex` for `-P`), `--password` retry for encrypted PDFs, GNU-grep-compatible color output, & a typed library API. No C toolchain required: PCRE features come from pure-Rust `fancy-regex`. Default mode adds `--help`, `--version`, & a `completions` subcommand; Strict mode reverts to byte-equal pdfgrep 2.2.0 stderr.

Part of the [Rusty portfolio](https://jsh562.github.io/rusty-portfolio).

## Install

```sh
cargo install rusty-pdfgrep
# or, with prebuilt binaries:
cargo binstall rusty-pdfgrep
# or, download directly from GitHub Releases:
# https://github.com/jsh562/rusty-pdfgrep/releases
```

## Usage

```sh
# Search a single PDF for a phrase
rusty-pdfgrep "experimental results" report.pdf

# Show which page each match lives on
rusty-pdfgrep -n "force majeure" contract.pdf

# Recursive search across a directory tree, case-insensitive
rusty-pdfgrep -r -i "compliance" ./contracts/

# Try multiple passwords against an encrypted PDF
rusty-pdfgrep --password "pwd1" --password "pwd2" "term" enc.pdf

# Fixed-string search (special regex chars are literal)
rusty-pdfgrep -F "$1.50" prices.pdf

# PCRE features (lookahead, backref) via the fancy-regex engine
rusty-pdfgrep -P "foo(?=bar)" file.pdf

# Count matches per file (no per-line output)
rusty-pdfgrep -c "TODO" *.pdf

# List only filenames containing matches (good for dialog --gauge style scripts)
rusty-pdfgrep -r -l "secret" ./reports/

# Strict pdfgrep-compat mode (drop-in pdfgrep 2.2.0 replacement)
rusty-pdfgrep --strict "term" file.pdf
RUSTY_PDFGREP_STRICT=1 rusty-pdfgrep "term" file.pdf
pdfgrep "term" file.pdf                    # via pdfgrep-alias argv[0] symlink

# Shell completions
rusty-pdfgrep completions bash              # > ~/.bash_completion.d/rusty-pdfgrep
rusty-pdfgrep completions zsh               # > ~/.zfunc/_rusty-pdfgrep
rusty-pdfgrep completions fish              # > ~/.config/fish/completions/rusty-pdfgrep.fish
rusty-pdfgrep completions powershell
```

## Library API

The library exposes the `PdfGrep` / `PdfGrepBuilder` / `Match` / `PageIterator` / `PdfGrepError` types without any CLI deps. Use it when you want pdfgrep semantics inside another Rust tool.

```rust,no_run
use rusty_pdfgrep::PdfGrepBuilder;
use std::path::Path;

let pdfgrep = PdfGrepBuilder::new()
    .pattern("force majeure")
    .case_insensitive(true)
    .build()
    .unwrap();

for result in pdfgrep.search_file(Path::new("contract.pdf")) {
    let m = result.unwrap();
    println!("{}:{}: {}", m.path.display(), m.page, m.text);
}
```

For library-only consumers without CLI deps see the [Cargo Features](#cargo-features) section.

## Cargo Features

`default` enables `full`, which (for this tightly-coupled-capability port) resolves to the `cli` umbrella. `pdfgrep-classic` reproduces v0.1.x bare-port behavior matching upstream pdfgrep 2.2.0 1:1. To strip the CLI surface use `default-features = false` or `--no-default-features` & add the features you want.

rusty-pdfgrep is a **tightly-coupled-capability port**: its one documented job is "grep through PDF text content". The CLI sub-capabilities (recursive walking, GREP_COLORS color output, encrypted-PDF retry, `--include` / `--exclude` glob filters, Strict-mode dispatcher, completions subcommand) are tightly coupled inside the `cli` umbrella. No optional feature leaves are carved beyond the required umbrellas; see [`docs/feature-layout.md`](docs/feature-layout.md) for why.

### Feature matrix

| Feature | Description | Umbrella(s) |
|---|---|---|
| `cli` | All CLI-only dependencies (`clap`, `clap_complete`, `anyhow`, `termcolor`, `anstyle`, `walkdir`, `globset`) and the binary entry point, recursive walker, color writer, include/exclude glob filters, encrypted-PDF retry CLI dispatcher, Strict-mode argv pre-scanner, and completions subcommand. Library consumers strip via `default-features = false`. | `full`, `pdfgrep-classic`, `pdfgrep-minimal` |

### Preset bundles

| Bundle | Composition | Use case |
|---|---|---|
| `pdfgrep-classic` | `cli` | Drop-in upstream pdfgrep 2.2.0 replacement. Strict mode is invoked via `--strict`, `RUSTY_PDFGREP_STRICT`, or `pdfgrep`/`pdfgrep-alias` argv[0] auto-detect. |
| `pdfgrep-minimal` | `cli` | Explicit minimal-CLI alias for users who prefer the `<port>-minimal` naming convention seen across other portfolio ports. Identical composition to `pdfgrep-classic`. |

### Keep-list workaround (Cargo features are union-only)

Cargo features cannot subtract from `default`. To get "everything except a specific feature," disable defaults & enumerate the features you want:

```sh
cargo install rusty-pdfgrep --no-default-features --features "cli"
# → bare CLI. Equivalent to pdfgrep-classic / pdfgrep-minimal.
```

For the common cases the named [preset bundles](#preset-bundles) are usually sufficient.

### Library-only consumers

```toml
[dependencies]
rusty-pdfgrep = { version = "0.2", default-features = false }
```

This strips `clap`, `clap_complete`, `anyhow`, `termcolor`, `anstyle`, `walkdir`, & `globset`. The resulting build pulls only `thiserror` (required by the always-on `PdfGrepError` enum) plus the foundational PDF + regex stack (`lopdf` + `regex` + `fancy-regex`). The CI `test-no-default` job runs `cargo tree --no-default-features` on every PR & fails the build if any CLI-only dep leaks back in.

### Convention authority

This layout follows the portfolio-wide Cargo Features Convention. The "why" lives in [ADR-0006](https://github.com/jsh562/rustylib/blob/main/specs/adrs/0006-cargo-features-convention-for-portfolio-ports.md); the "what" lives in [`project-instructions.md` §Cargo Feature Surface](https://github.com/jsh562/rustylib/blob/main/project-instructions.md). Every Rusty port from v0.2 onward exposes the same umbrella set (`default` / `full` / `cli` / `<port>-classic`), per-port leaves named in kebab-case, & 2 to 4 preset bundles.

## Compatibility

`rusty-pdfgrep` has two modes:

- **Default mode.** clap-styled flag parser. Conflicting flag pairs MUST be rejected at parse time. `--help`, `--version`, & the `completions` subcommand are all available.
- **Strict mode** (activated by `--strict`, `RUSTY_PDFGREP_STRICT=1`, or invoking the binary as `pdfgrep`/`pdfgrep-alias`). Byte-equal stderr against upstream v2.2.0 for documented diagnostics. Last-wins flag resolution. `--help`, `--version`, & `completions` MUST be rejected.

### `-P` / `--perl-regexp` engine

The `-P` engine is `fancy-regex` instead of upstream's libpcre2. Pure-Rust, no C toolchain. Edge-case PCRE features (recursive patterns, callouts, conditional patterns) diverge from upstream pdfgrep & MUST be tested against your specific patterns before relying on byte-equal results.

### BREAKING-CHANGE vs upstream

stdin is buffered into memory with a configurable cap (default 512 MiB). Upstream pdfgrep buffers unbounded; rusty-pdfgrep refuses to OOM on huge inputs.

See [`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md) for the full per-flag matrix.

## What's not shipped

- **`-w` / `--word-regexp`.** Not in upstream pdfgrep; omitted.
- **`--password-list FILE`.** Upstream uses repeated `--password` flags; rusty-pdfgrep matches that surface.
- **`-A` / `-B` / `-C` page-context.** Deferred.
- **`--cache`.** Page-extraction cache; deferred.
- **`--unac`.** Unicode accent normalization; deferred.
- **`-R` symlink-follow.** Recursive walk uses `walkdir`'s default (no follow). Deferred.
- **`pdfium-render` backend.** `lopdf` covers the 99% case without a C toolchain dependency.
- **Unbounded stdin buffering.** Capped at 512 MiB by default; configurable via the library builder.

## MSRV

Rust **1.85** (edition 2024). Re-verified against the portfolio's stable-minus-two policy at each release.

## License

Dual-licensed under [MIT](LICENSE) or [Apache-2.0](LICENSE-APACHE) at your option.