Signal9Signal9 — home
online
all lessons

Finding Files: find, and Acting on What You Find with xargs

Beginner25 min6 steps
What you'll learn
  • 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
  1. 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 to ls. 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. *.log will not reach into logs/2024/ or var/. (** helps only if your shell has globstar turned 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?"

    find is 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, find is a program that recurses and matches by attributes.

    Read a find command 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 / -exec something

    Leave the action off and find simply prints every file that passes the tests. That three-part shape — place, conditions, what to do — is the whole tool.

  2. 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 find command starts the same way: `find` `<where>` `<tests…>`.

    • `<where>` is one or more starting paths. . means "here and below"; /var/log means "that tree". find descends 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, find prints 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?

  3. 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 find is the tests beyond -name:

    • `-type f` matches regular files; `-type d` matches directories. Handy when you want only one or the other.
    • `-iname` is -name but case-insensitive — -iname '*.jpg' also matches Photo.JPG.
    • `-size` matches by size: -size +10M is "bigger than 10 megabytes", -size -1k is "smaller than 1 kilobyte". The `+` means "more than", `-` means "less than".
    • `-mtime` matches by modification time in days: -mtime -7 is "changed in the last 7 days", -mtime +30 is "last changed more than 30 days ago". (-mmin does the same in minutes.)

    When you list several tests, find requires 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 -1
    Every 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?

  4. 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. find gives you a few ways:

    • `-delete` removes the matches. Powerful and unforgiving — always run the same find without -delete first to see exactly what it would remove.
    • `-exec CMD {} \;` runs CMD once per file, with `{}` standing in for each path. The `\;` ends the command (the backslash hides the ; from the shell). Swap \; for `+` and find instead 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 by find itself.

    There's one catch that bites everyone: spaces (and newlines) in filenames. A plain pipe splits on whitespace, so my report.txt looks 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 -0 whenever names might contain spaces — which, on a real system, is always.

    find . -type f -name '*.tmp' -delete
    Delete 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 total
    Run 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.
  5. 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.

    find is 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 NAME prints every indexed path that contains NAME anywhere in it. It's a substring match, not an exact one, so expect extra hits.
    • updatedb rebuilds 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 updatedb is invisible to locate. Anything deleted since then is still listed.

    The rule of thumb: use locate to find where something that has been around for a while lives, and use find for 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_config comes back: the directory sshd_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-report
    The file exists, since touch just made it, but locate prints 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: locate is only as fresh as the last updatedb.
    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. A locate hit 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) makes locate check 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.txt prints nothing. Which command rebuilds locate's index so the file shows up?

  6. 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 find workflow 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 demo
    demo
    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.
Recap
  • 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.
find, at a glance
You writeEffect
find . -name '*.log'print every *.log in the tree below here
find /etc -type fonly regular files under /etc (not directories)
find . -size +10Mfiles larger than 10 MB (+ = more than)
find . -mtime -1files changed in the last day (- = less than)
find . -name '*.tmp' -deleteremove 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 xpipe matches on — space-safe with -print0/-0
updatedb && locate NAMErefresh the index, then search it instantly (substring match)