Mastering Large Codebases with ripgrep, fd, and jq

Mastering Large Codebases with ripgrep, fd, and jq

A practical terminal workflow for exploring unfamiliar repositories—fast text search, file discovery, and JSON filtering without leaving the shell.

Large codebases punish slow exploration. GUI search tools lag on million-line monorepos; memorizing every directory is unrealistic. Three command-line utilities—ripgrep (rg), fd, and jq—form a fast, composable workflow for finding text, locating files, and extracting structure from JSON.

Together they replace much of what developers do with nested find, grep, and ad hoc scripting—while respecting .gitignore by default and staying friendly to scripts and CI.

This guide explains each tool's role, how they chain together, and patterns for navigating unfamiliar repositories without claiming you need to learn every flag on day one.

Why these three tools

| Tool | Job | Replaces |
|------|-----|----------|
| ripgrep (rg) | Search file contents by regex | grep -R, IDE search |
| fd | Find files and directories by name | find, slow tree walks |
| jq | Query and transform JSON | Python one-liners, manual parsing |

All three are single binaries, cross-platform, and designed for scripting. They share sensible defaults: ignore hidden clutter, respect gitignore, parallelize work.

Install via your package manager (brew install ripgrep fd jq on macOS, analogous packages on Linux). Pin versions in devcontainer or CI images for reproducibility.

ripgrep: search contents fast

rg PATTERN searches recursively from the current directory, skipping ignored paths.

First moves in a new repo

```bash

What is the entry point?

rg "main\\(|bootstrap|createApp" -g '*.{ts,js,go,rs}'

Where is configuration loaded?

rg "process\\.env|config\\.|settings" -g '!node_modules'

Find TODO markers left by the team

rg "TODO|FIXME|HACK" --heading
```

Flags worth learning

  • -g GLOB: Include or exclude paths (-g '*.go', -g '!test/**')
  • --type: Built-in file type filters (--type ts, rg --type-add 'config:*.config.js')
  • -l: List files with matches only
  • -c: Count matches per file
  • -n: Line numbers (default in terminal output)
  • -C 3: Context lines around matches
  • -i: Case insensitive
  • --json: Machine-readable output for tooling
  • -F: Fixed string (no regex) when searching literals with special characters

Literal searches and escaping

Searching for foo.bar as regex matches any character between foo and bar. Use rg -F 'foo.bar' or escape dots.

Multiline and structural search

rg is line-oriented by default. For multiline patterns, -U --multiline-dotall helps, but AST-aware tools (tree-sitter, language-specific LSP) beat regex for structural queries. Use rg to narrow files first, then refine in an editor.

Integration with editors

Most editors bind "project search" to ripgrep under the hood. Knowing CLI flags translates directly to faster editor search configuration.

fd: find files by name

fd PATTERN finds paths by substring (regex by default; -g for glob).

Common patterns

```bash

All TypeScript files under src

fd -e ts . src

Config files anywhere

fd 'config|wrangler|docker' -t f

Empty directories (cleanup audits)

fd -t d --empty

Recently modified (requires fd with --changed-within on some versions)

fd . -t f --changed-within 2d
```

Flags worth learning

  • -t f / -t d: Files or directories only
  • -e ext: Extension filter (can repeat)
  • -H: Include hidden files
  • -I: Do not respect gitignore (use sparingly)
  • -x: Execute command per result (fd -e md -x wc -l)

fd is not a full replacement for find -mtime or complex boolean logic, but covers most "where is this file?" tasks faster.

jq: slice JSON pipelines

API responses, package-lock.json, Terraform plans, and CI artifacts are JSON. jq filters and maps JSON on stdin.

Basics

```bash

Pretty-print

curl -s https://api.example.com/data | jq .

Extract a field

jq '.name' package.json

Array map

jq '.dependencies | keys[]' package.json

Select objects matching a condition

jq '.[] | select(.status == "failed")' results.json
```

Combining with ripgrep

Search logs saved as JSON lines:

``bash
rg '"level":"error"' app.log | jq '{time: .timestamp, msg: .message}'
``

Or find JSON files then query:

``bash
fd -e json . config | xargs -I{} sh -c 'echo "==> {}"; jq ".version" {}'
``

Practical jq patterns for codebases

Inspect workspace package list (monorepo):

``bash
jq -r '.workspaces.packages[]?' package.json 2>/dev/null
``

List npm scripts:

``bash
jq -r '.scripts | to_entries[] | "\(.key): \(.value)"' package.json
``

Filter large lockfile for one package:

``bash
jq '.. | objects | select(.name? == "lodash")' package-lock.json
``

Use -r for raw string output without quotes. Use --slurp or -s when input is JSON lines.

Chaining the workflow

A typical exploration session on an unfamiliar repository:

1. Orient by manifest files

``bash
fd '(README|CONTRIBUTING|package\\.json|Cargo\\.toml|go\\.mod)' -t f
``

