Learn › Linux Foundations › Search
find - a hands-on Linux lab on a real virtual machine.
Turn find from a search tool into an action tool with -exec. Run a command on every match with the {} placeholder and the ; terminator, see it fire once per file, then batch all matches into one run with +. All on the archive fixture in the practice terminal.
You already know how to *find* files. In the find lesson you pointed find at a folder and filtered the matches by name, by type, by size, and by age, and it printed the path of every hit. On Linux a folder is called a directory, and the two words mean the same thing.
But a printed list of paths is rarely the end goal. You found the log files; now you want to count their lines. You found the scripts; now you want just their names, not the long paths. You found the big files; now you want their exact sizes. This lesson is about the one part of find that closes that gap: -exec, the flag that runs a command on every file find matches. This whole lesson is that single flag, from every angle.
The black boxes in this lesson are a practice terminal: a safe sandbox that only checks the one command each step teaches, so you cannot break anything. It replays the real output here. On the real machine at the end of the module, the mission builds this same archive tree and you run these commands for real.
You are working inside ~/archive, the same tree from the find lesson. The ~ is shorthand for your home directory, /home/student, so ~/archive is the full path /home/student/archive. As a refresher, here are the three shell files in it, matched by find ~/archive -type f -name '*.sh':
find ~/archive -type f -name '*.sh' | sort
prompt: student@linuxcamp:~$ answer: find ~/archive -type f -name '*.sh' | sort output: /home/student/archive/scripts/backup.sh /home/student/archive/scripts/cleanup.sh /home/student/archive/scripts/deploy.sh hint: Same filters as the find lesson, then pipe to sort: find ~/archive -type f -name '*.sh' | sort
Three scripts, printed as full paths. Those filters (-type f, -name '*.sh') are exactly what the find lesson taught, and this lesson does not revisit them. What it adds is what you do next: instead of just printing each path, hand each one to a command. That is -exec.
One habit carried over from the find lesson: find prints matches in filesystem order, which is not alphabetical and varies from machine to machine, so every command here ends in | sort. The | is a pipe: it feeds the output on its left into the command on its right, and sort puts the lines in dependable order.
Here is the one pattern the whole lesson hangs on. You add -exec, then the command you want to run, then two special pieces:
find <where> <filters> -exec COMMAND {} ';'
The {} is a placeholder. find swaps it for the path of each match, one at a time. The ';' is a terminator: it marks where the command to run ends, so find knows what to hand off and what belongs to itself. The quotes around ';' keep the shell from eating the semicolon before find ever sees it.
This pattern is older than almost everything else on your machine. find shipped with Unix at Bell Labs in the 1970s, and -exec, with the same {} placeholder you are about to type, was part of it from those early versions. Engineers have been dropping paths into those braces for fifty years.
Start with the gentlest possible command to run per match: basename, which takes a full path and prints just the final name, dropping every directory in front of it. Run it on each script. Before you do, decide: will each output line be a full path like before, or just a bare name like backup.sh?
find ~/archive/scripts -type f -exec basename {} ';' | sort
prompt: student@linuxcamp:~$ answer: find ~/archive/scripts -type f -exec basename {} ';' | sort output: backup.sh cleanup.sh deploy.sh hint: After the filters add -exec basename {} ';' then pipe to sort: find ~/archive/scripts -type f -exec basename {} ';' | sort
Look at the difference from the first step. There, each match printed as a full path like /home/student/archive/scripts/backup.sh. Here, find handed each of those paths to basename, which stripped everything but the final name, so you got backup.sh, cleanup.sh, deploy.sh. find ran basename three times, once per match, swapping {} for that match's path each time. The ';' told find the command stopped at basename {} and to run it separately for each file. That is the entire mechanism; the rest of the lesson just changes the command you run.
The {} does not have to sit right after the command. find substitutes it wherever it appears, so you can put words after it. That lets you build a real command line around each match, not just feed the path as the last word.
Run wc -l on every .txt file to count its lines. wc -l prints the number of lines in a file, then the filename. The {} sits where the filename goes, and find fills it in for each match. Every report in the archive holds exactly one line, so before you run this, decide: what number should start each output line?
find ~/archive -name '*.txt' -exec wc -l {} ';' | sort
prompt: student@linuxcamp:~$ answer: find ~/archive -name '*.txt' -exec wc -l {} ';' | sort output: 1 /home/student/archive/reports/2024/annual.txt 1 /home/student/archive/reports/2024/q1-summary.txt 1 /home/student/archive/reports/2024/q2-summary.txt 1 /home/student/archive/reports/2025/q1-summary.txt hint: After the name filter add -exec wc -l {} ';' then pipe to sort: find ~/archive -name '*.txt' -exec wc -l {} ';' | sort
find matched the four .txt files, then ran wc -l {} on each, swapping {} for that file's path so the command became, for example, wc -l /home/student/archive/reports/2024/annual.txt. Each report holds a single line, so every count reads 1, followed by the path wc was given. Notice the {} landed in the middle of the line this time, as wc's argument, not at the very end. Wherever you write {}, that is where the match goes.
This is the point that trips people up, so see it directly. -exec does not run your command once with the whole list of files. It runs it once for every single match, separately. When the command you run prints file contents, that means you see one file's contents after another, back to back.
Run cat, which prints a file's contents, on both .csv files. Each holds a header line and some short data rows, and both files start with the very same header, name,value. Before you run this, decide: will that header line appear in the output once, or twice?
find ~/archive -name '*.csv' -exec cat {} ';' | sort
prompt: student@linuxcamp:~$ answer: find ~/archive -name '*.csv' -exec cat {} ';' | sort output: a,1 b,2 c,3 d,4 name,value name,value hint: After the name filter add -exec cat {} ';' then pipe to sort: find ~/archive -name '*.csv' -exec cat {} ';' | sort
Two things to read here. First, the data rows a,1 through d,4 are the contents of the two files, printed one after the other. find ran cat twice, once per match, and each run poured out that file's lines. Second, and this is the giveaway, name,value appears twice. Both data.csv and metrics.csv begin with that same header line, so each cat printed its own copy. If -exec had somehow merged the files into one command, you would see the header once. Seeing it twice is proof the command fired once per match. (The lines are alphabetized here only because you piped through sort; the raw run order follows the filesystem.)
The command after -exec can be anything, including one that produces several columns of detail. ls -lh prints a long listing of a file: its permissions, owner, size, and modification date on one line, with the size in human-readable form like 1.5M instead of a raw byte count.
Filter to the files over one megabyte, then list each one in full. (The -size +1M filter is from the find lesson; here it just supplies the two matches for -exec to act on.) Before you run it, decide which columns you expect to match this page exactly, and which will differ on your machine.
find ~/archive -size +1M -exec ls -lh {} ';' | sort
prompt: student@linuxcamp:~$ answer: find ~/archive -size +1M -exec ls -lh {} ';' | sort output: -rw-r--r-- 1 student student 1.5M <date varies> /home/student/archive/images/hero.jpg -rw-r--r-- 1 student student 2.0M <date varies> /home/student/archive/logs/big.log hint: Keep the size filter, then add -exec ls -lh {} ';' and the usual sort: find ~/archive -size +1M -exec ls -lh {} ';' | sort
In that listing the size column is exact and dependable: 1.5M for the image, 2.0M for the log. The date and time, shown here as <date varies>, depend on when your archive was built, so those values vary on your machine. Read the size; do not memorize the date. The | sort keeps the order dependable here too: both lines open with the same permission and owner text, so sort reaches the size column first and lists 1.5M before 2.0M.
find matched the two large files, then ran ls -lh on each, swapping {} for the path each time, so instead of two bare paths you got a full detail line per file. This is the payoff of -exec: the same flag that ran basename and wc -l and cat just ran ls -lh, because -exec does not care what the command is. Any command you can type at the prompt, you can hand to every match.
The ';' terminator runs your command once per match, which is simple and always correct. But there is a second terminator, +, that changes the batching. End the command with {} + instead, and find collects the matches and passes them all at once to a single run of the command:
find ~/archive -name '*.txt' -exec wc -l {} +
Why care? Two reasons. Speed: launching a command once is far cheaper than launching it a hundred times. And some commands do more when they see the full list. Hand wc -l all four reports in one call and it prints each count plus a grand total line at the end, something the once-per-match ';' runs can never produce, because no single run ever sees more than one file.
The batched form is the one you do not pipe through sort: wc prints its total line last, and sorting would shuffle it into the middle. Because the batch follows filesystem order, the per-file lines can arrive in any order on any machine, so the sandbox here cannot replay one true output for it. You will fire this form on the real machine in the capstone. The rule of thumb to carry: ';' when each match should be handled on its own, which is the safe default, and + when the command can take many files at once and you want them batched.
Scaffolding off. From here to the end, no commands are shown. You have already run every piece you need.
Count the lines of every .txt file under ~/archive, one command run per file, with the output in dependable order.
prompt: student@linuxcamp:~$ answer: find ~/archive -name '*.txt' -exec wc -l {} ';' | sort output: 1 /home/student/archive/reports/2024/annual.txt 1 /home/student/archive/reports/2024/q1-summary.txt 1 /home/student/archive/reports/2024/q2-summary.txt 1 /home/student/archive/reports/2025/q1-summary.txt hint: Filter by name for .txt, then hand each match to the line counter with -exec, the placeholder, and the quoted terminator. Finish with the pipe that makes the order dependable.
Four reports, four separate runs of wc -l, one count each. You rebuilt the whole shape from memory: filter, -exec, the command, {}, ';', | sort. That is the exact move the real machine will ask of you.
Still no command shown. Print the contents of every .csv file under ~/archive, lines in dependable order.
prompt: student@linuxcamp:~$ answer: find ~/archive -name '*.csv' -exec cat {} ';' | sort output: a,1 b,2 c,3 d,4 name,value name,value hint: The command that prints a file's contents goes right after -exec. Same placeholder, same quoted terminator, same sort.
There is the double name,value again, and this time you summoned it yourself. Two matches, two runs of cat, two copies of the header. The proof that -exec fires once per match is now something you can produce on demand.
Last one. Find every file over one megabyte under ~/archive and print a full detail line for each, sizes readable by a human, order dependable.
prompt: student@linuxcamp:~$ answer: find ~/archive -size +1M -exec ls -lh {} ';' | sort output: -rw-r--r-- 1 student student 1.5M <date varies> /home/student/archive/images/hero.jpg -rw-r--r-- 1 student student 2.0M <date varies> /home/student/archive/logs/big.log hint: The size filter from the find lesson supplies the matches. The long-listing command with its human-readable flag goes after -exec.
Two giants, each with a full detail line, and you never saw the command written out. Filter first, act second: that split is the whole discipline of -exec, and you just walked it three times without a script to copy.
You earned this cheat sheet. Every ';' row is a command you ran on the archive, most of them from memory; the + row is the batching form waiting for you on the real machine:
The one idea under all of it: find locates the files, and -exec acts on each one. {} is where each path is dropped in, ';' runs the command once per match, and + runs it once for the whole batch. The filters in front (-name, -type, -size, -mtime) are the find lesson's job; -exec is what you do with whatever they hand you.
When an -exec command surprises you, split the problem in two. First run the find with no -exec and read the plain list of paths, so you know exactly which files will be acted on. Once that list looks right, add the -exec back. Confirming the matches before you act on them is the habit that keeps -exec from ever doing something you did not intend.
The practice terminal has walked you through -exec from every angle: the {} placeholder and the ';' terminator, running basename, wc -l, cat, and ls -lh once per match, and three challenges you solved with no command shown. The + batching form you met on paper; the capstone is where you fire it for real.
The Search module ends with one real Linux machine: the Search capstone mission. There a VM boots just for you on this same archive tree. The mission hands you objectives that use the find filters together with -exec to act on the matches, exactly as you practiced here, and it shows no commands. You read the objective, recall the shape, and type it. That recall is what makes it stick.
Finish the other Search lessons on find's filters and on locate, then go act on a real tree.
Practice find -exec: Act on Every Match in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.