Search a Whole Repo
Problem statement
Search every file in a folder tree at once with grep -r, limit the search to one kind of file, skip folders you do not care about, list which files match (or do not), and show lines around a match. You do this when you need to know "which manifests set replicas?", "where is this timeout configured?" or "which services forgot resource limits?".
The script builds this small repo:
repo/
k8s/web.yaml Deployment with replicas, resources and timeoutk8s/worker.yaml Deployment with replicas and timeout, no resourcesdocs/scaling.md mentions replicas in plain textapp/settings.py REQUEST_TIMEOUT = 15.git/ORIG_HEAD an old copy that mentions replicask8s/web.yaml
kind: Deploymentname: webreplicas: 3resources: limits: memory: 512Mitimeout: 30k8s/worker.yaml
kind: Deploymentname: workerreplicas: 1timeout: 120Print, in this order:
- The YAML files that set
replicas, without looking inside.git. - The YAML files that have no
resources:block. - Every line that mentions a timeout, in any case, with its file and line number.
- The
memory:line inweb.yamlwith the 2 lines above it.
Expected output:
== YAML files that set replicas (skip .git) ==./k8s/web.yaml./k8s/worker.yaml== YAML files with NO resources block ==./k8s/worker.yaml== every timeout, any case, with file and line ==./app/settings.py:1:REQUEST_TIMEOUT = 15./k8s/web.yaml:7:timeout: 30./k8s/worker.yaml:4:timeout: 120== the memory limit with 2 lines of context above it ==4-resources:5- limits:6: memory: 512MiHints
-r makes grep walk into every folder. --include='*.yaml' keeps only some files, and --exclude-dir=.git skips a folder.Approach
Optimal: grep -r with filters
Covers: grep -r, --include, --exclude-dir, -l, -L, -n, -i, -A, -B, -C, reading file:line:text, sorting results.
-r searches a whole tree. grep -r 'replicas' . starts at . (this folder), goes into every folder below it, and searches every file it finds. Each match is printed as file:text, and with -n as file:line:text, so you always know where it came from.
Choosing what to search. A real repo has things you do not want: .git holds old copies of every file, node_modules can hold thousands of files, and docs mention words in plain English. Two flags fix that:
searched"]:::green K --> WK["worker.yaml
searched"]:::green D --> S["scaling.md
not .yaml"]:::gray G --> O["skipped by
--exclude-dir"]:::red classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px
| Flag | Does | Example |
|---|---|---|
--include='*.yaml' |
only search files whose name matches | YAML manifests only |
--exclude='*.min.js' |
skip files whose name matches | skip built files |
--exclude-dir=.git |
do not go into folders with this name | skip git history |
Quote the patterns, like '*.yaml', so the shell passes the * to grep instead of expanding it itself. You can repeat a flag: --include='*.yaml' --include='*.yml'.
Choosing what to print.
| Flag | Prints |
|---|---|
| (none) | file:matching line |
-n |
file:line number:matching line |
-l |
only the names of files with at least one match |
-L |
only the names of files with no match |
-c |
file:count for every file |
-L is the hidden gem of audits. "Which manifests have no resource limits?" is really "which files do not contain resources:?", and grep -rL answers it in one line.
Seeing a match in its place. One matching line often makes no sense alone. In YAML, memory: 512Mi only means something once you see it sits under resources: and limits:. Context flags print neighbours:
| Flag | Adds |
|---|---|
-B 2 |
2 lines before each match |
-A 2 |
2 lines after each match |
-C 2 |
2 lines on both sides |
In the output, the matching line uses : after the line number, and context lines use -. When matches are far apart, grep prints -- between the groups.
context"]:::yellow ~~~ L5["5- limits:
context"]:::yellow ~~~ L6["6: memory: 512Mi
the match"]:::green end classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px style OUT fill:transparent,stroke:#7c3aed,stroke-width:2px
Why | sort? grep visits folders in the order the disk returns them, which can change between machines. Sorting the result gives the same order every time, which matters for comparing outputs and for tests.
Walking through the code. The # Setup: lines only build the sample repo, so skip past them.
grep -rl 'replicas' --include='*.yaml' --exclude-dir=.git .lists the two manifests.docs/scaling.mdis not a YAML file, and.gitis skipped.grep -rL 'resources:' ...lists the one manifest with no resources block.grep -rni 'timeout' --exclude-dir=.git .searches every file in any case, so it also findsREQUEST_TIMEOUTin the Python file.grep -n -B2 'memory:' k8s/web.yamlprints the match and the two lines above it.
Edge cases. With no matches, -l prints nothing and grep exits with 1. A folder you cannot read prints "Permission denied" on stderr; add 2>/dev/null to hide it. Binary files print "binary file matches" instead of the line; add -I to skip them.
# Setup: a small repo in a fresh temporary folder
cd "$(mktemp -d)"
mkdir -p k8s docs app .git
cat > k8s/web.yaml << 'F'
kind: Deployment
name: web
replicas: 3
resources:
limits:
memory: 512Mi
timeout: 30
F
cat > k8s/worker.yaml << 'F'
kind: Deployment
name: worker
replicas: 1
timeout: 120
F
cat > docs/scaling.md << 'F'
Raise replicas before a big sale.
F
cat > app/settings.py << 'F'
REQUEST_TIMEOUT = 15
F
echo 'replicas: old copy in git history' > .git/ORIG_HEAD
echo "== YAML files that set replicas (skip .git) =="
grep -rl 'replicas' --include='*.yaml' --exclude-dir=.git . | sort
echo "== YAML files with NO resources block =="
grep -rL 'resources:' --include='*.yaml' --exclude-dir=.git . | sort
echo "== every timeout, any case, with file and line =="
grep -rni 'timeout' --exclude-dir=.git . | sort
echo "== the memory limit with 2 lines of context above it =="
grep -n -B2 'memory:' k8s/web.yamlInterview follow-ups
Only search the files changed in the last commit.
Ask git for the list, then hand it to grep:
git diff --name-only HEAD~1 | grep '\.yaml$' | xargs grep -n 'replicas'. The middle grep keeps only YAML names. Plainxargssplits names at spaces, so for safety usegit diff -z --name-only HEAD~1 | xargs -0 grep -n 'replicas', where-zand-0separate names with a null character instead. This is how CI checks often look only at what a pull request changed.
Frequently asked questions
Without quotes, the shell expands *.yaml into the names of matching files in the current folder before grep even starts. If there are none, it may pass the text through unchanged and it seems to work, which hides the bug until the day a .yaml file appears there. Always quote: --include='*.yaml'. Also note that --include takes a file name pattern, while the search pattern is a regular expression, so . and * mean different things in each.
Inside a git repo, git grep -n 'replicas' only searches files tracked by git, so it skips .git, build output and ignored files without extra flags, and it is very fast. rg (ripgrep) does the same using .gitignore, and it is faster still on big trees, but it is not installed on most servers. Learn grep -r first, because it works on every Linux box. Reach for the others on your own laptop.
For lines with either word, use grep -rE 'timeout|retries', where | means "or" in an extended pattern (-E). You can also give several -e options: grep -r -e timeout -e retries .. For lines with both words, chain two greps: grep -r 'timeout' . | grep 'web'. For files that contain both words anywhere, use grep -rl timeout . | xargs grep -l retries, keeping the spaces-in-names problem in mind.