Read

Module 4 · Finding things

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.

FormRunsSafe with spaces?
find ... -exec CMD {} \;once per fileYes
find ... -exec CMD {} +once per batchYes
find ... \| xargs CMDonce per batchNo
find ... -print0 \| xargs -0 CMDonce per batchYes

Commands in this lesson

CommandWhat 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 CMDTurn lines/words into arguments.
xargs -n 1 / -I {} / -r / -tOne per run / placeholder / skip if empty / show commands.
find ... -print0 | xargs -0 CMDSafe with spaces in names.

Quiz

  1. In `find . -name '*.tmp' -exec rm {} \;`, what does `{}` stand for?

    • The current file's path
    • An empty argument
    • All the files at once
  2. 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
  3. 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
  4. `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`
  5. 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

  1. Delete every `.tmp` file anywhere under /root/lab/l37/build using `find` and its `-exec` action. Nothing else must be removed.

  2. 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`.

  3. 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.