File Search Cheatsheet: Every Command I Actually Use

2026/04/04

BUILDdevexemacs🍞

I keep forgetting the exact flags. Every time I need to find something on disk, I end up googling the same three patterns. So here’s the cheatsheet I wish I had pinned to my terminal. Heavy on tables and code blocks, light on prose. Organized by what you’re trying to do, not which tool you happen to know.

Platform bias: macOS daily driver, Emacs editor, but cross-platform notes where they matter.

Quick decision tree

I want to… Use this Why
Find a file by name fd (or find) Fast, respects .gitignore
Find a file by name, fuzzy fzf Interactive, forgiving
Find a file by Spotlight metadata mdfind Instant, macOS only
Search file contents rg (ripgrep) Fastest grep replacement
Search contents, interactively fzf + rg Live preview
Search contents in Emacs consult-ripgrep / counsel-rg Never leave the editor
Find large files find + -size No modern replacement needed
Find recently modified files fd --changed-within or find -mtime Both work fine
Search compressed files zgrep Handles .gz transparently

The classics

Command What it does Platform
find . -name "*.py" Recursive file search by name All (BSD on mac, GNU on linux)
find . -iname "*.py" Case-insensitive All
find . -type f -name "*.go" Files only (no dirs) All
find . -type d -name "vendor" Directories only All
locate filename Search pre-built index (fast) Linux; macOS needs setup
mdfind -name "report" Spotlight CLI search by filename macOS only
mdfind "kMDItemFSName == '*.pdf'" Spotlight with metadata query macOS only

Modern replacements

Command What it does Notes
fd pattern Find files matching pattern Respects .gitignore, colored output, smart case
fd -e py Find by extension Cleaner than -name "*.py"
fd -H pattern Include hidden files .gitignore still respected
fd -I pattern Don’t respect .gitignore Search everything
fd -t f pattern Files only -t d for dirs, -t l for symlinks
fd -t f -e go . src/ All .go files under src/ Dot pattern means “match all”
fzf Interactive fuzzy finder Pipe anything into it
fd -t f | fzf Fuzzy-find a file Best of both worlds

Install: brew install fd fzf

mdfind (Spotlight CLI, macOS only)

Spotlight indexes constantly in the background. mdfind queries that index, which makes it nearly instant for the kinds of searches Spotlight supports.

# Search by filename
mdfind -name "config.yaml"

# Search by content (Spotlight indexed content)
mdfind "database connection"

# Restrict to a directory
mdfind -onlyin ~/projects "TODO"

# Search by file type
mdfind "kMDItemContentType == 'public.python-script'"

# Files modified today
mdfind "kMDItemFSContentChangeDate > \$time.today"

# List all metadata attributes for a file
mdls somefile.py

Gotcha: mdfind doesn’t search inside .gitignore’d dirs or anything Spotlight excludes (check System Settings > Spotlight > Privacy). It also won’t index files in directories like node_modules or .git.

# Linux: usually pre-installed, index updated by cron
locate "*.conf"

# macOS: must build the index first
sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.locate.plist
# Wait for index build, then:
locate "*.conf"

locate is fast but stale. The index updates periodically (daily on most Linux distros). If you need fresh results, use find or fd.

grep family

