Text Processing for Network Engineers Part 9: find, xargs, and Safe Bulk Operations Across a Config Tree
Every tool so far in this series operates on a file, or a file’s content, once you’ve already decided which file. find decides which files. xargs takes whatever a previous command printed and turns it into arguments for the next one. Together they turn “run this check against every relevant config backup” into a single pipeline instead of a hand-typed loop.
find: selecting files by more than just name
find configs/ -name '*.cfg' # by filename pattern
find configs/ -type f # files only, not directories
find configs/ -mtime -1 # modified in the last 24 hours
find configs/ -size +1M # bigger than 1 MB (a config that's grown suspiciously large)
find configs/ -newer configs/last-good-baseline.cfg # changed since a known-good reference
-mtime -1 is the one that turns a static backup directory into an audit trigger: after a nightly config-pull job, “which devices actually changed since yesterday” is one find call, not a manual comparison against yesterday’s list.
Combine conditions with -a (and, usually implicit) or -o (or), and negate with !:
find configs/ -name '*.cfg' -mtime -1 -size +0
find configs/ ! -name '*baseline*'
-exec: running a command per file, without leaving find
find configs/ -name '*.cfg' -exec grep -l '10.50.0.15' {} \;
{} is replaced with each matched filename, \; ends the command for that one file (run once per file, which is correct but slow for a large tree, since it spawns a new grep process for every single file). The + terminator instead batches as many filenames as will fit onto one command line, running far fewer processes for the same result:
find configs/ -name '*.cfg' -exec grep -l '10.50.0.15' {} +
That single change, \; to +, is the most common performance fix for a slow find -exec pipeline, and it costs nothing in correctness for a command like grep -l that already accepts multiple filenames.
xargs: the general-purpose version of the same idea
xargs does the same job as -exec +, but works with any preceding command’s output, not just find’s:
find configs/ -name '*.cfg' | xargs grep -l '10.50.0.15'
That’s functionally identical to the -exec + version above, but the pattern generalizes: anything that prints a list of filenames, one per line, whether that’s find, a grep -l from Part 2, or a custom script, can feed straight into xargs.
The null-byte habit: -print0 and -0
Filenames with spaces (a device name like branch office 12.cfg, or a path with a space anywhere in it) will silently break the plain pipe above, because xargs by default splits its input on whitespace, which cannot distinguish “one filename with a space in it” from “two separate filenames.” The fix is a matched pair of flags:
find configs/ -name '*.cfg' -print0 | xargs -0 grep -l '10.50.0.15'
-print0 makes find separate output with a null byte instead of a newline, -0 on the xargs side tells it to split on that same null byte. Null bytes can’t legally appear inside a filename, so this is the one delimiter guaranteed to never collide with the data itself. Adopt -print0/-0 as a standing habit for any find-into-xargs pipeline that touches a directory you don’t fully control the naming convention for, rather than something to remember only after a script breaks on a device named with a space in it.
-I{} for placeholder substitution, and -P for parallelism
Where the command needs the filename somewhere other than at the end of the argument list, -I names a placeholder:
find configs/ -name '*.cfg' -print0 | xargs -0 -I{} cp {} /backup/archive/{}
And -P runs multiple invocations concurrently, which matters once the per-file work is slow enough that serial processing is the bottleneck, not the tool itself:
find configs/ -name '*.cfg' -print0 | xargs -0 -P4 -I{} some-slow-audit-script.sh {}
-P4 runs up to four instances of the script at once. Be deliberate with the number: too high against a directory of device configs that each trigger, say, an SSH connectivity check to the live device, will just open four times as many simultaneous connections and potentially trip a rate limit or connection cap on the far end, not actually finish four times faster.
Always dry-run before -exec with anything destructive
The single most important habit in this whole part: before running find ... -exec rm {} \;, or any -exec with a command that changes or deletes something, run the exact same find expression with no -exec at all, and read the list of files it would have acted on:
find configs/ -name '*.bak' -mtime +30 # look first
find configs/ -name '*.bak' -mtime +30 -exec rm {} \; # then, once the list above looks right
find’s selection logic is exactly as good as the conditions you wrote, no better, and a slightly-off -mtime or -name pattern combined with -exec rm has no undo. Reviewing the plain file list first costs a few seconds and catches the entire class of mistake this pattern is prone to.
Where this connects to the rest of the site
A common shape for a fleet-wide audit script: find selects the relevant config backups, a loop or xargs runs a check against each, and the per-device result gets emitted as a small JSON object for downstream tooling. If that’s the direction an audit script takes, parsing and filtering the resulting JSON is exactly what the existing jq for network engineers post on this site already covers in depth, so it isn’t repeated here. Part 10 covers the failure mode that quietly undermines every pipeline in this series if it goes unnoticed: character encoding and line-ending mismatches between a config exported from Windows and a script expecting plain Unix text.