Finding Files: find, and Acting on What You Find with xargs
- Explain why a glob (*.log) only matches names in one directory, while find walks a whole tree and matches by attributes
- Build a find command as a path plus tests: -name/-iname, -type, -size, -mtime — and combine them
- Quote find's -name pattern so the shell doesn't expand it before find ever sees it
- Act on the matches: -delete, -exec … {} \; vs +, and find -print0 | xargs -0 — and know when spaces in names force -print0/-0
- Use locate for instant name lookups, and explain its staleness: updatedb rebuilds the index, new files are invisible until then, and deleted files linger
Why find exists: a glob stops at one directory
See the gap find fills — searching a whole tree, by attributes, not just names in the current folder.
You already have one way to name files by pattern: a glob. When you type
ls *.log, the shell looks in the current directory, finds the names that match*.log, and hands them tols. That's expansion — it happens before the command runs (straight from the Quoting & Globbing lesson).But a glob has two hard limits:
- It only looks in one directory at a time.
*.logwill not reach intologs/2024/orvar/. (**helps only if your shell hasglobstarturned on.) - It only matches on the name. It can't ask "which files are bigger than 10 MB?" or "which changed in the last day?" or "which are directories, not files?"
findis the tool for exactly those questions. It walks an entire directory tree — the folder you point it at and everything beneath it — and keeps the files that pass the tests you give it. Name is just one possible test; size, type, and modification time are others. Where a glob is the shell matching names in one place,findis a program that recurses and matches by attributes.Read a
findcommand as three parts, left to right:- where to look — a starting path like
.(here and below) or/var/log - tests — conditions each file must pass:
-name,-type,-size,-mtime - action — what to do with the survivors: print them (the default), or
-delete/-execsomething
Leave the action off and
findsimply prints every file that passes the tests. That three-part shape — place, conditions, what to do — is the whole tool.- It only looks in one directory at a time.
find's shape: a path, then tests
Read and write the basic form — find WHERE TESTS — and know that printing is the default action.
Every
findcommand starts the same way: `find` `<where>` `<tests…>`.- `<where>` is one or more starting paths.
.means "here and below";/var/logmeans "that tree".finddescends into every subdirectory automatically. - `<tests>` are the conditions each file must pass. The most common is `-name`, which matches the file's name against a glob-style pattern.
If you give no action,
findprints every matching path — that's the default. So the simplest useful command is just a path and a-name.find . -name '*.log'./app.log ./logs/2024/access.log ./logs/2024/error.log
Start at '.' (here) and print every file named *.log anywhere below — including deep in logs/2024/, which a plain glob could never reach. find did the walking and the matching itself. Check yourself
In
find /etc -name '*.conf', which part is the starting place and which is the test?- `<where>` is one or more starting paths.
Testing by more than name: type, size, time
Filter by what a file IS, not just what it's called — and combine tests.
The payoff of
findis the tests beyond-name:- `-type f` matches regular files; `-type d` matches directories. Handy when you want only one or the other.
- `-iname` is
-namebut case-insensitive —-iname '*.jpg'also matchesPhoto.JPG. - `-size` matches by size:
-size +10Mis "bigger than 10 megabytes",-size -1kis "smaller than 1 kilobyte". The `+` means "more than", `-` means "less than". - `-mtime` matches by modification time in days:
-mtime -7is "changed in the last 7 days",-mtime +30is "last changed more than 30 days ago". (-mmindoes the same in minutes.)
When you list several tests,
findrequires a file to pass all of them — they're joined with an implicit AND. So the tests read left to right as "a file, that is a regular file, and is named*.log, and is bigger than 1M".find /var/log -type f -name '*.log' -size +1M/var/log/syslog /var/log/journal/system.log
Three tests, all required: a regular file (-type f), named *.log, and larger than 1 megabyte (-size +1M). find keeps only the files that pass every test — the classic 'which big logs are eating my disk?' query. Example — not runnable on your lab box
find . -type f -mtime -1Every regular file below the current directory changed in the last 24 hours (-mtime -1) — the fastest way to answer 'what did I just touch?' Your terminal supplies the actual list, since it depends on your files. Check yourself
You want directories (not files) below the current folder. Which test selects them?
Doing something with the matches: -delete, -exec, and xargs
Turn a list of found files into an action — safely, even when names contain spaces.
Finding files is half the job; usually you want to do something with them.
findgives you a few ways:- `-delete` removes the matches. Powerful and unforgiving — always run the same
findwithout-deletefirst to see exactly what it would remove. - `-exec CMD {} \;` runs
CMDonce per file, with `{}` standing in for each path. The `\;` ends the command (the backslash hides the;from the shell). Swap\;for `+` andfindinstead packs many files into one invocation — far faster for big lists. - Pipe into `xargs` —
find … | xargs CMD— which reads the found paths and builds command lines from them. This is the classic pairing when the tool you want to run doesn't have to be launched byfinditself.
There's one catch that bites everyone: spaces (and newlines) in filenames. A plain pipe splits on whitespace, so
my report.txtlooks like two files. The fix is a matched pair: `find … -print0` separates paths with a zero byte instead of a newline, and `xargs -0` reads that format. Use-print0 | xargs -0whenever names might contain spaces — which, on a real system, is always.find . -type f -name '*.tmp' -deleteDelete every .tmp regular file in the tree. Because -delete is irreversible, the safe habit is to run `find . -type f -name '.tmp'` first (no -delete) and read the list, THEN add -delete once you trust it. Nothing prints on success. find . -name '*.log' -exec wc -l {} +12 ./app.log 840 ./logs/2024/access.log 852 totalRun wc -l (count lines) on the found logs. The {} is where each path goes; the trailing + packs them all into a single wc call, so you get a 'total' line. Had we used \; instead of +, wc would run once per file with no total. find . -type f -name '*.conf' -print0 | xargs -0 grep -l 'timeout'Find every .conf file, then hand the list to grep to print only those that CONTAIN the word timeout (grep -l = list matching filenames). The -print0/-0 pair keeps it correct even if a path has a space in it. Your terminal supplies the matching files. - `-delete` removes the matches. Powerful and unforgiving — always run the same
locate: instant answers from an index (and why they go stale)
Use locate for a fast name lookup, and know when its index is out of date and how to refresh it with updatedb.
findis always right because it walks the real disk, every time. On a big filesystem that walk is slow. `locate` takes the opposite trade: it never touches the disk at search time. It reads a prebuilt index of every path on the system (on this box that's/var/lib/plocate/plocate.db) and answers almost instantly.locate NAMEprints every indexed path that contains NAME anywhere in it. It's a substring match, not an exact one, so expect extra hits.updatedbrebuilds the index by walking the filesystem once. Distributions run it on a schedule, usually daily (on this box it's/etc/cron.daily/plocate).- Anything created after the last
updatedbis invisible tolocate. Anything deleted since then is still listed.
The rule of thumb: use
locateto find where something that has been around for a while lives, and usefindfor anything new, or when the answer has to be current.updatedb && locate sshd_config/etc/ssh/sshd_config /etc/ssh/sshd_config.d /usr/share/man/man5/sshd_config.5.gz /usr/share/openssh/sshd_config /usr/share/openssh/sshd_config.md5sum /var/lib/ucf/cache/:etc:ssh:sshd_config
- updatedb
- rebuild locate's index by walking the filesystem once
Refresh the index, then search it. Every path that merely contains sshd_configcomes back: the directorysshd_config.d, the man page, a checksum file. That's the substring match. You still read the list and pick the one you meant (/etc/ssh/sshd_config).mkdir -p ~/stash && touch ~/stash/fresh-report.txt && locate fresh-reportThe file exists, since touchjust made it, butlocateprints nothing. It isn't broken. It is answering from an index that was built before this file existed.find ~/stash -name 'fresh-report.txt'would find it right away.updatedb && locate fresh-report/root/stash/fresh-report.txt
Rebuild the index and the new file shows up. That's the whole cycle: locateis only as fresh as the lastupdatedb.rm ~/stash/fresh-report.txt && locate fresh-report/root/stash/fresh-report.txt
Staleness works in the other direction too. The file is gone, but the index still lists it until the next updatedb. Alocatehit is a lead, not proof that the file exists.locate -e fresh-report- -e
- only print paths that still exist on disk right now
-e(--existing) makeslocatecheck each hit on disk and drop the ones that no longer exist, so the ghost from the previous step disappears.Check yourself
You created report.txt a minute ago, and
locate report.txtprints nothing. Which command rebuilds locate's index so the file shows up?Now do it — search a tree, then act on the results
Chain the pieces: build a tree, find by attribute, and pipe the matches into another tool.
One continuous experiment. You'll make a small tree, find files by name and by time, then feed the matches to another command — the everyday
findworkflow end to end.mkdir -p demo/logs/old && printf 'a\nb\n' > demo/app.log && printf 'x\n' > demo/logs/access.log && printf 'old\n' > demo/logs/old/archive.log && find demo -type f -name '*.log'demo/app.log demo/logs/access.log demo/logs/old/archive.log
Build a three-level tree of .log files, then find them all with one command. Notice find reached demo/logs/old/archive.log three directories deep — a glob like demo/*.log would have found only the top one. find demo -type f -name '*.log' -exec wc -l {} +2 demo/app.log 1 demo/logs/access.log 1 demo/logs/old/archive.log 4 total
Count the lines in every log at once. The + form packs all three paths into a single wc call, which is why you get a 'total' line summing them. find demo -type f -name '*.log' -print0 | xargs -0 grep -H 'a'demo/app.log:a
Pipe the found logs into grep to show lines containing 'a', with -H so each hit is prefixed by its filename. The -print0/-0 pair is the space-safe way to pass a file list from find into another program. find demo -type d; echo '--- cleaning up ---'; rm -r demodemo demo/logs demo/logs/old --- cleaning up ---
-type d lists just the directories of the tree (the files are filtered out), then we remove the demo folder. You've now searched a tree by name, by type, and by content-through-a-pipe — the whole find toolkit in one sitting.
- A glob (*.log) is the shell matching names in ONE directory; find walks an entire tree and matches by attributes — name, type, size, and modification time.
- The shape is find <where> <tests>: a starting path plus conditions. List several tests and a file must pass ALL of them (implicit AND). With no action, find prints the matches.
- Quote the -name pattern ('*.log') so the shell doesn't expand it first; for -size and -mtime, + means 'more than' and - means 'less than'.
- Act on matches with -delete, -exec CMD {} \; (once per file) or -exec CMD {} + (batched), or pipe into xargs. Use find -print0 | xargs -0 when names might contain spaces, and always dry-run before -delete.
- locate reads a prebuilt index instead of the disk, so it's instant but only as fresh as the last updatedb. New files are missing, deleted ones linger (locate -e filters them out), and PRUNEPATHS dirs like /tmp are never indexed.
| You write | Effect |
|---|---|
| find . -name '*.log' | print every *.log in the tree below here |
| find /etc -type f | only regular files under /etc (not directories) |
| find . -size +10M | files larger than 10 MB (+ = more than) |
| find . -mtime -1 | files changed in the last day (- = less than) |
| find . -name '*.tmp' -delete | remove the matches (dry-run first!) |
| find . -name '*.c' -exec wc -l {} + | run one wc on all found files ({} = each path) |
| find . -print0 | xargs -0 grep x | pipe matches on — space-safe with -print0/-0 |
| updatedb && locate NAME | refresh the index, then search it instantly (substring match) |