Command What it does Notes
grep "pattern" file Search one file Basic regex
grep -r "pattern" . Recursive search Slow on large trees
grep -rn "pattern" . With line numbers Almost always want -n
grep -ri "pattern" . Case-insensitive
grep -rl "pattern" . List matching files only No content, just paths
grep -rw "pattern" . Whole word match Won’t match “patterned”
grep -A3 -B3 "pattern" file 3 lines context -C3 for both before/after
grep -E "foo|bar" Extended regex (OR) Same as egrep
grep -P "\d{3}-\d{4}" Perl regex GNU only, not on macOS BSD grep
grep --include="*.py" -r "import" Filter by file extension Works with BSD and GNU
egrep "pattern1|pattern2" Multiple patterns Shorthand for grep -E
zgrep "pattern" file.gz Search compressed files Also: zcat, zless
zgrep -r "ERROR" /var/log/*.gz Search compressed logs Lifesaver for log analysis

Modern replacements

Command What it does Notes
rg "pattern" Recursive content search Fastest. Respects .gitignore
rg -i "pattern" Case-insensitive
rg -w "pattern" Whole word
rg -l "pattern" List files only
rg -t py "pattern" Search only Python files Built-in type filters
rg -T js "pattern" Exclude JavaScript files
rg -g "*.go" "pattern" Glob filter More flexible than -t
rg -g "!vendor" "pattern" Exclude by glob Negate with !
rg --hidden "pattern" Include hidden files
rg -c "pattern" Count matches per file
rg --json "pattern" JSON output Pipe to jq
rg -U "multi\nline" Multiline search
rg -o "\d{3}-\d{4}" Print only the match Great for extraction
rg --stats "TODO" Show match statistics
ag "pattern" The Silver Searcher Slightly slower than rg, same idea

Install: brew install ripgrep (for rg), brew install the_silver_searcher (for ag)

When to use which

Tool Speed .gitignore aware Regex flavor Best for
grep Slow No BRE/ERE Single file, quick checks
rg Fastest Yes Rust regex (PCRE2 optional) Daily driver, codebase search
ag Fast Yes PCRE If you already have it
grep -r Slowest No BRE/ERE When nothing else is installed
zgrep N/A No BRE Compressed files

Practical recipes

Find files

# All Python files modified in the last 24 hours
fd -e py --changed-within 1d
# find equivalent:
find . -name "*.py" -mtime -1

# All Python files modified today (calendar day)
fd -e py --changed-within 1d
# mdfind equivalent (macOS):
mdfind -onlyin . "kMDItemFSName == '*.py' && kMDItemFSContentChangeDate > \$time.today"

# Files larger than 100MB
find . -type f -size +100M
# With human-readable sizes (macOS/BSD):
find . -type f -size +100M -exec ls -lh {} \;
# GNU coreutils version:
gfind . -type f -size +100M -printf '%s %p\n' | sort -rn

# Find and delete all .DS_Store files
fd -H -t f '.DS_Store' -x rm
# find equivalent:
find . -name '.DS_Store' -type f -delete

# Find empty files
find . -type f -empty

# Find empty directories
find . -type d -empty

# Find broken symlinks
find . -type l ! -exec test -e {} \; -print

# Find files NOT matching a pattern
fd -e py --exclude "test_*"

# Find duplicate filenames (different directories)
fd -t f | xargs -I{} basename {} | sort | uniq -d

# Executable files
find . -type f -perm +111
# GNU:
find . -type f -executable

Search content

# Search for "TODO" in all Go files
rg -t go "TODO"

# Search for a string, replace it (preview)
rg "old_name" -l | xargs sed -n 's/old_name/new_name/gp'
# Actually replace:
rg "old_name" -l | xargs sed -i '' 's/old_name/new_name/g'   # macOS BSD sed
rg "old_name" -l | xargs sed -i 's/old_name/new_name/g'      # GNU sed

# Find all imports of a module
rg "^(import|from)\s+requests" -t py

# Find function definitions
rg "def (test_\w+)" -t py -o --no-filename | sort -u

# Count TODOs per file
rg -c "TODO" --sort path

# Search with context, piped to less
rg -C5 "database" --color=always | less -R

# Search for IP addresses
rg -o "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"

# Search only in filenames matching a pattern
rg "error" -g "*.log"

# Files containing BOTH "import" and "class" (two greps)
rg -l "import" | xargs rg -l "class"

Combo patterns

# Fuzzy find a file, open in $EDITOR
fd -t f | fzf --preview 'head -50 {}' | xargs $EDITOR

# Fuzzy search file contents
rg --no-heading --line-number "" | fzf --delimiter : --preview 'bat --color=always {1} --highlight-line {2}'

# Find recently modified files, search their contents
fd --changed-within 2h -e py | xargs rg "def "

# Find large files, sorted
find . -type f -size +10M -exec ls -lhS {} + 2>/dev/null | head -20

# Git-tracked files only, search contents
git ls-files '*.py' | xargs rg "pattern"

Emacs integration

Built-in

Command What it does Notes
M-x grep Run grep, results in buffer Basic, always available
M-x rgrep Recursive grep with prompts Asks for pattern, file glob, directory
M-x find-dired Run find, results in dired Good for file operations after search
M-x find-name-dired Find by name, results in dired Simpler than find-dired
M-x find-grep-dired Find + grep combo in dired Files containing a pattern
M-x occur Search current buffer, list matches M-x multi-occur for multiple buffers

Ivy/Counsel ecosystem

;; Requirements: counsel, ivy
(use-package counsel
  :bind
  (("C-c g" . counsel-rg)        ; ripgrep from project root
   ("C-c f" . counsel-fzf)       ; fzf integration
   ("C-c l" . counsel-locate)))  ; locate integration

;; counsel-rg is the daily driver:
;; M-x counsel-rg  →  type pattern  →  results stream in  →  C-n/C-p to navigate
;; Prefix with C-u to change starting directory
Command What it does
counsel-rg Ripgrep with live results
counsel-fzf Fuzzy file finder
counsel-locate System locate integration
counsel-git-grep Grep only git-tracked files

Vertico/Consult ecosystem

;; Requirements: consult, vertico, orderless (recommended)
(use-package consult
  :bind
  (("C-c g" . consult-ripgrep)   ; ripgrep with live preview
   ("C-c f" . consult-find)      ; find with live preview
   ("C-c l" . consult-locate)))  ; locate integration

;; consult-ripgrep: type #pattern#filter to narrow results
;; e.g., #TODO#.py  →  searches "TODO" in files matching ".py"
Command What it does
consult-ripgrep Ripgrep with live preview
consult-grep Regular grep with live preview
consult-find Find files with preview
consult-locate Locate with preview
consult-line Search lines in current buffer (like swiper)

Projectile / project.el

;; Projectile (classic)
(projectile-ripgrep "pattern")    ; rg in project
(projectile-grep "pattern")      ; grep in project
(projectile-find-file)            ; find file in project (C-c p f)

;; project.el (built-in, Emacs 28+)
(project-find-regexp "pattern")   ; grep in project (C-x p g)
(project-find-file)               ; find file in project (C-x p f)

Helm ecosystem

Command What it does
helm-do-ag Silver Searcher with helm
helm-ag ag from project root
helm-do-grep-ag ag with file type selection
helm-find find integration
helm-locate locate integration

wgrep: edit grep results in-place

This is the killer feature. Search, edit matches in the grep buffer, save back to files.

(use-package wgrep
  :config
  (setq wgrep-auto-save-buffer t))  ; auto-save after applying changes

;; Workflow:
;; 1. M-x counsel-rg (or consult-ripgrep)  →  search for pattern
;; 2. C-c C-o  →  export results to occur/grep buffer (ivy: C-c C-o, consult: embark-export)
;; 3. C-c C-p  →  enter wgrep mode (editable)
;; 4. Edit the matches directly in the buffer
;; 5. C-c C-c  →  apply changes to all files
;; 6. C-c C-k  →  discard changes

This replaces sed for most interactive refactoring. Search for the old name, edit in the buffer, apply. No regex escaping headaches.

My Emacs search setup

The Emacs integration section above covers generic options. Here’s my actual config. Vertico + Orderless + Marginalia + Consult stack, no Ivy/Counsel/Helm.

Completion framework

The foundation. Vertico handles the minibuffer UI, Orderless makes every prompt a fuzzy/regex match, Marginalia annotates candidates with metadata.

(use-package vertico
  :init (vertico-mode)
  :config (setq vertico-cycle t))

(use-package orderless
  :config
  (setq completion-styles '(orderless basic)
        completion-category-defaults nil
        completion-category-overrides '((file (styles partial-completion)))))

(use-package marginalia
  :after vertico
  :init (marginalia-mode))

orderless is the secret sauce. Type space-separated tokens in any order, and it matches all of them. py init matches __init__.py, init_config.py, anything with both substrings. partial-completion override for files means ~/p/s/f TAB still expands paths.

Consult: the search engine

Every search command gets live preview. The :preview-key 'any setting means candidates preview as you move through them, no extra keypress needed.

(use-package consult
  :bind (("C-x b" . consult-buffer)      ; buffer switching with preview
         ("C-s" . consult-line)           ; replaces isearch for in-buffer search
         ("M-g g" . consult-goto-line)
         ("M-s r" . consult-ripgrep))     ; project-wide ripgrep
  :config
  (consult-customize
   consult-ripgrep consult-git-grep consult-grep
   consult-line consult-find
   consult-projectile-find-file consult-projectile-recentf
   :preview-key 'any)
  (setq consult-find-args "find . -not ( -path */.git* -prune )"))

