Skip to content

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:

find STARTING_POINT... EXPRESSION
find ~/lab/search -type f -name '*.log' -print

Here:

  • ~/lab/search is the starting point, where the search begins.
  • -type f is a test that matches regular files.
  • -name '*.log' is a test that matches the basename against a pattern.
  • -print is 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 f matches regular files, -type d matches directories, -type l matches symbolic links.
  • -maxdepth 2 limits the walk to two levels below the starting point.
  • -size +1k selects files larger than 1024 bytes on GNU find.
  • -mtime +7 selects files whose modification time falls outside the last seven complete 24-hour periods.
  • -newer FILE compares 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

find ~/lab/search -type f \( -name '*.log' -o -name '*.conf' \) -print

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:

find ~/lab/search -type f ! -path '*/archive/*' -print

Acting on What You Found

A read-only example:

find ~/lab/search -type f -name '*.conf' -exec file -- {} +

{} 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:

/etc/app/app.conf
find: '/etc/ssl/private': Permission denied
/etc/hosts

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/null hides 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.log keeps 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:

command -v locate || echo "locate is not installed"

If it is available:

locate app.conf
locate -b '\app.conf'

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.conf may 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:

grep -nF 'level=ERROR' ~/lab/search/logs/app.log
2:level=ERROR order_id=ord-1002 message=payment gateway timeout

-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
  • -i ignores case.
  • -c prints the count of matching lines, not the count of matches within a line.
  • -v selects lines that do not match.
  • -l prints the name of each file that has at least one match, and nothing else.
  • -C 1 shows one line of context before and after each match.

Literal Text, Basic Regex, and Extended Regex

grep -F 'a.b' file.txt
grep '^level=' file.txt
grep -E 'level=(ERROR|WARN)' file.txt

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":

needle='message=timeout'
grep -nF -- "$needle" ~/lab/search/logs/app.log

-- stops grep from treating a value that begins with - as an option, and quoting the variable stops word splitting and globbing.

grep -RInF --include='*.log' 'timeout' ~/lab/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.

find ~/lab/search -type f -name '*.log' -print0 |
    xargs -0 -P4 -I{} gzip -k -- {}
  • -print0 on the find side 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.
  • -0 on the xargs side tells it to expect NUL-terminated input, matching -print0.
  • -P4 runs up to four instances of gzip concurrently 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.

  1. Confirm the scope and inventory the objects first:

    root=$HOME/lab/search
    test -d "$root/logs" || exit 1
    find "$root/logs" -maxdepth 1 -type f -name '*.log' -printf '%f %s bytes\n'
    
  2. Search only the log files, as a literal string:

    grep -HnF 'timeout' "$root"/logs/*.log
    grep_status=$?
    printf 'grep status=%s\n' "$grep_status"
    
  3. For a larger tree, bound the recursion instead of relying on a glob:

    grep -rHnF --include='*.log' --exclude-dir=archive 'timeout' "$root"
    
  4. Add context when you need to see what happened around the match:

    grep -HnC 1 -F 'timeout' "$root"/logs/app.log
    

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 find and grep as interchangeable, when one searches names and metadata and the other searches content.
  • Leaving -name '*.log' unquoted and letting the shell expand it before find runs.
  • Reading -mtime +1 as "before yesterday" in a calendar sense instead of a rolling 24-hour period.
  • Assuming locate reflects the current, real-time state of the file system.
  • Assuming grep -c counts 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 grep exit status 1 as an operational failure, or status 2 as "no match."
  • Discarding permission errors into /dev/null and 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

  1. In the lab directory, find only the plain .conf files using find, and explain each part of the expression you wrote.
  2. Create two additional lab files: one larger than 1 KiB, one modified in the last five minutes. Write a find expression that isolates each condition separately.
  3. Write three patterns suited to grep -F, plain grep, and grep -E respectively, and note why each mode was the right choice for that pattern.
  4. Reproduce grep exit statuses 0, 1, and 2 safely inside the lab directory.
  5. Create two files whose names contain a space and a newline, then list both correctly with a find -print0 loop.

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