Alright, let’s get our hands dirty. You’ve used find to get a list of files. Great. But now you want to do something with them. Your first thought might be to pipe that list into another command. Don’t. Please, for the love of all that is holy, just don’t. Filenames can contain spaces, newlines, and other characters that will make most command-line tools have a complete meltdown.

This is where -exec and -execdir come in. They are find’s built-in, robust, “I’ve got this” mechanism for handing results to another command. They handle all the weird characters correctly, because find talks directly to the command, not through a shell that might misinterpret things.

The Basic -exec Syntax

The -exec action is followed by the command you want to run, terminated by a literal ; or a +. The magic string {} is a placeholder that find replaces with the current filename.

The semicolon ; is the “do it for every single file, one at a time” operator. Because the semicolon is a special shell character, you have to escape it with a backslash or quote it so that find sees it, not the shell.

# Find all .tmp files and delete them. Yes, rm's -i is a good idea here first.
find . -name "*.tmp" -exec rm -i {} \;

# Find all files modified in the last 24 hours and get their detailed listing
find /var/log -mtime -1 -exec ls -l {} \;

Think of \; as a instruction to find meaning “okay, replace {} with one filename and run the command now.” It’s bulletproof, but it can be slow if you’re dealing with thousands of files because you’re spawning a new process for each one. If you’re deleting 50,000 files, you’re running rm 50,000 times. The overhead is real.

The Efficient -exec + Syntax

This is where you get to feel clever. Using a + instead of a \; at the end tells find “aggregate as many filenames as you can onto the end of this command and run it once.”

# This is vastly more efficient for large operations
find . -name "*.tmp" -exec rm {} +

Here, find builds a list of all the .tmp files and hands them as arguments to a single rm command. It’s the difference between telling a courier “here’s one package, deliver it. now here’s another, deliver it…” and “here’s a truckload, take them all to the same place.” The command line has a maximum length limit (getconf ARG_MAX will show you), so find is smart enough to run the command multiple times if the list gets too long, but it’s still infinitely more efficient than once-per-file.

Crucial Gotcha: The {} placeholder must be the last argument before the + for this to work. The command has to be built to accept a list of files at the end, which rm, chmod, and most other file utilities do.

Why You Should Probably Use -execdir Instead

Here’s the part where I call out the questionable choice. -exec runs the command with the file’s path relative to your starting point. This is fine until you do something like this:

find /some/path -name "*.sh" -exec chmod +x {} \;

If find encounters a file named ../../bin/evil_script.sh, chmod is run with that full path. No problem, right? Now consider this more dangerous scenario:

find /tmp/untrusted_source -name "*.pdf" -exec rm {} \;

What if a malicious user created a file inside /tmp/untrusted_source named -f /home/you/important.txt? The command find would try to run would be rm -f /home/you/important.txt. Yikes. This is a classic “argument injection” vulnerability.

Enter -execdir, the smarter, safer sibling. It runs the command in the directory where the file is located, and it passes only the file’s basename (e.g., evil_script.sh) to the command, not the full path.

# Safer: changes into each file's directory and runs 'rm ./evil_script.sh'
find /tmp/untrusted_source -name "*.pdf" -execdir rm {} \;

The ../ trick is neutered because the command is only ever given a simple filename prefixed with ./. It’s a best practice you should adopt by default, especially in scripts or when dealing with untrusted directory trees. The only downside is that the command must be in your PATH; you can’t use a relative path to a script from your original directory.

The Curious Case of the $0 Problem

Now for a truly absurd edge case. Let’s say you want to use -execdir to run a Bash script you wrote, and that script uses $0 (its own path) to find ancillary files. This will break horribly.

#!/bin/bash
# This is my_script.sh. It uses $0 to find its config.
config_dir="$(dirname "$0")/config"
echo "Loading config from: $config_dir"

find . -name "targetfile" -execdir /path/to/my_script.sh {} \;

When find runs your script with -execdir, it changes into the subdirectory first. Your script’s $0 is still /path/to/my_script.sh, but its current working directory is now the subdirectory. Your $(dirname "$0")/config now resolves to /path/to/config, which probably isn’t where you keep configs for each subdirectory. It’s a mind-bender. For these scenarios, you might be better off with a safer -exec call or redesigning your script to not rely on $0 for this purpose. It’s not find’s fault; it’s just a gnarly interaction.