Key rebinds worth noting:

Consult + Projectile

consult-projectile bridges the two. File finding and recent files get consult’s preview and orderless matching.

(use-package consult-projectile
  :after (consult projectile)
  :config
  (setq consult-projectile-use-projectile-switch-project t))

Projectile config

Projectile handles project detection. When rg is available, it uses rg --files for indexing, which respects .gitignore and is fast.

(use-package projectile
  :init
  (setq projectile-project-search-path
        '(("~/zcode/" . 1) ("~/mcode/" . 1) ("~/ocode/" . 1) ("~/zorg" . 1))
        projectile-completion-system 'default
        projectile-enable-caching t
        projectile-indexing-method 'alien
        projectile-sort-order 'recentf)
  :config
  (projectile-mode 1)
  (when (executable-find "rg")
    (setq projectile-generic-command "rg --files --color=never --hidden"))
  (projectile-discover-projects-in-search-path)
  :bind-keymap ("C-c p" . projectile-command-map))

projectile-project-search-path auto-discovers projects under ~/zcode/, ~/mcode/, ~/ocode/, and ~/zorg/. The (. 1) means one level deep.

Search hydra

All search commands behind a single C-c s prefix. Organized by what I’m looking for.

(use-package hydra)

(defhydra hydra-search (:color blue :hint nil)
  "
╭─────────────────────────────────────────────────╮
│ Project Search                                  │
├─────────────────────────────────────────────────┤
│ Content:  _r_ ripgrep   _g_ git-grep   _l_ line │
│ Files:    _f_ find      _F_ recent     _d_ dir  │
│ Buffers:  _b_ all       _B_ project             │
│ Projects: _p_ switch    _P_ find-file           │
╰─────────────────────────────────────────────────╯
"
  ("r" consult-ripgrep "ripgrep in project")
  ("g" consult-git-grep "git grep (faster)")
  ("l" consult-line "search current buffer")
  ("f" consult-projectile-find-file "find file in project")
  ("F" consult-projectile-recentf "recent files")
  ("d" consult-find "find in directory")
  ("b" consult-buffer "switch buffer")
  ("B" consult-project-buffer "project buffers")
  ("p" consult-projectile-switch-project "switch project")
  ("P" (lambda () (interactive)
         (consult-projectile-switch-project)
         (consult-projectile-find-file)) "project → file")
  ("q" nil "quit"))

(global-set-key (kbd "C-c s") #'hydra-search/body)

C-c s r for ripgrep, C-c s f for find file, C-c s g for git-grep. Blue hydra means each key exits after action, no sticky menu.

Not strictly search, but they close the loop between “found it” and “I’m there.”

(use-package avy
  :custom
  (avy-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))
  (avy-timeout-seconds 0.5)
  :bind
  (("C-'" . avy-goto-char-timer)    ; jump to visible text
   ("C-:" . avy-goto-char-2)
   ("M-g w" . avy-goto-word-1))
  :config
  (with-eval-after-load 'isearch
    (define-key isearch-mode-map (kbd "C-'") 'avy-isearch)))  ; jump during isearch

(use-package expand-region
  :bind ("C-=" . er/expand-region))   ; grow selection around search results

Dired as search output

(global-set-key (kbd "C-x C-j") 'dired-jump)

C-x C-j opens dired at the current file’s directory. From there, find-dired and find-name-dired dump find results into a dired buffer for bulk file operations (rename, delete, chmod).

Missing piece: wgrep

I don’t have wgrep in my config yet, but I should. It lets you edit grep/ripgrep results in-place and write changes back to files. The config from the section above works:

(use-package wgrep
  :config
  (setq wgrep-auto-save-buffer t))

The workflow: consult-ripgrepembark-export to a grep buffer → C-c C-p to enter wgrep → edit matches → C-c C-c to apply. Replaces sed for most interactive refactoring.

Common usage reference

Quick-reference table

Keybindings for my Vertico/Consult/Projectile setup. Not generic Emacs, these are the bindings from the config above.

I want to… Keys What happens
Search current buffer C-s consult-line with orderless fuzzy matching
Search project contents M-s r or C-c s r consult-ripgrep with live preview
Find file in project C-c p f or C-c s f consult-projectile-find-file
Find recent file C-c s F consult-projectile-recentf
Git grep (faster in git repos) C-c s g consult-git-grep
Search and replace across files M-s r → export → wgrep → edit → apply See workflow below
Find file by name anywhere C-c s d consult-find in directory
Switch buffer C-x b consult-buffer with preview
Jump to line M-g g consult-goto-line
Open search hydra C-c s hydra-search menu
Jump to visible text C-' avy-goto-char-timer
Open file manager at current file C-x C-j dired-jump
Switch project C-c s p or C-c p p consult-projectile-switch-project
Project buffers only C-c s B consult-project-buffer

Workflow recipes

Rename a variable across a project

  1. M-s r (or C-c s r) → type the old variable name
  2. Results stream in with live preview. Verify the matches are correct.
  3. embark-export (usually C-c C-o or E in embark) → exports matches to a grep buffer
  4. C-c C-p → enter wgrep mode. The buffer becomes editable.
  5. M-% (query-replace) or manual edit → change the variable name in each match
  6. C-c C-c → apply all changes to the source files
  7. C-c C-k if you want to discard instead

Find a file I half-remember the name of

  1. C-c s f → type fragments of the filename, in any order
  2. Orderless matches fuzzy: conf yaml finds yaml_config.py, config.yaml, etc.
  3. Live preview shows the file content as you move through candidates
  4. RET opens it

Search then jump to a specific match

  1. C-c s r → type the search pattern
  2. Results appear, live preview shows surrounding context as you navigate
  3. Arrow keys or C-n/C-p to find the right match
  4. RET opens the file at the exact line
  1. C-c s P → select a project from the list
  2. Immediately opens consult-projectile-find-file in the new project
  3. Or: C-c s p to switch project, then C-c s r to ripgrep in it

GNU coreutils on macOS

macOS ships BSD versions of find, grep, sed, xargs, etc. They differ from GNU versions in small but annoying ways.

Key differences

Feature BSD (macOS default) GNU (Linux / Homebrew)
sed -i Requires '' arg: sed -i '' 's/a/b/' No arg needed: sed -i 's/a/b/'
find -printf Not supported Supported
find -executable Not supported Supported
find -delete Supported (both) Supported (both)
grep -P (Perl regex) Not supported Supported
xargs -d (delimiter) Not supported Supported
readlink -f Not supported Supported
date formatting Different flags Different flags
stat output Different format strings Different format strings

Install GNU coreutils

brew install coreutils findutils grep gnu-sed gawk

# GNU tools are prefixed with 'g':
# gfind, ggrep, gsed, gawk, gxargs, greadlink, gstat, gdate
#
# If you want them as default (unprefixed), add to PATH:
export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:$PATH"
export PATH="/opt/homebrew/opt/findutils/libexec/gnubin:$PATH"
export PATH="/opt/homebrew/opt/grep/libexec/gnubin:$PATH"
export PATH="/opt/homebrew/opt/gnu-sed/libexec/gnubin:$PATH"

My preference: Keep the g prefix. Muscle memory is already on fd and rg for daily work. I only reach for gfind or gsed when I need GNU-specific features like -printf or -P.

Performance comparison

Tested on a ~1M-file monorepo. Numbers are approximate, your mileage will vary.

Task Tool Time Notes
Find file by name fd ~0.3s Respects .gitignore
find ~2.5s Searches everything
mdfind ~0.05s Pre-indexed, macOS only
locate ~0.1s Pre-indexed, may be stale
Content search rg ~0.8s Parallelized, .gitignore aware
ag ~1.2s Good, slightly slower
grep -r ~8s No parallelism, searches everything
git grep ~0.6s Fast, git-tracked only

Why rg and fd are fast

  1. Parallelism. They use all CPU cores by default.
  2. .gitignore awareness. Skip node_modules, __pycache__, .git, vendor, etc. automatically.
  3. Smart defaults. Skip binary files, use memory-mapped I/O.
  4. Regex engine. Rust’s regex crate compiles to DFA, avoids catastrophic backtracking.

When to NOT use rg/fd

Cross-platform reference

Linux

Most of the above works identically on Linux. Key differences:

Topic macOS Linux
Default grep BSD (no -P) GNU (has -P)
Default find BSD (no -printf) GNU (has -printf)
Default sed BSD (-i '' required) GNU (-i without arg)
Package manager brew install apt install / dnf install / pacman -S
locate Needs manual setup Usually pre-installed + cron
mdfind Spotlight CLI N/A (use locate or recoll)

Windows

Unix command PowerShell equivalent Notes
find . -name "*.py" Get-ChildItem -Recurse -Filter "*.py" Alias: gci -R or ls -R
find . -type f -size +100M gci -R | ? { $_.Length -gt 100MB }
grep -r "pattern" . Select-String -Recurse -Pattern "pattern" Alias: sls
grep -rl "pattern" . sls -Recurse -Pattern "pattern" | select Path -Unique
N/A findstr /s /i "pattern" *.* CMD legacy, still works

WSL is the escape hatch. If you’re on Windows and want the Unix tools, wsl gives you a real Linux environment. All the rg, fd, fzf commands work as-is inside WSL.

# Install fd and rg on Windows (without WSL)
winget install sharkdp.fd
winget install BurntSushi.ripgrep.MSVC
scoop install fzf

# They work identically:
fd -e py
rg "pattern" -t py

Install everything

# macOS (Homebrew)
brew install fd ripgrep fzf the_silver_searcher \
             coreutils findutils grep gnu-sed

# Ubuntu/Debian
sudo apt install fd-find ripgrep fzf silversearcher-ag
# Note: on Debian/Ubuntu, fd is installed as 'fdfind'
# alias fd=fdfind

# Arch
sudo pacman -S fd ripgrep fzf the_silver_searcher

# Windows (winget)
winget install sharkdp.fd
winget install BurntSushi.ripgrep.MSVC
winget install junegunn.fzf

Quick reference card

The 10 commands I use most, in order:

# Command What When
1 rg "pattern" Search file contents Multiple times daily
2 fd pattern Find files by name Multiple times daily
3 fd -e ext Find files by extension Daily
4 rg -t py "pattern" Search in specific file type Daily
5 fd | fzf Fuzzy find a file When I half-remember the name
6 consult-ripgrep Search from Emacs When I’m already in Emacs
7 rg -l "pattern" List matching files Before bulk operations
8 find . -size +100M Find large files Disk cleanup
9 fd -H '.DS_Store' -x rm Delete junk files Occasional cleanup
10 mdfind -name "file" Instant filename search When I have no idea where it is

The best search tool is the one you remember the flags for. Pin this somewhere.