Finding Files and Searching Text: find, locate, and grep
"Where is the file?" and "which file contains this message?" are two different questions, even though beginners often reach for the same command to answer both. find and locate search by path, name, and metadata. grep searches the actual content of a file, line by line. Picking the right tool saves time and prevents a permission error from being misread as "nothing matched."
The examples assume Ubuntu Server LTS with GNU Findutils and GNU Grep, running in Bash. You should already be comfortable with quoting and wildcards from File and Directory Commands, and with standard output and standard error from Viewing Text Files. This article covers searching by name, type, size, and time with find; reading grep output and exit status correctly; and handling filenames that contain spaces or newlines without corrupting the result.
Lab Setup
mkdir -p ~/lab/search/{config,logs,archive}
printf 'listen_port=8080\nenv=production\n' > ~/lab/search/config/app.conf
printf 'level=INFO order_id=ord-1001 message=checkout started\n' > ~/lab/search/logs/app.log
printf 'level=ERROR order_id=ord-1002 message=payment gateway timeout\n' >> ~/lab/search/logs/app.log
printf 'level=error order_id=ord-1003 message=invalid card\n' > ~/lab/search/logs/auth.log
printf 'quarterly report draft\n' > ~/lab/search/archive/report.txt
touch -d '10 days ago' ~/lab/search/archive/report.txt
find ~/lab/search -maxdepth 3 -printf '%y %p\n'
touch -d is a GNU Coreutils extension. It only changes the timestamp of this lab file, so later -mtime and -newer examples have something meaningful to compare.
find: Walking the Live File System
The general shape of a find invocation is:
Here:
~/lab/searchis the starting point, where the search begins.-type fis a test that matches regular files.-name '*.log'is a test that matches the basename against a pattern.-printis the action that writes each matching path to standard output.
When no operator separates two tests, find assumes logical AND (-a). The pattern '*.log' is quoted so that find evaluates it once per candidate file, instead of letting Bash expand it against the current directory before find even runs.
Always write the starting point explicitly. GNU find defaults to . when it is omitted, but which directory a script happens to be run from can silently change the result.
Matching by Name and Path
find ~/lab/search -type f -name '*.conf'
find ~/lab/search -type f -iname '*.LOG'
find ~/lab/search -path '*/archive/*'
-name compares the basename case-sensitively, -iname ignores case, and -path compares the entire matched path instead of just the final component. The exact Unicode behavior of -iname depends on locale and implementation, so keep naming conventions consistent rather than relying on case-insensitive matching in automation.
Type, Depth, Size, and Time
find ~/lab/search -maxdepth 2 -type d -print
find ~/lab/search -type f -size +1k -print
find ~/lab/search -type f -mtime +7 -print
find ~/lab/search -type f -newer ~/lab/search/config/app.conf -print
-type fmatches regular files,-type dmatches directories,-type lmatches symbolic links.-maxdepth 2limits the walk to two levels below the starting point.-size +1kselects files larger than 1024 bytes on GNUfind.-mtime +7selects files whose modification time falls outside the last seven complete 24-hour periods.-newer FILEcompares each candidate's modification time against a reference file's modification time.
Do not read -mtime 1 as "yesterday" in the calendar sense. GNU find rounds by 24-hour periods from the moment the command runs, not from local midnight. When minute-level precision matters, use -mmin; when a true calendar boundary matters, check -daystart in your local man find.
Combining Tests with Logic
The parentheses are escaped because the shell treats them as special characters. Without parentheses, operator precedence between -a, -o, and ! can produce a result you did not intend. Negation looks like this:
Acting on What You Found
A read-only example:
{} is replaced with the matched paths, and + batches as many paths as possible into a single invocation of file. The alternative form, -exec ... {} \;, typically starts one new process per match, which is far slower on a large tree.
Danger
find ... -delete, -exec rm, -exec chmod, and -exec chown change many files at once, and there is no confirmation step by default. Before running any of them against production data, run the identical expression with -print first, check whether the search should cross mount points or follow symlinks, and take a backup or VM snapshot. Even in a lab, prefer -ok (which asks before each action) or a separate, reviewed step over a bare -exec rm.
-execdir reduces some path-race risks in directories a less-trusted user can write to, but it does not replace a safe PATH or the invoked command's own precautions. Do not make an unrestricted find / your default troubleshooting habit while running as root.
Reading find's Errors: Permission Denied
On a real system, find walks directories your user cannot enter, and each one produces a line on standard error, interleaved with the matches on standard output:
The matches (stdout) and the errors (stderr) are two separate streams that happen to share the terminal. That distinction is the whole game the moment you redirect:
find / -name '*.conf' 2>/dev/nullhides every error — including a mistyped path or a genuinely unreadable target you needed to know about. A truncated result then looks identical to a complete one.find / -name '*.conf' 2>find-errors.logkeeps the matches clean on screen while preserving the errors for review — the safer default when the search result has to be trusted.
Blanket-discarding stderr is the single most common way a find-based audit silently under-reports. If you legitimately only care about readable locations, make that explicit — start the search inside a directory you own, rather than muting the very evidence that the scope was incomplete.
locate: Fast Name Search from an Index
On Ubuntu LTS, locate is usually provided by the plocate package and may not be present on a minimal install:
If it is available:
locate searches an index that updatedb builds ahead of time, which is why it is fast even on a large file system. The trade-off is staleness:
- a file created after the last index update will not appear yet;
- a deleted file's name may linger in the index for a while;
updatedb.confmay exclude certain paths or file systems entirely;- what you see depends on the index's own visibility rules, not the live file system.
Use find when you need the current, real-time state and metadata filters. Rebuilding the index costs I/O and follows system policy, so do not run sudo updatedb in production just to locate a single file. On Ubuntu, plocate's index refresh is normally handled by a systemd timer already.
grep: Searching Inside Files
For a literal string, -F is the most precise choice:
-n adds the line number. By default grep prints the entire matching line, not just the matched substring; -o prints only the matched portion.
Options used often together:
grep -niF 'level=error' ~/lab/search/logs/*.log
grep -cF 'level=ERROR' ~/lab/search/logs/app.log
grep -vF 'level=INFO' ~/lab/search/logs/app.log
grep -lF 'timeout' ~/lab/search/logs/*.log
grep -C 1 -F 'timeout' ~/lab/search/logs/app.log
-iignores case.-cprints the count of matching lines, not the count of matches within a line.-vselects lines that do not match.-lprints the name of each file that has at least one match, and nothing else.-C 1shows one line of context before and after each match.
Literal Text, Basic Regex, and Extended Regex
With -F, every character is literal. Plain grep uses basic regular expressions (BRE); grep -E uses extended regular expressions (ERE). In a regex, ^ anchors to the start of the line, $ to the end, . matches any single character, and * repeats the preceding element zero or more times. That * is unrelated to the shell's wildcard *, which expands filenames before grep ever sees them.
When the search term comes from an untrusted source, use grep -F -- "$needle":
-- stops grep from treating a value that begins with - as an option, and quoting the variable stops word splitting and globbing.
Recursive Search
-R may follow symbolic links; -r usually does not, which makes it the safer default when you are not sure what a directory tree contains. An uncontrolled recursive search over a large repository, or over /, adds real I/O load. Narrow the scope with --include, --exclude, and --exclude-dir.
Reading the Exit Status Correctly
GNU grep uses three exit statuses:
0: at least one line matched.1: no line matched.2: an error occurred (bad pattern, unreadable file, and so on).
if grep -qF 'level=ERROR' ~/lab/search/logs/app.log; then
echo "found an ERROR line"
else
status=$?
if ((status == 1)); then
echo "no ERROR line found"
else
printf 'grep failed, status=%s\n' "$status" >&2
fi
fi
-q suppresses normal output and works well in a condition. Redirecting 2>/dev/null on top of this can hide the difference between "no match" and "grep failed" — do not do that where the distinction matters.
Filenames with Spaces and Newlines
A Linux filename can legally contain a newline character. Because of that, find ... -print | while read ... is not reliable for every possible name. Separate entries with NUL bytes instead:
find ~/lab/search -type f -name '*.log' -print0 |
while IFS= read -r -d '' file; do
printf 'file: %q\n' "$file"
done
Bash's read -d '' reads up to a NUL byte. When piping into external tools, find -print0 paired with xargs -0 is common, but find -exec ... {} + is simpler and safer when it fits the task.
Running Work in Parallel: xargs -0 -P
find -exec ... {} + runs one command, however many arguments it can batch in. xargs offers the same NUL-safe batching plus one thing -exec cannot do on its own: running several invocations of the command at the same time.
-print0on thefindside terminates each path with a NUL byte instead of a newline, so a filename containing a space, a tab, or even a literal newline is never split or misread.-0on thexargsside tells it to expect NUL-terminated input, matching-print0.-P4runs up to four instances ofgzipconcurrently instead of one after another — useful when compressing, checksumming, or converting a large batch of independent files on a multi-core machine, since each invocation blocks on its own I/O and CPU work.-I{}substitutes each path into{}in the command template, one path per invocation.
Without -print0/-0, xargs's default whitespace-and-newline splitting silently corrupts any filename that contains a space or a newline — it is treated as two or more separate arguments rather than one path. This is the same failure mode as the read loop shown above, and it is a common source of "the script works on my test files but breaks in production" bugs, since production trees eventually contain a name nobody anticipated.
Interview angle
If asked why -print0/-0 matters, the answer is not "it's the modern way" — it is that a Linux filename can contain any byte except / and NUL, including spaces and newlines, so any tool that splits on whitespace or newlines by default (xargs, a naive for f in $(find ...), or read without -d '') is only safe by accident until it meets a filename that breaks the assumption.
Practical Scenario: Tracing a Timeout
Problem: you need to know which log files, and which exact lines, mention a timeout somewhere under the application directory.
-
Confirm the scope and inventory the objects first:
-
Search only the log files, as a literal string:
-
For a larger tree, bound the recursion instead of relying on a glob:
-
Add context when you need to see what happened around the match:
The result shows the path, the line number, and the event itself. Before sharing a production log, mask tokens, cookies, emails, and any other sensitive data it may contain.
Common Mistakes
- Treating
findandgrepas interchangeable, when one searches names and metadata and the other searches content. - Leaving
-name '*.log'unquoted and letting the shell expand it beforefindruns. - Reading
-mtime +1as "before yesterday" in a calendar sense instead of a rolling 24-hour period. - Assuming
locatereflects the current, real-time state of the file system. - Assuming
grep -ccounts every occurrence of the text instead of the number of matching lines. - Passing user-supplied text straight into a regex without considering it as a literal string first.
- Interpreting
grepexit status1as an operational failure, or status2as "no match." - Discarding permission errors into
/dev/nulland concluding the search was complete. - Processing filenames that may contain newlines with a line-based pipeline.
- Running a broad
find /search as root out of habit rather than necessity.
Exercises
- In the lab directory, find only the plain
.conffiles usingfind, and explain each part of the expression you wrote. - Create two additional lab files: one larger than 1 KiB, one modified in the last five minutes. Write a
findexpression that isolates each condition separately. - Write three patterns suited to
grep -F, plaingrep, andgrep -Erespectively, and note why each mode was the right choice for that pattern. - Reproduce
grepexit statuses0,1, and2safely inside the lab directory. - Create two files whose names contain a space and a newline, then list both correctly with a
find -print0loop.
Verification criterion: you should be able to explain, with evidence, the difference between "no match," "the search itself failed," and "the index has not been updated yet."
References
- GNU Findutils manual: https://www.gnu.org/software/findutils/manual/html_node/find_html/index.html
- GNU Findutils: running a command on multiple files: https://www.gnu.org/software/findutils/manual/html_node/find_html/Multiple-Files.html
- GNU Grep manual: https://www.gnu.org/software/grep/manual/grep.html
- GNU Grep: Exit Status: https://www.gnu.org/software/grep/manual/html_node/Exit-Status.html
- Ubuntu manual page:
plocate(1): https://manpages.ubuntu.com/manpages/noble/man1/plocate.1.html - Ubuntu manual page:
updatedb(8): https://manpages.ubuntu.com/manpages/noble/man8/updatedb.plocate.8.html