Right, let’s talk about find. This is the command you reach for when ls just won’t cut it. It’s the swiss army knife of file searching, capable of slicing through your filesystem based on almost any attribute you can think of: name, type, size, when you last cried over your code (modification time), and who’s allowed to see it (permissions). It’s powerful, it’s ubiquitous, and its syntax is a historical artifact that will make you question the life choices of early Unix developers. Don’t worry, we’ll get through it together.

The core structure of a find command is both logical and, frankly, a bit weird. It goes like this:

find [where to start looking] [what to look for] [what to do with it]

The [what to look for] part is made up of one or more tests, actions, and options. The most common pitfall is forgetting that these are all evaluated together logically. find isn’t running your tests one after another like a filter chain; it’s building a giant expression. This becomes crucial when we use -or and -and later.

The Basics: -name and -type

Let’s start with the two you’ll use 90% of the time. You want to find a file named stupid_mistake.txt? Here you go:

find . -name "stupid_mistake.txt"

See the dot .? That’s the starting directory. Always specify it. If you forget, find will merrily start from your current directory, which is usually what you want, but it’s good practice to be explicit. Now, the -name test. Crucially, it’s case-sensitive. The universe is case-sensitive, so find is, too. If you want to be that person who ignores case, use -iname. I won’t judge.

find . -iname "StUpId_MiStAkE.txt" # finds your file despite your shouting

Next, -type is your best friend. You almost never want to find directories when you’re looking for a file, and vice-versa. find . -name "src" will match both a file and a directory named src. That’s a great way to accidentally rm -rf the wrong thing. Be specific.

find . -type f -name "*.py"  # Only files ending in .py
find /var/log -type d -name "*apache*"  # Only directories with 'apache' in the name

The common types are f (file), d (directory), and l (symlink). Using -type f is probably the best safety habit you can develop with find.

Going Deeper: -size, -mtime, and -perm

This is where find starts to feel like magic. You can find files based on their size. The syntax is, again, a bit quirky. + means “greater than”, - means “less than”, and no prefix means “exactly”. The units are also… creative.

find . -type f -size +10M  # Files larger than 10 Megabytes
find /tmp -type f -size -100k  # Files smaller than 100 Kilobytes
find . -type f -size 1024c  # Files exactly 1024 bytes (specified in 'c' for bytes)

Want to find files you modified in the last 24 hours? Use -mtime (modification time) or -atime (access time). The number argument is in days, and it’s rounded. This is a common gotcha.

find . -type f -mtime -1  # Files modified less than 1 day ago (aka, in the last day)
find . -type f -mtime +7  # Files modified more than 7 days ago

So -mtime +0 is “more than 24 hours ago”. It’s weird, but you get used to it.

Permissions are powerful but the syntax is dense. You can search by exact mode (-perm 644) or by symbolic notation. The most useful trick is finding files with dangerous permissions, like world-writable files or SUID binaries.

find /usr/bin -perm /4000  # Find SUID binaries (the / means "any of these bits are set")
find /home -type f -perm /o=w  # Find files writable by 'others' (world-writable)

Boolean Logic and the -a That Wasn’t There

Here’s the part that trips everyone up. By default, tests are joined with an implicit -and operator. So this:

find . -type f -name "*.js"

Is actually evaluated as:

find . -type f -a -name "*.js"

This becomes critical when you want to use -or (-o). You must group your expressions, usually with escaped parentheses, because the shell will eat them otherwise. It’s ugly, but it works.

# Find either .html or .php files
find . -type f \( -name "*.html" -o -name "*.php" )
# Find files that are either owned by root OR are SUID
find / -type f \( -user root -o -perm /4000 \)

The parentheses group the expressions together. Without them, the logic falls apart. This is the number one reason people’s complex find commands don’t work.

The Action: -print, -delete, and -exec

By default, find just prints the results (-print). But you can tell it to do things. The big one is -exec, which lets you run a command on every file found. Its syntax is the final boss of find quirks: it requires a semicolon to mark the end of the command, and that semicolon must be escaped from the shell, so it’s always \;. The magic string {} is replaced by the current filename.

# A classic: find and delete .DS_Store files
find . -name ".DS_Store" -delete  # Simple, safe action

# For more complex jobs, use -exec
find . -name "*.old" -exec rm -v {} \;  # Verbose remove

The problem with -exec ... {} \; is that it launches a new rm process for every single file. That’s incredibly slow. This is where xargs comes in, which we’ll cover next. However, find has a better built-in option: -exec ... {} +. This appends as many found files as possible to the end of the command, minimizing process launches.

# This is efficient. This is the way.
find . -name "*.tmp" -exec rm -v {} +

Always use + with -exec if the command allows it (which rm, chmod, mv, etc., all do). It’s a massive performance win. And there you have it. find is a cantankerous old wizard—its rules are arcane, but its power is absolute once you learn them.