find -exec and xargs
Doing something with every file find selects: -exec with {} and \;, the batching + form, and xargs for turning any list into arguments, spaces included.
What you will learn
- Run a command on each result with `-exec CMD {} \;` and in batches with `-exec CMD {} +`.
- Turn a list on stdin into arguments with `xargs`, `-n` and `-I`.
- Handle names with spaces using `-print0 | xargs -0`, and preview with `echo`.
Finding files is half the job; usually you then want to delete them, copy them, count their lines or change their permissions. find can run any command on each result through the -exec action, and the separate tool xargs turns any list of names, from find or from anywhere else, into arguments for a command. Between them they cover every "do X to all files that Y" task.
-exec CMD {} \;
After -exec you write a command as you would type it, with {} where the file name should go, and end it with \;. The semicolon tells find where the command stops; the backslash (or quotes, ';') stops the shell from treating it as its own command separator. find build -name '*.tmp' -exec rm {} \; runs rm once per file found. The BusyBox find here has no -delete or -ok, so -exec rm is the way to delete what you find.
~% cd /root/lab/l37
l37% find build -name '*.tmp' -exec echo rm {} \;
rm build/a.tmp
rm build/obj/b.tmp
l37% find build -name '*.tmp' -exec rm {} \;
l37% find srv -name '*.log' -exec cp {} collected/ \;
l37% ls collected
a.log b.log c.log
l37% find srv -name '*.log' -exec wc -l {} +
3 srv/a.log
1 srv/web/b.log
2 srv/db/c.log
6 total
Notice the first line: putting echo in front of the real command is a dry run that shows exactly what would execute. Ending with + instead of \; makes find pass many names to a single run of the command, like wc -l a b c, which is faster and gives commands such as wc the chance to print a total.
xargs
xargs reads words from stdin and appends them as arguments to a command: find build -name '*.o' | xargs rm becomes rm build/x.o build/y.o. It works with any producer, not just find: cat list.txt | xargs touch creates every file named in a list. -n 1 runs the command once per item, -I {} places each item wherever {} appears (xargs -I {} cp {} {}.bak), -r skips running when the input is empty and -t prints each command before running it.
The space problem
xargs splits its input on spaces and newlines, so a file called my notes.bak arrives as two arguments, my and notes.bak, and both fail. The fix is to separate names with the NUL character, which can never appear in a file name: find ... -print0 | xargs -0 rm. -exec never has this problem, because find hands each name over intact.
| Form | Runs | Safe with spaces? |
|---|---|---|
find ... -exec CMD {} \; | once per file | Yes |
find ... -exec CMD {} + | once per batch | Yes |
find ... \| xargs CMD | once per batch | No |
find ... -print0 \| xargs -0 CMD | once per batch | Yes |
Commands in this lesson
| Command | What it does |
|---|---|
find ... -exec CMD {} \; | Run CMD once for each file found. |
find ... -exec CMD {} + | Run CMD with many files at once. |
find ... -exec echo CMD {} \; | Dry run: print instead of execute. |
LIST | xargs CMD | Turn lines/words into arguments. |
xargs -n 1 / -I {} / -r / -t | One per run / placeholder / skip if empty / show commands. |
find ... -print0 | xargs -0 CMD | Safe with spaces in names. |
Quiz
In `find . -name '*.tmp' -exec rm {} \;`, what does `{}` stand for?
- The current file's path
- An empty argument
- All the files at once
Why is the semicolon written `\;`?
- So the shell passes it to `find` instead of treating it as a command separator
- Because `find` requires a backslash before every argument
- It is a typo that happens to work
What changes if you end `-exec wc -l {}` with `+` instead of `\;`?
- Nothing
- `wc` runs once with many files, so it can print a total
- Only the first file is counted
`find . -name '*.bak' | xargs rm` fails on `my notes.bak`. What is the robust fix?
- `find . -name '*.bak' -print0 | xargs -0 rm`
- `find . -name '*.bak' | xargs -n 1 rm`
- `find . -name '*.bak' | xargs -f rm`
Which command shows what a `find ... -exec rm` would delete, without deleting anything?
- `find build -name '*.tmp' -exec echo rm {} \;`
- `find build -name '*.tmp' -delete`
- `find build -name '*.tmp' -exec rm -f {} \;`
Practice
Delete every `.tmp` file anywhere under /root/lab/l37/build using `find` and its `-exec` action. Nothing else must be removed.
Log files are scattered at several depths under /root/lab/l37/srv. Copy every `.log` file into the single directory /root/lab/l37/collected, using `find -exec`.
Remove every `.bak` file under /root/lab/l37/docs. Some names contain spaces, so pipe `find` into `xargs` in the way that is safe for such names.
Open this lesson in the app to do the tasks in a real Linux machine and have them checked.