# rusty-pdfgrep
Grep through PDF text content. Rust port of [Hans-Peter Deifel's `pdfgrep(1)`](https://pdfgrep.org) 2.2.0.
[](https://crates.io/crates/rusty-pdfgrep)
[](https://docs.rs/rusty-pdfgrep)
[](https://github.com/jsh562/rusty-pdfgrep/actions/workflows/ci.yml)
[](#msrv)
[](#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.