Read README and manifest for build commands and layout.

2. Locate domain concepts

``bash
rg -n "UserService|authenticate|/api/v1" --heading
``

Note directories that repeat in results—that is often the module boundary.

3. Narrow file set

``bash
fd -e ts . src/services/auth
rg "class.*Service" src/services/auth
``

4. Parse machine output

``bash
rg --json "deprecated" | jq -r '.data.path.text + ":" + (.data.line_number|tostring)'
``

Produces path:line for quick jumping (some tools consume this format).

5. Document findings

Save commands in a scratch note or exploration script—future you (or teammates) reuse the same paths.

Performance habits

  • Scope directories: Search src/ not the whole disk
  • Use -g exclusions: -g '!{dist,build,coverage}'
  • Prefer rg -l then read: List files first when matches are dense
  • Avoid piping huge binary: rg skips binaries by default; do not override without reason
  • xargs parallelism: fd ... -x rg -H 'pattern' {} for per-file search when needed

On very large trees, ensure you are not searching node_modules or vendor dirs—defaults usually exclude via gitignore; verify with rg --debug if results look slow.

Git-aware exploration

Both rg and fd respect .gitignore. For untracked generated files you need to search, pass -u / --unrestricted (rg) or -I (fd)—then narrow with globs to avoid noise.

Search only tracked files in a pinch:

``bash
git ls-files '*.ts' | xargs rg 'pattern'
``

Useful when local build artifacts are not gitignored correctly.

When to reach for something else

| Need | Better tool |
|------|-------------|
| Symbol definition across languages | LSP, ctags, scip |
| Call graph, refactor | IDE, specialized static analysis |
| Semantic code search | Code indexing services, ast-grep |
| Binary or PDF content | Not rg/fd/jq |
| Interactive fuzzy pick | fzf piped with fd or rg -l |

Add fzf (fd . | fzf) for interactive selection; it complements but does not replace scripted workflows.

Safety and scripting

  • Quote patterns in shell scripts; prefer arrays over string concatenation
  • fd -x and xargs can run many commands—test with echo first
  • jq programs can error on null—use ? optional operator: .foo?.bar
  • Do not pipe untrusted JSON into jq with --rawfile execution tricks; treat data as data

CI and automation

The same commands work in CI for guardrails:

```bash

Fail if forbidden import appears

rg -q "from 'lodash/fp'" src && exit 1 || true

Ensure all packages have license field

fd package.json | while read -r f; do jq -e '.license' "$f" >/dev/null || echo "missing license: $f"; done
```

Keep checks fast: narrow globs, cache dependencies, run only on changed paths when possible.

Worked example: tracing an HTTP handler

Suppose you need to find where POST /api/orders is handled in an unknown service-oriented repo.

  1. Search route registration:

``bash
rg -n "(/api/orders|orders.*post|router\\.(post|route))" -i -g '!node_modules'
``

  1. List likely handler files:

``bash
fd -e 'ts|go|py' handler
fd -e 'ts|go|py' orders
``

  1. Follow imports from a hit file (example TypeScript):

``bash
rg -n "from ['\\\"].*orders" src/
``

  1. If the service returns JSON error shapes, inspect OpenAPI or fixture files:

``bash
fd openapi -e yaml -e json
jq '.paths["/api/orders"].post' openapi.json
``

  1. Check tests for expected behavior (often faster than reading implementation):

``bash
rg -n "POST.orders|/api/orders" -g 'test' -g 'spec*'
``

This sequence typically locates the handler, its validation layer, and integration tests in minutes without opening every directory tree manually.

Aliases and shell ergonomics

Short aliases reduce friction enough that you actually use the tools:

``bash
alias rg='rg --smart-case'
alias rgf='rg --files-with-matches'
``

Combine fd with fzf for interactive file open:

``bash
vim "$(fd -e ts | fzf)"
``

Store project-specific search snippets in scripts/explore.sh or documented Makefile targets so onboarding engineers inherit the same commands.

Limitations

Regex search does not understand syntax. Renamed symbols break text search—supplement with language-aware tools for refactors.

jq loads entire JSON into memory for standard filters. Multi-gigabyte JSON needs streaming tools (jq --stream) or different formats.

Cross-platform shell differences (macOS BSD vs GNU) affect xargs and sed—prefer tool-native flags over fragile pipes.

Mastering these three tools is not about memorizing every flag—it is about a default loop: fd to locate, rg to search, jq to structure. Repeat until the codebase stops feeling opaque.

Summary

ripgrep finds text in code at speed. fd finds files by name without verbose find syntax. jq extracts fields from JSON outputs and config. Chained together, they form a practical workflow for large codebases: orient from manifests, search concepts, narrow paths, parse structured output. Add an IDE and language server for semantics—but keep this trio in muscle memory for every repo you do not yet know.