grep and find are your go-to tools for digging through the filesystem in Bash, but understanding their interplay is key to unlocking their true power.

Let’s see find and grep in action. Imagine you’re looking for all .log files in your home directory and its subdirectories that contain the exact phrase "connection refused":

find ~ -type f -name "*.log" -exec grep -H "connection refused" {} \;

This command does a few things:

  1. find ~: Starts searching from your home directory (~).
  2. -type f: Restricts the search to only files.
  3. -name "*.log": Filters for files whose names end with .log.
  4. -exec ... \;: Executes a command for each file found.
  5. grep -H "connection refused" {}: The command executed. grep searches for "connection refused", and -H prints the filename before each match. {} is a placeholder for the current file found by find.

Here’s a more complex scenario: finding all Python files (.py) modified in the last 7 days that have the word "TODO" on a line starting with #:

find . -type f -name "*.py" -mtime -7 -exec grep -H -E "^#.*TODO" {} \;

Let’s break this down:

  • find .: Starts the search in the current directory.
  • -type f: Again, only files.
  • -name "*.py": Looking for Python files.
  • -mtime -7: Finds files modified less than 7 days ago.
  • -exec grep -H -E "^#.*TODO" {} \;: Executes grep.
    • -H: Prepends the filename.
    • -E: Enables extended regular expressions.
    • "^#.*TODO": This is the pattern. ^ anchors to the start of the line, # matches the literal hash, .* matches any character zero or more times, and TODO matches the literal string. This ensures we find lines like # TODO: Fix this but not My TODO list.

The mental model for find is that it’s a powerful file selector. It navigates the directory tree, applying a series of tests (name, type, size, modification time, permissions, etc.) to identify specific files. Once it has a list of files that pass all its tests, it can then perform an action on them. The -exec action is where grep usually comes in for text searching.

grep, on the other hand, is a text pattern matcher. It reads input (from files or standard input) line by line and checks if each line matches a given pattern. Its strength lies in its sophisticated pattern matching capabilities, from simple strings to complex regular expressions.

Think of find as the scout, meticulously mapping out the terrain and identifying every potential target. grep is the sniper, precisely hitting the targets identified by the scout. You can also reverse this: pipe the output of grep to find, though this is less common for simple searches. For example, if you wanted to find all files containing "error" and then only report the ones that are larger than 1MB:

grep -rl "error" . | xargs find . -type f -size +1M -print

Here, grep -rl "error" . lists all files (-l) recursively (-r) containing "error" starting from the current directory. The output is then piped to xargs, which takes that list of filenames and passes them as arguments to find. find then filters this list further to include only files (-type f) larger than 1MB (-size +1M).

The -exec ... {} + syntax in find is often more efficient than -exec ... {} \; because it passes multiple filenames to a single grep command invocation, rather than running grep once for every single file found. For example, the first command could be written as:

find ~ -type f -name "*.log" -exec grep -H "connection refused" {} +

This reduces the overhead of starting many grep processes. The + tells find to build up a list of files and pass them to grep in batches, similar to how xargs works.

A common pitfall is forgetting to quote patterns containing special shell characters, leading grep to interpret them incorrectly or causing the shell to expand them prematurely. Always quote your search patterns, especially when they involve regular expressions. For instance, grep '.*' is very different from grep .* (where .* might be expanded by the shell if there are files matching that pattern in the current directory).

The next step in mastering command-line searching is to explore more advanced grep features like -i for case-insensitive searching, -v for inverting matches, -w for whole-word matching, and grep -P for Perl-compatible regular expressions, which offer even more powerful pattern matching.

Want structured learning?

Take the full Bash course →