CLI tools

ripgrep and fd: search that skips what you would have skipped

September 02, 2026

The win for today: search a real Go repo with rg and fd instead of grep -r and find, and explain — without looking it up — why a search that should have matched came back empty. About twenty minutes.

Key idea —

rg and fd are fast mostly because they search less. Both walk the directory tree in parallel and skip whatever .gitignore excludes, plus hidden files. You would have skipped those anyway. The catch: sometimes the thing you're looking for is in the pile they skipped.

1. Install (2 min)

macOS
brew install ripgrep fd
Linux
sudo apt install ripgrep fd-find. Debian already has a package calledfd, so the binary is fdfind. Fix it once: ln -s $(which fdfind) ~/.local/bin/fd.

Both are single static binaries. Sources: ripgrep README, fd README.

2. ripgrep: grep with the defaults you'd have set (6 min)

cd into any Go repo you have locally — Ink is a good one — and type:

rg ListenAndServe

Compare with what you'd have typed before: grep -rn ListenAndServe .. rg is recursive by default, searches the current directory by default, prints line numbers, colours the match, and groups results by file. That's the whole pitch for daily use.

Now the part that matters. From the official guide, rg skips four things unless told otherwise:

  1. Anything matched by .gitignore (and .ignore, .rgignore).
  2. Hidden files and directories (anything starting with .).
  3. Binary files — anything containing a NUL byte.
  4. Symbolic links.

Each has an off switch, and one flag turns them off in stages: -u stops honouring ignore files, -uu also searches hidden files, -uuu also searches binaries. So the rule for the rest of your career:

Key idea —

When rg says there are no matches and you don't believe it, run it again with -uu. If the match appears, it was in an ignored or hidden path — usually vendor/, node_modules/, .git/ or a build directory.

The flags worth knowing today, all from the guide:

FlagMeaning
-t go / -T goOnly, or never, files of a type. rg --type-list shows the types.
-g '*.md' / -g '!vendor/**'Include or exclude by glob. Same syntax as .gitignore. Quote it.
-FTreat the pattern as a literal string. Reach for it the moment the pattern has a dot or bracket.
-wWord boundaries, so id stops matching uuid.
-lFilenames only.
-C 3Three lines of context either side.
--filesList the files rg would search, without searching.
-r 'x'Show what replacing the match with x would look like.

That last one comes with a sentence to memorise, straight from the guide: "ripgrep will never modify your files." -r only changes the output. There is no in-place flag.

Drill — Every file in the repo that contains func main, filenames only.

Drill — All calls to a method literally called .Get( — the dot and bracket are part of the search.

Drill — Search for TODO in Go files only, skipping test files, with two lines of context either side.

Drill — Something you know is in vendor/ (or node_modules/, or a dist/ folder) that a plain rg can't find. Make it find it.

3. fd: find, but the pattern is a regex on the filename (6 min)

find takes a directory and a chain of predicates. fd takes a pattern and matches it against the filename — not the full path — as a regex, with the same ignore rules as rg (fd README). So:

fd router          # any file or directory whose name contains "router"
fd '^main\.go$'    # exactly main.go
fd                 # no pattern: list everything rg would search

Two defaults that differ from find: the search is case-insensitive unless your pattern has an uppercase letter (the README calls this smart case), and hidden and gitignored entries are skipped. -H shows hidden, -I ignores the ignore files, and -HI (or -u) is "actually everything" — the fd spelling of rg -uu.

The flags to know:

FlagMeaning
-e goBy extension. Repeatable.
-t f / -t d / -t xFile, directory, executable. Also l symlink, e empty.
-pMatch the pattern against the full path instead of the filename.
-d 2Maximum depth.
-E node_modulesExclude by glob.
--changed-within 1dModified in the last day. Also --changed-before.
-x cmd {}Run cmd once per result, in parallel.
-X cmdRun cmd once, with every result as an argument.

The -x / -X distinction is the one people get wrong. fd -e go -x gofmt -l {} starts one gofmt per file; fd -e go -X gofmt -l starts one gofmt with all the files. For anything that's cheap to start, -X is faster; for anything where one bad file shouldn't stop the others, -x. The README's own safety rule applies to both: if the command deletes things, run the fd on its own first and look at the list.

Drill — Count the Go files in the repo two ways — once with fd, once with rg — and check the numbers agree.

Drill — Every file you've touched in the last 24 hours, excluding anything under .git.

Drill — Run gofmt -l across every Go file with a single gofmt process.

Drill — List the dotfiles and dot-directories at the top level of your home directory — nothing deeper.

4. Where the old tools still win (1 min)

Two honest limits. First, rg and fd won't be on the box you SSH into at 2am; keep grep -rnI --exclude-dir={.git,vendor,node_modules} and find . -type f -name '*.log' -mtime +30 in your head as the floor. Second, find is still better for predicate-heavy or destructive jobs — -newer against a reference file, -size, -perm, -delete — because it is explicit about every step.

5. Check yourself

Options are deliberately the same length. Answer from memory.

Q1. You search a Go repo with rg and get nothing, but the string is in vendor/, which .gitignore lists. Shortest fix?

Q2. What does rg -r 'bar' foo do to the files on disk?

Q3. fd router — what is the pattern matched against, by default?

Q4. fd -e md -X wc -l — how many wc processes start?

The primary source for this lesson is the ripgrep user guide. Read the "Automatic filtering" and "Manual filtering" sections — ten minutes, and you'll never be surprised by an empty result again. The fd README is the whole fd manual and reads in five.

Compressed version for later: rg and fd cheat sheet.