When you manage servers or development environments with deep trees, you often
need to quickly find a huge log, a lost script or a specific directory.
find walks the file system in real time and lets you select files
(name, type, size, date, permissions) then act on them (print, audit, run a
command, clean up).
What you will learn
- Target files by name, type, size, date, permissions
- Combine criteria (AND/OR/NOT, parentheses)
- Apply an action safely (
-print→ then-exec/-delete)
The find command in the Linux ecosystem
find belongs to the file search and processing commands of Linux. Each
tool has its speciality, and find is the one that searches by
attributes:
| Command | Speciality | When to use it |
|---|---|---|
find | Search for files by attributes | Find by name, size, date, permissions |
locate | Ultra-fast search by name | When speed matters most (indexed database) |
grep | Search in the content | Find a string in files |
which / whereis | Locate executables | Find where a program is installed |
xargs | Run commands in bulk | Pass the results of find to a command |
awk | Process tabular data | Extract/transform columns |
sed | Edit files as a stream | Replace text, delete lines |
cut | Extract columns | Split by delimiter or position |
Frequent combinations:
# find + grep: search a pattern in some filesfind /var/log -name '*.log' -exec grep -l "error" {} +
# find + xargs: bulk processing (robust with spaces)find . -name '*.tmp' -print0 | xargs -0 rm -f
# locate + grep: filter the results of locatelocate '*.conf' | grep nginxUnderstand the find command in 2 minutes
The mental model below sums up how find works: where to search, what
to keep, what to do.
🧠 Mental model: How find works
find = Where to search → What to keep → What to do
Key points
- find walks a directory and all its subdirectories
- It keeps the files that match your criteria (name, type, date, size)
- Several criteria = AND (both must match)
- If you use OR (-o), think of the parentheses
- You can run a command on the files found (-exec)
- You can delete files (-delete) BUT always check first with -print
Golden rules
Essential vocabulary
- -name '*.log'
- Files whose name matches the pattern
- -type f / -type d
- File (f) or directory (d)
- -mtime +7
- Modified more than 7 days ago
- -size +100M
- Size above 100 MiB
- -maxdepth 2
- Do not go down more than 2 levels
- -print / -delete
- Print the results / Delete
📚 Going further: 12 advanced options
- -iname
- Like -name but case-insensitive
- -path / -ipath
- Pattern on the full path
- -mmin -60
- Modified less than 60 minutes ago
- -perm 644
- Exact permissions
- -user / -group
- By owner or group
- -prune
- Exclude a subtree
- -xdev
- Do not cross mount points
- -exec cmd {} \;
- Run for each file
- -exec cmd {} +
- Run in batches (faster)
- -ok cmd {} \;
- Like -exec with confirmation
- -printf FORMAT
- Custom formatted output
- -quit
- Stop at the first result
Unlike locate (pre-computed index), find does not depend on a database: the
result reflects the current state of the disk.
Minimal syntax
The order of the three groups is not decorative: find evaluates the
arguments from left to right, and a filter placed after an action no longer
filters anything. It is the number one cause of accidental deletions, such as
find /tmp -delete -name '*.tmp', which empties the whole of /tmp instead of
targeting the .tmp files. Remember the rule: path, then filters, then
action, always in this order.
find [CHEMIN...] [FILTRES...] [ACTIONS...]The 3 parts of a find command
The find command always follows the same logic in three steps: a
starting point, filters, an action:
| Step | Question | Examples | Required? |
|---|---|---|---|
| Where to search | Which directory to start from? | /var/log, ., /home | Implicit (default: .) |
| Which results to keep | Which files match my criteria? | -name '*.log', -type f, -size +100M | No (default: everything) |
| What to do | Which action to run on the results? | -print, -delete, -exec rm {} + | No (default: -print) |
Where to search (optional): it is the starting point. find walks this
directory and all its subdirectories recursively. Without a path, GNU find
uses . (the current directory), but the BSD and macOS implementations require
an explicit path: get into the habit of always writing it.
Which results to keep (optional): these are the filters. Without a
filter, find returns everything. Several criteria can be combined: by default
they are linked by AND (all of them must match).
What to do (optional): it is the final action. Without an explicit
action, find simply prints the paths (-print). For destructive actions
(-delete, -exec rm), always test with -print first.
Filters: target your files precisely
Filters narrow the results down to the files you care about. Here are the most used ones, starting with the name tests and their case sensitivity:
| Filter | What it tests | Example |
|---|---|---|
-name 'motif' | File name (case-sensitive) | -name '*.log' |
-iname 'motif' | File name (case-insensitive) | -iname '*.LOG' |
Always put the pattern between quotes: -name '*.log'. Otherwise the
shell interprets * before find and you get unexpected results.
| Filter | What it tests | Example |
|---|---|---|
-type f | Files only | -type f |
-type d | Directories only | -type d |
-size +100M | Size above 100 MB | -size +100M |
-size -1k | Size below 1 KB | -size -1k |
-mtime -7 | Modified less than 7 days ago | -mtime -7 |
-mtime +30 | Modified more than 30 days ago | -mtime +30 |
-user alice | Belongs to the user alice | -user alice |
-perm 644 | Permissions exactly 644 | -perm 644 |
-perm -644 | At least these bits (read/write) | -perm -644 |
-perm /222 | At least one write bit | -perm /222 |
Note on sizes, the rounding trap: -size always rounds up to the unit. A
500-byte file therefore counts as 1k, and find . -size -1k only returns
0-byte files, never small files. Same effect with M: -size -1M returns
almost nothing. To reason in real size, use the c unit (bytes):
find . -type f -size -1000c. The available units are b (512-byte blocks, the
default), c (bytes), w (2-byte words), k (kibibytes), M (mebibytes) and
G (gibibytes): they are powers of 1024, not decimal megabytes.
Combine filters, AND / OR / NOT:
By default, filters are combined with AND (all of them must match), without writing any operator:
# .log files AND larger than 10 MB (both conditions)find /var/log -type f -name '*.log' -size +10MTip: add -type f to match files only (it avoids the noise of
directories).
To use OR, add -o and parentheses (escaped), so that the
grouping is explicit:
# .log OR .txt filesfind /var/log -type f \( -name '*.log' -o -name '*.txt' \)Why \( and \)? The shell interprets bare parentheses. You
escape them (\( \)) or put them between quotes ('(' ')').
To exclude by name, use ! or -not, the negation:
# Every file EXCEPT those starting with .gitfind . -type f ! -name '.git*'Watch out: this does NOT exclude the .git/ directory from the walk. find
still goes down into it and prints its content, since .git/config is named
config and does not start with .git: only the directory itself disappears
from the list, and the walk still costs time on large repositories. To really
skip a directory, use -prune:
# Exclude the .git directory from the search (correct)find . -name .git -prune -o -type f -printActions: what to do with the files found?
Once the files are filtered, you can act on them. The first two actions are
risk-free, the next three modify the disk. The important distinction is
between -delete and -exec rm: -delete is internal to find, it launches no
process and correctly handles exotic names, but it automatically turns on
-depth, which makes it incompatible with -prune. If your command
contains -prune, go through -exec rm {} + or an xargs pipeline.
| Action | What it does | Example |
|---|---|---|
-print | Prints the path (default) | -print |
-ls | Prints in ls -l format | -ls |
-delete | Deletes the file | -delete |
-exec cmd {} \; | Runs cmd on each file | -exec chmod 644 {} \; |
-exec cmd {} + | Runs cmd with every file | -exec rm {} + |
Golden rule: always preview before destroying
# DANGEROUS: deletes directly, without any checkfind /tmp -name '*.tmp' -delete
# SAFE: preview firstfind /tmp -name '*.tmp' -print# Check the list, then only:find /tmp -name '*.tmp' -deleteRobust alternative (handles spaces and special characters in names):
find . -type f -name '*.tmp' -print0 | xargs -0 rm -f-exec {} \; vs -exec {} +: the difference matters for performance,
because it sets the number of processes:
\;: launches one command per file (slow with many files)+: groups the files and launches a single command (fast, recommended)
# Slow: 1000 files = 1000 calls to chmodfind . -type f -exec chmod 644 {} \;
# Fast: 1000 files = a few calls to chmodfind . -type f -exec chmod 644 {} +Control the walk: -depth and -prune
By default, find walks the tree from top to bottom. Two options change
this behaviour, -depth and -prune, plus the depth limits.
-depth processes the content of a directory before the directory
itself:
# Without -depth: /tmp is listed BEFORE its contentfind /tmp -type d
# With -depth: /tmp is listed AFTER its contentfind /tmp -depth -type dIt is automatic with -delete, to avoid deleting a directory before its
content. A direct and often ignored consequence: since -depth neutralises
-prune, a command that mixes -prune and -delete cannot protect the
directory you thought you were excluding. GNU find 4.10 actually refuses
it, with return code 1 and without deleting anything:
find: The -delete action automatically turns on -depth, but -prune does nothing when -depth is in effect. If you want to carry on anyway, just explicitly use the -depth option.Adding -depth explicitly silences this refusal, and the exclusion is then
ignored.
-prune stops the descent into a directory (useful to exclude
directories):
# Exclude the node_modules directory from the searchfind . -name 'node_modules' -prune -o -name '*.js' -print
# Exclude several directoriesfind . \( -name 'node_modules' -o -name '.git' \) -prune -o -type f -printHow to read -prune: "If you find this directory, do not go into it
(-prune), otherwise (-o) apply the rest."
| Option | Effect | Use case |
|---|---|---|
-depth | Processes children before parents | Recursive deletion |
-prune | Does not enter the directory | Exclude node_modules, .git |
-maxdepth N | Limits the depth | find . -maxdepth 1 (current level) |
-mindepth N | Ignores the first N levels | find . -mindepth 2 (subdirectories) |
Practical examples:
# List only the immediate content (no recursion)find /var/log -maxdepth 1 -type f -name '*.log'
# Search from the 2nd level on (skip the root directory)find . -mindepth 2 -type f -name '*.conf'
# Combine both: search between level 2 and 4find /home -mindepth 2 -maxdepth 4 -type d -name '.ssh'
# Delete empty directories (depth needed to process children first)find /tmp -depth -type d -empty -delete
# List the files of the current level only (equivalent of ls)find . -maxdepth 1 -type fTypical errors (and fixes)
These four errors have two origins. The first comes from the shell, which
expands * before find receives it: rerun the command prefixed with
echo to see the arguments actually passed, and if echo find -name *.log
prints a list of files instead of the pattern, you have found the culprit. The
other three come from the grammar of find itself: operator
precedence, the difference between filtering the output and preventing the
descent, and the missing type filter.
| Error | Cause | Fix |
|---|---|---|
find -name *.log finds nothing | The shell interpreted * before find | Put it between quotes: -name '*.log' |
-o does not work as expected | Operator precedence | Add parentheses: \( ... -o ... \) |
"I excluded .git but find searches it" | ! -name does not stop the descent | Use -prune: -name .git -prune -o ... |
| Results include directories | No -type filter | Add -type f for files only |
Common search patterns
Now that you understand the logic of find, here are ten ready-to-use
patterns. Each one gives its general formula and a concrete example.
Search by exact name
-name only compares the last component of the path, never the full path.
Searching for -name "syslog" therefore returns /var/log/syslog as well as
/var/log/journal/syslog, but -name "log/syslog" returns nothing at all. To
reason on the whole path, -path is the one to use.
find <chemin> -name "<nom>" # formulafind /var/log -name "syslog" # examplechemin: starting point of the search (default:.)nom: exact name or glob pattern
Search by pattern (glob)
The glob of -name differs from the shell's on one surprising point: the * of
find crosses dots, so -name "*.log" also returns hidden files such
as .hidden.log. On the other hand, it never crosses the / separator,
which guarantees that the pattern stays confined to the file name.
find <chemin> -name "*.ext" # formulafind . -name "*.log" # example*: wildcard, any characters- always between quotes, otherwise the shell interprets the
*
Filter by type
Adding -type f is not cosmetic: without it, an -exec or a -delete also
applies to directories and symbolic links carrying the same pattern. The
case of -type l deserves special attention: find tests the link itself,
not its target, unless you add the -L option at the start of the command.
find <chemin> -type <f|d|l> # formulafind /etc -type d -name "*.d" # examplef: regular file,d: directory,l: symbolic link
Filter by size
This is the filter where the rounding up to the unit described above does
the most damage. For a disk space audit, -size +100M works perfectly since
you are looking for large files. To track small files, you must switch to bytes
with the c suffix, otherwise you will only get empty files.
find <chemin> -type f -size <+|->N<c|k|M|G> # formulafind /var/log -type f -size +100M # example+/-: larger / smaller than- units:
cbytes,kKiB,MMiB,GGiB (powers of 1024)
Filter by modification date
-mtime reasons in 24-hour slices, counted from the moment the command
starts, not from midnight. "Modified more than 7 days ago" therefore means "more
than 7 times 24 hours ago", which does not match the calendar week. When you
clean up logs with a dated retention policy, add the -daystart option to
align the calculation on the start of the current day. For short durations,
-mmin works in minutes.
find <chemin> -mtime <+|->N # formulafind /tmp -type f -mtime +30 # example+N: modified more than N days ago-N: modified less than N days ago
Run a command (-exec)
-exec launches the program directly, without going through a shell. It
protects against hostile file names, but also forbids pipes,
redirections and wildcards in the command run:
-exec grep motif {} > sortie.txt \; redirects the output of find, not that
of grep. For real shell, you must spell out -exec sh -c '...' _ {} \;. Careful variant: -ok
replaces -exec and asks for a confirmation before each run.
find <chemin> <tests> -exec <cmd> {} \; # formulafind . -name "*.bak" -exec rm {} \; # example{}: replaced with the file name\;: ends the command (escaped)
Run in batches (-exec +)
The performance gain comes from the number of processes created: \;
launches one per file, + launches one per pack of arguments fitting within the
system limit. Two caveats to know before generalising. First, {} must be
the last argument of the command, which rules out invocations such as
cp {} /destination. Next, the output changes: wc -l in + mode adds a
total line, absent in \; mode.
find <chemin> <tests> -exec <cmd> {} + # formulafind . -name "*.txt" -exec wc -l {} + # example+: aggregates the files (likexargs)
Safe pipeline with xargs -0
The danger of a find | xargs pipeline without -print0 is not theoretical:
xargs splits on spaces and on newlines, and a Unix file name can legally
contain both. A file named rapport annuel.pdf becomes two arguments,
rapport and annuel.pdf, and the command applies to targets that do not exist
or, worse, that exist elsewhere. The null character is the only byte
forbidden in a file name, hence the only reliable separator.
find <chemin> <tests> -print0 | xargs -0 <cmd> # formulafind . -name "*.pdf" -print0 | xargs -0 mv -t ~/backup/ # example-print0: separates the results with a null characterxargs -0: reads that same null separator
Safe deletion
The two-step check has a limit worth knowing: between the -print and the
-delete, the tree may have changed. On an active server, prefer to freeze
the list in a file, reread it, then delete from that list rather than rerunning
find. Also check that the starting path is really the one you think, a
typo on /var instead of /var/tmp cannot be undone.
find <chemin> <tests> -print # step 1: previewfind <chemin> <tests> -delete # step 2: delete- Never
-deletewithout running-printfirst.
Exclude a directory (-prune)
The construction reads like a switch: condition -prune -o tests -print.
When the condition is true, -prune returns true, the -o short-circuits the
rest and find does not enter the directory. The final -print is
mandatory here: without it, the implicit -print action would also apply to
the -prune branch and the excluded directories would show up anyway. It is the
most frequent error with this syntax.
find <chemin> -path "*/<dossier>" -prune -o <tests> -print # formulafind . -path "./node_modules" -prune -o -name "*.js" -print # example-prune: stops the descent into the targeted directory-o: applies the tests to the rest of the tree
Pitfalls to avoid
Six errors come back constantly with find. Some are destructive,
-delete does not forgive, and neither does a careless pattern.
Case-sensitive search
-name distinguishes uppercase from lowercase.
find . -name "Report.txt" # WRONG: does not find "report.txt"Symptom: the search returns nothing although the file exists. Cause:
-name distinguishes uppercase from lowercase.
find . -iname "report.txt" # RIGHT: -iname ignores caseFile names with spaces
A space in a name breaks a pipeline to xargs.
find . -name "*.txt" | xargs rm # WRONG: breaks on "mon fichier.txt"Symptom: No such file error or the wrong files deleted. Cause: xargs
splits on spaces.
find . -name "*.txt" -print0 | xargs -0 rm # RIGHT: null separatorOR without parentheses
The implicit AND binds tighter than -o, which changes the meaning of
the expression.
find . -name "*.log" -o -name "*.txt" -mtime +7 # WRONG: -mtime only applies to the .txt filesSymptom: -mtime only filters part of the results. Cause: operator
precedence groups -mtime with the second -name.
find . \( -name "*.log" -o -name "*.txt" \) -mtime +7 # RIGHT: escaped parentheses-delete without checking
-delete acts immediately, without any preview.
find /var -name "*.tmp" -delete # WRONG: run directly, without any checkSymptom: important files deleted by mistake. Cause: an overly broad pattern applied without a preview.
find /var -name "*.tmp" -print # RIGHT: check first, then replace -print with -deletemtime / ctime / atime confusion
Three timestamps coexist, and none of them is the creation date that
find could use.
find . -mtime -1 # WRONG: does not reflect the "creation date"Symptom: copied files show up as old. Cause: cp sometimes preserves the
original mtime, which remains the modification date of the content. The
ctime (change date of the inode) is then the right lead, since it is
updated on every copy, every chmod and every rename.
# -mtime = content, -ctime = inode metadata, -atime = last accessfind . -ctime -1A nuance to know: modern file systems (ext4, XFS, Btrfs) do record a creation
date (birth time), readable with stat --printf='%w\n' fichier. But find
cannot always read it, support for the -newerBt predicate depends on how
the binary was built. On Debian 13 as on AlmaLinux 10 (findutils 4.10), the
command answers
This system does not provide a way to find the birth time of a file, whereas stat does print that date: do not build a retention script
on this date without testing it on the target machine.
Regex instead of glob
-name speaks glob, not regex.
find . -name ".*\.log$" # WRONG: returns nothingSymptom: no result. Cause: -name uses globs (*, ?), not
regular expressions.
find . -regex ".*\.log$" # RIGHT: -regex for a real regex (or simply -name "*.log")Hands-on lab
Nothing replaces practice. Create the test environment below, then go
through the eight steps. At the end, clean up with rm -rf ~/find-lab.
Prepare the ground
This script creates a tree in ~/find-lab with files of varied
sizes, dates and permissions, including names containing spaces.
# Create the labmkdir -p ~/find-lab/{logs,config,data,backup}cd ~/find-lab
# Log files of different sizesdd if=/dev/zero of=logs/app.log bs=1M count=5 2>/dev/nulldd if=/dev/zero of=logs/error.log bs=1K count=100 2>/dev/nulldd if=/dev/zero of=logs/debug.log bs=1M count=20 2>/dev/null
# Config filesecho "port=8080" > config/app.confecho "debug=true" > config/debug.conf
# Files with spaces in the nametouch "data/mon fichier.txt"touch "data/rapport 2024.pdf"
# Files with different datestouch -d "30 days ago" backup/old-backup.tartouch -d "7 days ago" backup/weekly.tartouch backup/daily.tar
# Varied permissionschmod 777 logs/debug.logchmod 644 config/app.conf
echo "Lab créé dans ~/find-lab"ls -laR ~/find-labThe eight steps
Each step adds a filter or an action to the previous one, up to the controlled deletion of step 8. Always compare the number of lines returned with what the lab actually contains: it is by noticing that a command returns more results than expected that you spot a misplaced filter. Steps 7 and 8 are the most important, they cover the two situations that most often break production scripts, names with spaces and unchecked deletions.
-
Find every
.logfile. List every file with the.logextension in the lab.Terminal window find ~/find-lab -name "*.log"-nameuses a glob pattern;*matches any characters. To ignore case:find ~/find-lab -iname "*.LOG". -
Filter by size. Find the files larger than 1 MB.
Terminal window find ~/find-lab -type f -size +1M+1Mmeans "more than 1 mebibyte", the+indicates "greater than". Expected result:app.loganddebug.log. -
Old files. Find the files modified more than 7 days ago.
Terminal window find ~/find-lab -type f -mtime +7+7means "modified more than 7 days ago";-7would be "less than 7 days". Onlyold-backup.tarcomes out:weekly.taris exactly 7 days old, which is not "more than 7". -
Combine size AND date. Find the files larger than 1 MB modified less than 1 day ago.
Terminal window find ~/find-lab -type f -size +1M -mtime -1By default, criteria combine with AND, no need to write
-a. -
.logOR.conffiles. Find the.logor.conffiles.Terminal window find ~/find-lab \( -name "*.log" -o -name "*.conf" \)-ois OR; the escaped parentheses (\(\)) group the conditions. -
Run a command on each file. Print the readable size of each
.log.Terminal window find ~/find-lab -name "*.log" -exec ls -lh {} \;{}is replaced with the file name;\;ends the command. Faster version:-exec ls -lh {} +. -
Handle names with spaces. List the files of the
data/directory in detail.Terminal window find ~/find-lab/data -type f -print0 | xargs -0 ls -la-print0andxargs -0use the null character:mon fichier.txtstays a single argument. -
Delete safely. Delete the
.tarfiles older than 7 days, preview first, delete next.Terminal window # Step 1: previewfind ~/find-lab/backup -name "*.tar" -mtime +7 -print# Step 2: once the list is checkedfind ~/find-lab/backup -name "*.tar" -mtime +7 -deleteNever use
-deletewithout checking the list with-print.
Progressive exercises
Eight exercises to validate your mastery. Search on your own first, then unfold the solution to check yourself against the lab.
Foundation level
These three exercises cover a single filter at a time. If you get stuck, the
answer always fits in a short option such as -maxdepth, -empty or
-mtime: look for the dedicated option rather than composing a complex
expression, find almost always offers one.
Exercise 1 - Find the .conf files. List the .conf files directly in /etc, without going into the subdirectories. Hint: -maxdepth 1 limits the depth.
Show the solution
find /etc -maxdepth 1 -name "*.conf"-maxdepth 1 restricts the search to the starting directory, without recursion.
Exercise 2 - Empty files. Find every empty file (0 bytes) in /tmp. Hint: there is an option dedicated to empty files.
Show the solution
find /tmp -type f -empty-empty is more readable than -size 0 and also works for empty directories.
Exercise 3 - Recent files. Find the files of your home directory modified in the last 24 hours. Hint: -mtime with a negative number.
Show the solution
find ~ -type f -mtime -1-mtime -1 means "modified less than 1 day ago".
Composition level
Here you move on to combining criteria, and it is operator precedence that
traps you. Remember that the implicit AND binds tighter than -o: as soon as
an OR appears in a command that contains other filters, the escaped
parentheses become mandatory, or the result is silently wrong.
Exercise 4 - Size and extension. Find the .log files larger than 10 MB in /var/log. Hint: combine -name and -size.
Show the solution
find /var/log -type f -name "*.log" -size +10MCriteria combine with AND by default, no need to write -a.
Exercise 5 - Logical OR. Find the .jpg or .png files in ~/Images. Hint: -o with escaped parentheses.
Show the solution
find ~/Images -type f \( -name "*.jpg" -o -name "*.png" \)The parentheses group the OR; without them, the logic would be wrong.
Industrialisation level
At this stage, find no longer looks, it acts. The two exercises oppose the
two available mechanisms, the built-in -exec and the xargs pipeline.
On large trees, measure the difference with time: measured on 10,000 files,
\; takes 5.6 s against 0.09 s for +.
Exercise 6 - Run ls -lh on the results. Print the readable size of each file larger than 100 MB in /var. Hint: -exec with ls -lh.
Show the solution
find /var -type f -size +100M -exec ls -lh {} \;{} is replaced with each file found; \; ends the command.
Exercise 7 - Safe xargs pipeline. Count the total number of lines of every .py file of a project, even with spaces in the names. Hint: -print0 then xargs -0.
Show the solution
find . -name "*.py" -print0 | xargs -0 wc -l-print0 and xargs -0 handle names with spaces safely.
Safety level
The last exercise is the only destructive one, and that is on purpose. Run it
first on a throwaway directory of your own, never directly on the /tmp of a
shared server: other processes drop working files there whose disappearance
causes failures that are hard to link back to your command.
Exercise 8 - Safe cleanup. Delete the .tmp files older than 7 days in /tmp, showing first what will be deleted. Hint: two commands, -print then -delete.
Show the solution
# Step 1: previewfind /tmp -name "*.tmp" -mtime +7 -print# Step 2: once the list is checkedfind /tmp -name "*.tmp" -mtime +7 -deleteAlways preview with -print before using -delete.
Troubleshooting
When find does not return what you expect, the problem most often comes from
quoting or permissions. Here is how to diagnose it.
Diagnostic methods
The method is to remove the filters one by one until you get results, then
put back the last one removed: it is the culprit. The last command of the block
deserves a word, it redirects stderr to stdout to filter the
Permission denied messages. Without this 2>&1 redirection, grep sees
nothing because the errors of find do not go through standard output.
# Remove the filters one by one to isolate the problemfind /chemin
# Add an explicit -print to see what is processedfind /chemin -name "*.log" -print
# Check the quoting (the shell may interpret * before find)find /chemin -name "*.log"
# Spot the directories where find has no rightsfind /chemin 2>&1 | grep -i permissionFrequent errors
The Fix column sometimes suggests 2>/dev/null, to handle with care: hiding
permission errors is comfortable for an interactive search, but in a script it
also hides the real failures, a full disk or a vanished mount point. In a
scheduled script, keep the errors visible and rather check the return code of
find, which is 1 as soon as a directory could not be walked.
| Error message | Probable cause | Fix |
|---|---|---|
find: '/root': Permission denied | No read right on the directory | Use sudo, or hide the errors: find /chemin 2>/dev/null |
find: '/chemin': No such file or directory | The starting path does not exist | Check the path with ls, check the spelling |
find: missing argument to '-exec' | \; or + forgotten at the end of -exec | End the command: -exec ls -l {} \; |
find cheat sheet
The four tables that follow serve as a reminder once the guide is absorbed. They are organised in the order in which you build a command, walk then filters then actions then composition, which lets you read them from top to bottom while writing.
Walk options
These options go before the path or just after, and they set the extent of
the walk rather than the selection. -xdev is the one most often forgotten:
without it, a find / crosses NFS mounts, backup partitions and /proc,
which turns a thirty-second search into several minutes.
| Option | Role | Example |
|---|---|---|
-maxdepth N | Limits the search depth | find . -maxdepth 2 -name "*.txt" |
-mindepth N | Ignores the first N levels | find . -mindepth 1 -type d |
-L | Follows symbolic links | find -L /opt -name "*.conf" |
-xdev | Stays on the same file system | find / -xdev -size +100M |
Filters
Filters stack freely and combine with an implicit AND. Two rows deserve special
attention: -path compares the full path where -name only sees the name,
and -newer compares with the modification date of a witness file,
which gives a handy way to find everything that changed since a deployment by
dropping a marker file before the operation.
| Filter | Role | Example |
|---|---|---|
-name "*.ext" | Name with glob (case-sensitive) | -name "*.log" |
-iname "*.ext" | Name with glob (case-insensitive) | -iname "*.JPG" |
-type f / -type d | Files / directories only | -type f |
-path "*pattern*" | Pattern on the full path | -path "*/test/*" |
-size +100M / -size -1k | Larger / smaller than | -size +1G |
-empty | Empty files or directories | -type f -empty |
-mtime +7 / -mtime -1 | Modified more / less than N days ago | -mtime +30 |
-newer fichier | Newer than a reference file | -newer ref.txt |
-perm 644 | Exact permissions | -perm 755 |
-user bob | Belongs to a user | -user root |
Actions
A single action per command in everyday practice, but nothing forces it:
find . -name '*.log' -print -delete prints then deletes in the same pass.
Avoid this form, it gives the illusion of control whereas the deletion has
already happened when you read the output.
| Action | Role | Example |
|---|---|---|
-print | Prints the path (default) | -print |
-print0 | Prints separated by a null character | -print0 | xargs -0 |
-exec cmd {} \; | Runs the command for each file | -exec rm {} \; |
-exec cmd {} + | Runs the command in batches | -exec ls {} + |
-delete | Deletes the files found | -mtime +30 -delete |
Composition
The precedence rule fits in one sentence: AND (-a, implicit) binds tighter
than OR (-o), exactly as multiplication binds tighter than addition. As
soon as an -o lives alongside another filter, escaped parentheses are
mandatory, otherwise find groups the terms differently from how you read them.
| Operator | Role | Example |
|---|---|---|
-a | AND (implicit between two criteria) | -name "*.log" -a -size +1M |
-o | OR | -name "*.jpg" -o -name "*.png" |
\( … \) | Grouping of conditions | \( -name "*.jpg" -o -name "*.png" \) |
-prune | Excludes a subtree from the walk | -path "./vendor" -prune |
Mastery checklist
You master find when you can tick each of these points, from -name to
-prune and safe deletion:
- I can use
-nameand-iname - I can filter by
-type(f,d,l) - I can use
-sizewith+/-and the units - I can use
-mtime,-ctime,-atime - I can combine criteria with
-oand parentheses - I can use
-exec {} \;and-exec {} + - I can secure a pipeline with
-print0 \| xargs -0 - I can secure
-deleteby running-printfirst - I can exclude a directory with
-prune - I can diagnose a search that returns nothing
Conclusion
The find command is an essential tool for anyone who regularly works on
the command line. Thanks to its flexibility and rich features, it lets you
find, filter and handle files efficiently in any tree. Whether for a quick
search, an automated cleanup or a permissions audit, mastering find saves
precious time and strengthens the quality of your scripts and system work.
Articles you may find useful
Key points
findwalks the disk in real time: path, then filters, then action, always in this order.- The pattern of
-namegoes between quotes, otherwise the shell expands it beforefind. - Filters combine with an implicit AND; an
-orequires escaped parentheses. -sizerounds up to the unit: for small files, reason in bytes withc.! -namefilters the output,-prunereally stops the descent into a directory.-exec … {} +groups the files,\;launches one process per file.- Always
-printbefore-delete, and never-deletewith-prune.
Knowledge check
Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: tar: create and extract a tar.gz archive
A new full quiz with random questions
Work again only on the questions you missed
FAQ: frequently asked questions
The answers below go further than the page, with security audits, performance measurements and production practices.
Comparison with other tools
| Tool | Method | Speed | Freshness | Use |
|---|---|---|---|---|
| find | Real-time walk | Slow | 100% up to date | Precise search with complex criteria |
| locate | Database | Very fast | Daily update | Simple search by name |
| grep | File contents | Variable | Real time | Search in the content |
Basic syntax
find [chemin] [critères] [actions]
# Simple examples
find /home -name "*.log" # .log files in /home
find . -type d -name "cache" # Directories named "cache"
find /var -size +100M # Files > 100 MB
find /tmp -mtime +7 -delete # Delete files > 7 days
Concrete use cases
- Cleanup: delete temporary files or old logs
- Security audit: detect SUID/SGID files or 777 permissions
- Selective backup: archive recently modified files
- Advanced search: combine name, size, date, permissions
Detailed comparison
| Criterion | find | locate |
|---|---|---|
| Method | Real-time walk of the file system | Search in a database |
| Speed | Slow (physically walks the directories) | Very fast (reading an index) |
| Freshness | 100% up to date (current files) | Depends on the last update (updatedb) |
| Database | None | /var/lib/plocate/plocate.db (plocate) or /var/lib/mlocate/mlocate.db (mlocate) |
| Criteria | Multiple (name, size, date, permissions, etc.) | File name only |
| Resources | CPU and I/O intensive | Very light |
| Permissions | Respects the user's permissions | Also filters by read rights (mlocate, plocate) |
Updating the locate database
# Update the database manually
sudo updatedb
# Check the date of the last update (plocate, or mlocate on older systems)
ls -lh /var/lib/plocate/plocate.db
# Configuration (daily by default through cron)
cat /etc/updatedb.conf
Practical examples
# locate: fast search by name
locate nginx.conf # Instant
locate -i readme # Case-insensitive
# find: precise search with criteria
find /etc -name nginx.conf -type f # Slower but precise
find /var/log -name "*.log" -mtime -1 # Files modified < 24h
When to use which?
- locate: fast search for known files, stable system
- find: precise search, recent files, multiple criteria, automated scripts
find [path] [tests] [actions]Detailed structure
find [chemin_départ] [options] [tests/critères] [actions]
└─ where to search └─ how └─ what to look for └─ what to do
Components
| Component | Description | Examples |
|---|---|---|
| Path | Starting point of the search | /home, ., /var/log |
| Options | Behaviour modifiers | -maxdepth 2, -mount |
| Tests | Filtering criteria | -name, -type, -size, -mtime |
| Actions | Operations on results | -print, -delete, -exec |
Progressive examples
# 1. Simple: list every file
find /home/user
# 2. With a test: .txt files only
find /home/user -name "*.txt"
# 3. Multiple tests: .txt files > 1 MB
find /home/user -name "*.txt" -size +1M
# 4. With an action: delete .tmp files
find /tmp -name "*.tmp" -delete
# 5. Complex: logs > 100 MB modified > 7 days → archive
find /var/log -name "*.log" -size +100M -mtime +7 -exec gzip {} \;
Logical operators
# Implicit AND (default)
find . -name "*.log" -size +10M # name AND size
# Explicit OR
find . -name "*.log" -o -name "*.txt" # .log OR .txt
# Negation
find . ! -name "*.git" # everything EXCEPT .git
find . -not -user root # not owned by root
# Grouping with parentheses
find . \( -name "*.log" -o -name "*.txt" \) -size +1M
Flow diagram
find /var/log
↓
[Tests] -name "*.log" → No → Skip
↓ Yes
[Tests] -mtime +30 → No → Skip
↓ Yes
[Action] -delete → Delete the file
Search options
| Option | Case sensitivity | Scope | Use |
|---|---|---|---|
-name |
Sensitive | File name only | Exact search |
-iname |
Insensitive | File name only | Flexible search |
-path |
Sensitive | Full path | Filtering by tree |
-ipath |
Insensitive | Full path | Flexible filtering |
-regex |
Sensitive (by default) | Regular expression | Complex search |
Wildcards
# *: any characters (0 or more)
find /var/log -name "*.log" # Every .log
find . -name "access*" # access.log, access_2024.log...
# ?: exactly one character
find . -name "file?.txt" # file1.txt, fileA.txt (not file10.txt)
# []: one character from a list
find . -name "file[0-9].txt" # file0.txt to file9.txt
find . -name "[A-Z]*.txt" # Starts with an uppercase letter
# Combinations
find . -name "[Dd]ocker*.[jt]son" # Docker.json, dockerfile.json...
Case sensitivity
# -name: case-sensitive
find . -name "README.md" # Finds README.md only
# -iname: case-insensitive
find . -iname "readme.md" # Finds README.md, readme.MD, ReadMe.md...
Search by path
# -path: include the path in the search
find . -path "*/node_modules/*" # Files in node_modules
find . -path "*/test/*.js" # .js files in test directories
# Exclude paths
find . -path "*/node_modules" -prune -o -name "*.js" -print
Regex (advanced search)
# Regular expression on the full path
find . -regex ".*/[a-z]+\\.log" # Lowercase names + .log
# Extended POSIX regex (-regextype posix-extended)
find . -regextype posix-extended -regex ".*\\.(jpg|png|gif)$"
Practical cases
# Configuration files
find /etc -name "*.conf"
# Shell scripts
find . -name "*.sh" -o -name "*.bash"
# Hidden files
find ~ -name ".*" -type f
# Temporary files
find /tmp -name "*~" -o -name "*.tmp" -o -name "*.swp"
Available types
| Type | Description | Example |
|---|---|---|
f |
Regular file | Documents, scripts, executables |
d |
Directory | Folders |
l |
Symbolic link (symlink) | System shortcuts |
c |
Character device | /dev/null, /dev/tty |
b |
Block device | /dev/sda, disks |
p |
Named pipe (pipe/FIFO) | Inter-process communication |
s |
Socket | Local network communication |
Common examples
# Regular files only
find /home -type f -name "*.pdf"
# Directories only
find /var -type d -name "cache"
# Symbolic links
find /usr/bin -type l
# Broken symbolic links (dead symlinks)
find /opt -type l ! -exec test -e {} \; -print
Useful combinations
# Executable .sh files
find . -type f -name "*.sh" -executable
# Empty directories
find /tmp -type d -empty
# Empty files (0 bytes)
find /var/log -type f -empty
# Sockets in /var/run
find /var/run -type s
Specific use cases
# Audit the symbolic links in /etc
find /etc -type l -ls
# Find every .git directory
find ~/projects -type d -name ".git"
# List the block devices
find /dev -type b
# Clean up empty directories
find /tmp -type d -empty -delete
Size units
| Suffix | Unit | Real size |
|---|---|---|
c |
Bytes | 1 byte |
k |
Kibibytes | 1024 bytes |
M |
Mebibytes | 1024 KiB |
G |
Gibibytes | 1024 MiB |
T |
Tebibytes | 1024 GiB |
| (none) | 512-byte blocks | Old syntax |
Operators
# Exactly N units, after rounding up to the unit
find . -size 100k # Between 99 KiB (excluded) and 100 KiB
# Larger than (+)
find . -size +100M # Files > 100 MB
# Smaller than (-), watch the rounding
find /var/log -size -1k # EMPTY files only (1 byte counts as 1k)
find /var/log -size -1024c # Really less than 1 KiB
# Ranges (combination)
find . -size +10M -size -100M # Between 10 MB and 100 MB
Practical examples
# Large logs (> 100 MB)
find /var/log -type f -size +100M
# Empty files (0 bytes)
find /tmp -type f -size 0
# Docker cache (images > 1 GB)
find /var/lib/docker -type f -size +1G
# Suspicious files (very small or very large)
find /home -type f \( -size -10c -o -size +5G \)
Cleanup and optimisation
# Delete temporary files > 100 MB
find /tmp -type f -size +100M -mtime +7 -delete
# Archive logs > 50 MB
find /var/log -name "*.log" -size +50M -exec gzip {} \;
# List the 10 largest files
find /home -type f -size +10M -exec ls -lh {} \; | sort -k5 -hr | head -10
Unit conversion
# 100 MB = 102400 KB = 104857600 bytes
find . -size +100M
find . -size +102400k # Equivalent
find . -size +104857600c # Equivalent
Date options
| Option | Unit | Meaning | Use |
|---|---|---|---|
-mtime |
Days | Content modification | Files edited X days ago |
-atime |
Days | Read access | Files read X days ago |
-ctime |
Days | Metadata change | Permissions/owner modified |
-mmin |
Minutes | Modification (minutes) | Recently modified files |
-amin |
Minutes | Access (minutes) | Recently read files |
-cmin |
Minutes | Change (minutes) | Recently changed metadata |
-newer |
Reference | Newer than a file | Relative comparison |
Time operators
# +N: more than N (strictly greater)
find /var/log -mtime +30 # Modified MORE than 30 days ago
# -N: less than N (strictly lower)
find /tmp -mtime -7 # Modified LESS than 7 days ago
# N: exactly N
find . -mtime 0 # Modified today (last 24h)
Concrete examples
# Recent files (< 24h)
find /var/log -type f -mtime 0
find /var/log -type f -mmin -60 # Last hour
# Old files (> 90 days)
find /backup -type f -mtime +90
# Unused files (not accessed for 1 year)
find /opt -type f -atime +365
# Files modified in the last 7 days
find /home -type f -mtime -7
Comparing with a reference file
# Files newer than reference.txt
find . -newer reference.txt
# Files modified after a precise date
touch -t 202601010000 /tmp/marker # 1 January 2026
find /data -newer /tmp/marker
Useful combinations
# Old AND large logs
find /var/log -name "*.log" -mtime +30 -size +100M
# Recent files of a specific user
find /home -user alice -mtime -7
# Cache cleanup (> 7 days and not accessed)
find ~/.cache -type f -atime +7 -delete
Date ranges
# Between 7 and 30 days
find /tmp -mtime +7 -mtime -30
# Modified today or yesterday
find /var/log -mtime -2
Notations
| Notation | Example | Description |
|---|---|---|
| Octal | 755, 644 |
Permissions as digits (rwxr-xr-x) |
| Symbolic | u=rwx,g=rx,o=rx |
Readable permissions (user/group/other) |
Matching modes
# Exact match (no prefix)
find . -perm 644 # Exactly rw-r--r--
# At least these permissions (- prefix)
find . -perm -644 # At least rw-r--r-- (may have more)
# At least one permission (/ prefix)
find . -perm /222 # Writable by user OR group OR other
Security examples
# Files writable by everyone (DANGER)
find / -type f -perm -002 2>/dev/null
find /var/www -perm /o+w # Symbolic
# 777 permissions (very permissive)
find /home -type f -perm 0777
# SUID files (privilege escalation)
find / -perm -4000 -type f 2>/dev/null
# SGID files
find / -perm -2000 -type f 2>/dev/null
# Both SUID and SGID
find / -perm -6000 -type f 2>/dev/null
Permission audit
# Executable files not owned by root
find /usr/bin -type f -perm -111 ! -user root
# Directories without execute permission
find /var -type d ! -perm -111
# World-readable files in /etc
find /etc -type f -perm -004
# Non-executable shell scripts
find . -name "*.sh" ! -perm -100
Advanced symbolic notation
# Files with user write permission
find . -perm -u+w
# Files with group read permission
find . -perm -g+r
# Files with other execute permission
find /usr/local -perm -o+x
Fixing permissions
# Remove write permission for other
find /var/www -type f -perm /o+w -exec chmod o-w {} \;
# Add execute permission to .sh files
find . -name "*.sh" ! -perm -u+x -exec chmod u+x {} \;
# Directories: 755, files: 644
find /var/www -type d -exec chmod 755 {} \;
find /var/www -type f -exec chmod 644 {} \;
Special bits
| Bit | Value | Meaning |
|---|---|---|
| SUID | 4000 | Runs with the owner's rights |
| SGID | 2000 | Runs with the group's rights |
| Sticky | 1000 | Only the owner can delete (e.g. /tmp) |
# Find every file with special bits
find / -perm /7000 -ls 2>/dev/null
Available options
| Option | Type | Description | Example |
|---|---|---|---|
-user |
Name or UID | Files belonging to a user | alice, 1000 |
-group |
Name or GID | Files belonging to a group | www-data, 33 |
-uid |
UID only | Search by numeric identifier | 1000 |
-gid |
GID only | Search by group identifier | 1000 |
-nouser |
Orphan | Files without an existing owner | Security audit |
-nogroup |
Orphan | Files without an existing group | Security audit |
Examples by name
# A user's files
find /home -user alice
# A group's files
find /var/www -group www-data
# root files only
find /etc -user root -type f
Examples by ID (UID/GID)
# Search by UID
find /home -uid 1000
# Search by GID
find /var -gid 33 # www-data
# Useful after deleting a user
find / -uid 1005 2>/dev/null # Deleted user
Combining with permissions
# alice's files with 644 permissions
find /home/alice -user alice -perm 644
# www-data files writable by the group
find /var/www -group www-data -perm -g+w
# Non-root files with SUID (DANGER)
find / ! -user root -perm -4000 2>/dev/null
Security audit
# Files without an owner (orphans)
find / -nouser 2>/dev/null
# Files without a group
find / -nogroup 2>/dev/null
# Files of deleted users (invalid UIDs)
find /home -nouser -ls
# Files belonging to a specific user in /tmp
find /tmp -user alice -type f
Changing ownership
# Reassign orphan files
find / -nouser -exec chown root:root {} \;
# Change the group of every file of a project
find /var/www/projet -exec chgrp www-data {} \;
-exec syntaxes
| Syntax | Execution | Use | Performance |
|---|---|---|---|
-exec cmd {} \; |
One command per file | Simple actions | Slow (fork per file) |
-exec cmd {} + |
Grouped command (batch) | Bulk actions | Fast (like xargs) |
The {} placeholder
The {} symbol is replaced with the path of the file found.# Basic syntax
find /tmp -name "*.tmp" -exec rm {} \;
│ │ │
command | placeholder | terminator
Practical examples
# Delete files
find /tmp -name "*.log" -exec rm {} \;
find /tmp -name "*.log" -delete # Faster equivalent
# Change permissions
find /var/www -type f -exec chmod 644 {} \;
find /var/www -type d -exec chmod 755 {} \;
# Move files
find . -name "*.bak" -exec mv {} /backup/ \;
# Copy files
find /src -name "*.conf" -exec cp {} /dest/ \;
# Archive logs
find /var/log -name "*.log" -mtime +30 -exec gzip {} \;
The {} + batch syntax
# Grouped execution (faster)
find /var/log -name "*.log" -exec rm {} +
# Equivalent to: rm file1.log file2.log file3.log...
# Vs the classic syntax (slow)
find /var/log -name "*.log" -exec rm {} \;
# Equivalent to: rm file1.log && rm file2.log && rm file3.log...
Difference with xargs
# -exec: built into find, handles spaces
find . -name "*.txt" -exec cat {} \;
# xargs: pipeline, more flexible
find . -name "*.txt" | xargs cat # WATCH OUT for spaces!
find . -name "*.txt" -print0 | xargs -0 cat # Safe
Complex commands
# Run several commands
find . -name "*.jpg" -exec sh -c 'convert "$1" "${1%.jpg}.png"' _ {} \;
# Use variables
find . -type f -exec sh -c 'echo "Fichier: $1, Taille: $(stat -c%s "$1")"' _ {} \;
# Redirection inside -exec
find /var/log -name "*.log" -exec sh -c 'cat "$1" >> /tmp/all.log' _ {} \;
Good practices
# 1. Always test with -print before -exec
find /data -name "*.tmp" -print # Check the results
find /data -name "*.tmp" -exec rm {} \; # Then run
# 2. Use -ok for interactive confirmation
find . -name "*.bak" -ok rm {} \; # Asks for confirmation
# 3. Prefer {} + for performance
find /logs -name "*.log" -exec gzip {} + # Batch
# 4. Handle errors
find / -name "*.conf" -exec cat {} \; 2>/dev/null
Built-in actions (alternatives)
# -delete: delete (faster than -exec rm)
find /tmp -name "*.tmp" -delete
# -print: print (default)
find /etc -name "*.conf" -print
# -ls: detailed output (like ls -l)
find /var -size +100M -ls
-exec vs -ok
| Option | Confirmation | Use | Use case |
|---|---|---|---|
-exec |
None | Automatic execution | Scripts, safe actions |
-ok |
Manual (y/n) | Interactive execution | Critical deletions |
Syntax
find [chemin] [tests] -ok [commande] {} \;
│ │ │
confirmation file terminator
Interactive example
# Delete with confirmation
$ find /tmp -name "*.bak" -ok rm {} \;
< rm ... /tmp/file1.bak > ? y
< rm ... /tmp/file2.bak > ? n
< rm ... /tmp/file3.bak > ? y
# Each file asks for confirmation (y = yes, n = no)
Security use cases
# Deleting important files
find /home -name "*.conf" -ok rm {} \;
# Changing sensitive permissions
find /etc -type f -ok chmod 600 {} \;
# Moving critical files
find /backup -name "*.sql" -ok mv {} /archive/ \;
Alternative: -okdir
# Runs the command from the file's directory
find /var/log -name "*.log" -okdir gzip {} \;
# Safer: avoids symbolic link attacks
When to use -ok?
- Bulk deletions: check before deleting
- First run of a complex find command
- Sensitive environments: production, critical data
- Learning: understand what the command does
Alternative: test with -print
# Recommended method in production
# 1. Test with -print
find /data -name "*.tmp" -print
# 2. Check the results
# 3. Run with -exec (or -delete)
find /data -name "*.tmp" -delete
Optimisation options
| Option | Description | Performance gain |
|---|---|---|
-maxdepth N |
Limits the search depth | Very high |
-mindepth N |
Ignores the first N levels | Moderate |
-prune |
Excludes whole directories | Very high |
-mount (-xdev) |
Does not cross file systems | Moderate |
-quit |
Stops after the first result | Maximal |
Limit the depth
# Search only in the current directory (no recursion)
find . -maxdepth 1 -name "*.txt"
# 2 levels maximum
find /var -maxdepth 2 -name "*.conf"
# Ignore the current directory (start at depth 2)
find /home -mindepth 2 -name "*.log"
# Combine min and max
find /var -mindepth 2 -maxdepth 4 -type d
Exclude directories with -prune
# Exclude node_modules
find . -path "*/node_modules" -prune -o -name "*.js" -print
# Exclude .git and node_modules
find . \( -path "*/.git" -o -path "*/node_modules" \) -prune -o -type f -print
# Exclude /proc and /sys (system)
find / \( -path /proc -o -path /sys \) -prune -o -name "*.conf" -print
# Exclude several patterns
find /home \( -name ".cache" -o -name ".npm" -o -name ".local" \) -prune -o -type f -print
Do not cross mounts
# Stay on the current file system
find / -mount -name "*.log"
find / -xdev -name "*.log" # Alias
# Useful to avoid /proc, /dev, NFS, etc.
find /home -xdev -type f -size +1G
Early stop
# Stop after the first result (-print before -quit, otherwise nothing shows)
find /etc -name "nginx.conf" -print -quit
# Find a single example
find /var/log -name "*.log" -print -quit
Performance examples
# SLOW: full recursive search
find /home -name "config.json" # Can take several minutes
# FAST: limit the depth
find /home -maxdepth 3 -name "config.json" # A few seconds
# VERY FAST: exclude the large directories
find /home \( -path "*/node_modules" -o -path "*/.cache" \) -prune -o -name "config.json" -print
Measurements (Debian 13 container, warm cache)
# Without optimisation: 3,875 results
time find /usr -name "*.h"
# real: 0.15 s
# With -maxdepth: 20 times faster, but only 954 results
time find /usr -maxdepth 3 -name "*.h"
# real: 0.008 s
# The gain of a limit is paid in results: check that it cuts nothing useful
Good practices
- Always limit the depth when possible:
-maxdepth - Exclude the system directories:
/proc,/sys,/dev - Exclude the large directories:
node_modules,.git,vendor - Use
-xdevnot to cross mounts - Put the most selective tests first (e.g.
-namebefore-size) - Use
-quitif a single result is enough
Optimised template
# Standard optimised search
find /chemin \
-maxdepth 5 \
-xdev \
\( -path "*/node_modules" -o -path "*/.git" \) -prune -o \
-type f -name "*.ext" -print
xargs advantages
| Feature | -exec {} \; |
-exec {} + |
xargs |
|---|---|---|---|
| Runs | One per file | Grouped | Grouped |
| Performance | Very slow | Fast | Fast (equivalent to {} +) |
| Flexibility | Limited | Limited | High |
| Space handling | Native | Native | Requires -0 |
Problem: spaces and special characters
# DANGER: basic xargs breaks on spaces
find . -name "*.txt" | xargs cat
# The file "mon document.txt" will be processed as 2 files: "mon" and "document.txt"
# SOLUTION: -print0 and -0
find . -name "*.txt" -print0 | xargs -0 cat
# ✅ Works correctly with spaces, tabs, newlines
-print0 and -0 (NULL separator)
# find -print0: NULL separator (\0) instead of \n
# xargs -0: reads with a NULL separator
find /var/log -name "*.log" -print0 | xargs -0 ls -lh
# How it works
find . -print # Output: file1\nfile 2\nfile3\n
find . -print0 # Output: file1\0file 2\0file3\0
Practical examples
# Count the lines of every .py file
find . -name "*.py" -print0 | xargs -0 wc -l
# Search a pattern in several files
find /var/log -name "*.log" -print0 | xargs -0 grep "ERROR"
# Copy files
find /src -name "*.conf" -print0 | xargs -0 -I {} cp {} /dest/
# Bulk delete
find /tmp -name "*.tmp" -mtime +7 -print0 | xargs -0 rm
# Archive
find /data -name "*.csv" -print0 | xargs -0 tar czf archive.tar.gz
Useful xargs options
# -I {}: custom placeholder
find . -name "*.txt" | xargs -I {} mv {} {}.bak
# -n N: max N arguments per run
find . -type f | xargs -n 10 ls -l # 10 files at a time
# -P N: parallel execution (N processes)
find . -name "*.jpg" -print0 | xargs -0 -P 4 -I {} convert {} {}.png
# -t: print the commands run (debug)
find . -name "*.log" -print0 | xargs -0 -t gzip
Performance difference
# Measured on Debian 13: delete 10,000 files
# -exec {} \;: 5.6 s (10,000 rm processes)
time find /tmp/test -name "*.tmp" -exec rm {} \;
# -exec {} +: 0.09 s (in batches)
time find /tmp/test -name "*.tmp" -exec rm {} +
# xargs: 0.09 s (in batches, equivalent to -exec +)
time find /tmp/test -name "*.tmp" -print0 | xargs -0 rm
ARG_MAX limits
# Maximum argument size
getconf ARG_MAX # Example: 2097152 (2 MB)
# xargs splits into batches automatically
find / -name "*.log" -print0 | xargs -0 rm
# If > ARG_MAX, xargs makes several calls automatically
Error handling
# Go on even on error
find . -name "*.txt" -print0 | xargs -0 -r cat
# -r: do nothing if the input is empty
# Redirect errors
find /var -name "*.conf" 2>/dev/null | xargs cat 2>/dev/null
When to prefer -exec?
- Complex commands with redirections
- Need for confirmation (
-ok) - Strict POSIX compatibility
- Security (less injection risk)
When to prefer xargs?
- Critical performance (thousands of files)
- Parallel processing (
-P) - Flexibility of the commands
- Pipeline with other tools
Safe template
# Recommended template for production
find /chemin \
-type f \
-name "pattern" \
-print0 | xargs -0 -r -P 4 commande
│ │ │ │ │
NULL | | | +-- parallel (4 processes)
separator | | +-- do nothing if empty
| +-- read the NULL separator
+-- safe pipeline
Production checklist
1. Always test with -print
# ❌ BAD: direct execution
find /data -name "*.tmp" -delete
# ✅ GOOD: test first
find /data -name "*.tmp" -print # Check the results
find /data -name "*.tmp" -delete # Then run
2. Redirect errors
# Drop the "Permission denied" errors
find / -name "*.conf" 2>/dev/null
# Redirect errors to a file
find /var -type f -size +1G 2>/var/log/find_errors.log
# Separate stdout and stderr
find / -name "*.log" > found.txt 2> errors.txt
3. Protect patterns with quotes
# ❌ BAD: interpreted by the shell
find . -name *.txt # The shell expands *.txt BEFORE find
# ✅ GOOD: quotes
find . -name "*.txt" # find receives the pattern intact
find . -name '*.txt' # Same
4. Back up before destructive actions
# Back up before deleting
find /data -name "*.old" -exec cp {} /backup/ \; -delete
# Archive before modifying
tar czf backup_$(date +%Y%m%d).tar.gz /data
find /data -name "*.log" -exec gzip {} \;
5. Use -ok for confirmation (first time)
# Interactive confirmation
find /etc -name "*.bak" -ok rm {} \;
# Then automate once validated
find /etc -name "*.bak" -delete
Robust scripts
#!/bin/bash
# Safe cleanup script
set -euo pipefail # Stop on error
# Variables
TARGET_DIR="/var/log"
MAX_AGE=30
LOG_FILE="/var/log/cleanup.log"
# Logging function
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# Preliminary checks
if [[ ! -d "$TARGET_DIR" ]]; then
log "ERREUR: Répertoire $TARGET_DIR introuvable"
exit 1
fi
# Dry run (test mode)
log "Mode TEST : fichiers qui seraient supprimés"
find "$TARGET_DIR" -name "*.log" -mtime +$MAX_AGE -print | tee -a "$LOG_FILE"
# Confirmation
read -p "Confirmer la suppression ? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log "Opération annulée"
exit 0
fi
# Execution
log "Début du nettoyage"
COUNT=$(find "$TARGET_DIR" -name "*.log" -mtime +$MAX_AGE -delete -print | wc -l)
log "$COUNT fichiers supprimés"
Error handling
# Go on even on errors
find /var -name "*.log" -exec gzip {} \; 2>/dev/null || true
# Check the return code
if ! find /data -name "*.tmp" -delete 2>/dev/null; then
echo "Erreur lors du nettoyage" >&2
exit 1
fi
Logging and audit
# Log the processed files
find /data -name "*.csv" -exec sh -c 'echo "Traité: $1" >> /var/log/process.log' _ {} \;
# Detailed report
find /backup -type f -mtime -1 -ls > /var/log/backup_report_$(date +%Y%m%d).txt
Production optimisations
# Limit the system load with ionice/nice
nice -n 19 ionice -c3 find /data -name "*.log" -exec gzip {} +
# Run in the background
nohup find / -name "*.tmp" -delete > /var/log/cleanup.log 2>&1 &
# Resource limits (cgroups)
systemd-run --scope -p CPUQuota=50% find /data -name "*.old" -delete
Final checklist
- ✅ Test with
-printbefore-deleteor-exec - ✅ Redirect stderr (
2>/dev/nullor to a log) - ✅ Quotes around patterns (
-name "*.txt") - ✅ Backups before destructive actions
- ✅ Logging of operations (audit trail)
- ✅ Confirmation for the first run (
-ok) - ✅ Optimise with
-maxdepth,-prune,-xdev - ✅ Limit the system load (
nice,ionice) - ✅ Monitoring of scripts (alerts on failure)
- ✅ Documentation of the expected behaviour
Production script examples
# Log cleanup (daily cron)
0 2 * * * find /var/log -name "*.log" -mtime +90 -delete 2>>/var/log/cron_errors.log
# Backup archiving (weekly cron)
0 3 * * 0 find /backup -name "*.sql" -mtime +7 -exec gzip {} \; 2>&1 | logger -t backup
# Security audit (daily cron)
0 4 * * * find / -perm -4000 -o -perm -2000 2>/dev/null | mail -s "SUID/SGID Audit" admin@example.com
Audit types
| Type | Goal | Risk level |
|---|---|---|
| SUID/SGID | Detect privilege escalation | Critical |
| 777 permissions | Overly permissive files | Critical |
| World-writable | Directories writable by everyone | Critical |
| Large files | Disk space management | Moderate |
| Old files | Cleanup and archiving | Low |
| Orphan files | Deleted users | Moderate |
1. SUID/SGID audit (special bits)
# SUID files (run with the owner's rights)
find / -perm -4000 -type f -ls 2>/dev/null
# SGID files (run with the group's rights)
find / -perm -2000 -type f -ls 2>/dev/null
# SUID AND SGID
find / -perm -6000 -type f -ls 2>/dev/null
# Non-root SUID (DANGEROUS)
find / -perm -4000 ! -user root -ls 2>/dev/null
# Detailed report
find / \( -perm -4000 -o -perm -2000 \) -type f -printf "%M %u:%g %p\n" 2>/dev/null > /tmp/suid_audit.txt
2. Overly permissive permissions audit
# 777 files (everyone can do everything)
find / -type f -perm 0777 -ls 2>/dev/null
# 777 directories
find / -type d -perm 0777 -ls 2>/dev/null
# Files writable by "other"
find /var/www -type f -perm -002 -ls
# World-readable files in /etc
find /etc -type f -perm -004 -ls
# World-writable executable scripts (SERIOUS)
find /usr/local/bin -type f -perm -111 -perm -002 -ls
3. Large files audit
# Files > 1 GB
find / -type f -size +1G -exec ls -lh {} \; 2>/dev/null
# Top 20 largest files
find /var -type f -size +100M -exec ls -lh {} \; 2>/dev/null | sort -k5 -hr | head -20
# Logs > 100 MB
find /var/log -name "*.log" -size +100M -printf "%s %p\n" | sort -nr
# Large Docker cache
find /var/lib/docker -type f -size +500M -ls 2>/dev/null
4. Old/unused files audit
# Files not modified for 1 year
find /home -type f -mtime +365 -ls
# Files not accessed for 6 months
find /opt -type f -atime +180 -ls
# Logs older than 90 days
find /var/log -name "*.log" -mtime +90 -exec du -h {} \;
# Obsolete backups
find /backup -name "*.tar.gz" -mtime +30 -ls
5. World-writable directories audit
# Directories writable by everyone (except /tmp with the sticky bit)
find / -type d -perm -002 ! -perm -1000 -ls 2>/dev/null
# Directories without the sticky bit in /tmp
find /tmp -type d ! -perm -1000 -ls
6. Orphan files audit
# Files without an owner (deleted user)
find / -nouser -ls 2>/dev/null
# Files without a group
find / -nogroup -ls 2>/dev/null
# Combined
find /home -nouser -o -nogroup 2>/dev/null
Full audit reports
#!/bin/bash
# Full security audit script
REPORT="/var/log/security_audit_$(date +%Y%m%d).txt"
echo "=== AUDIT SÉCURITÉ $(date) ===" > "$REPORT"
printf '\n### Fichiers SUID/SGID\n' >> "$REPORT"
find / \( -perm -4000 -o -perm -2000 \) -type f -ls 2>/dev/null >> "$REPORT"
printf '\n### Permissions 777\n' >> "$REPORT"
find / -type f -perm 0777 -ls 2>/dev/null >> "$REPORT"
printf '\n### Fichiers world-writable\n' >> "$REPORT"
find / -type f -perm -002 ! -type l -ls 2>/dev/null >> "$REPORT"
printf '\n### Fichiers > 1 Go\n' >> "$REPORT"
find / -type f -size +1G -ls 2>/dev/null >> "$REPORT"
printf '\n### Fichiers orphelins\n' >> "$REPORT"
find / \( -nouser -o -nogroup \) -ls 2>/dev/null >> "$REPORT"
# Send the report
mail -s "Audit sécurité quotidien" admin@example.com < "$REPORT"
Automation (cron)
# /etc/cron.daily/security-audit
#!/bin/bash
# Daily SUID audit
find / -perm -4000 -type f -ls 2>/dev/null > /var/log/suid_$(date +%Y%m%d).log
# Alert on new SUID files (comparison with the previous record)
PRECEDENT=$(ls -1t /var/log/suid_*.log | sed -n 2p)
if [ -n "$PRECEDENT" ] && ! diff -q "$PRECEDENT" /var/log/suid_$(date +%Y%m%d).log > /dev/null; then
echo "ALERTE: Nouveaux fichiers SUID détectés" | mail -s "Sécurité: SUID" admin@example.com
fi
# 777 permissions audit
find /var/www -type f -perm 0777 -ls 2>/dev/null | \
mail -s "Fichiers 777 dans /var/www" admin@example.com
Dashboards
# Disk statistics
echo "Fichiers par taille:"
echo " < 1 Mo: $(find /data -type f -size -1048576c | wc -l)"
echo " 1-10 Mo: $(find /data -type f -size +1M -size -10M | wc -l)"
echo " 10-100 Mo: $(find /data -type f -size +10M -size -100M | wc -l)"
echo " > 100 Mo: $(find /data -type f -size +100M | wc -l)"
# Files by age
echo "Fichiers par ancienneté:"
echo " < 7 jours: $(find /data -mtime -7 | wc -l)"
echo " 7-30 jours: $(find /data -mtime +7 -mtime -30 | wc -l)"
echo " > 30 jours: $(find /data -mtime +30 | wc -l)"