Linux find Command
last modified October 23, 2026
The find command in Linux searches a directory tree for files
and directories that match given criteria, such as name, type, size,
modification time, owner, or permissions, and then performs an action on
each match. The default action is to print the path, but find
can also run commands, delete files, or produce custom reports.
The general syntax is find [starting-points] [expression]. The
expression consists of tests (such as -name),
actions (such as -print or -exec), and
operators that combine them. find examines every file
below the starting points and evaluates the expression for each one. The
examples use GNU find from findutils 4.9.
Table of contents
- Basics: sample project, basic usage, names, quoting, types, paths and regexes, depth
- Tests: size, empty files, time, owner and permissions
- Expressions: operators, precedence, excluding directories
- Actions: custom output, -exec, find and grep, -execdir and -ok, -delete, xargs
- More: symbolic links, permission errors, recipes
Sample project
The examples run in the following project directory. The files have different sizes and modification times, one file belongs to root, and there are two symbolic links, one of them broken.
tree -a -I .git
The -a option of tree shows hidden files, and
-I .git hides the Git directory. Unless noted otherwise, the
commands are run from the project directory.
$ tree -a -I .git
.
├── broken.lnk -> old/config.ini
├── build
├── data
│ ├── empty.csv
│ └── sales.csv
├── docs
│ ├── guide.md
│ ├── Guide-old.MD
│ └── images
│ ├── diagram.svg
│ └── logo.png
├── .env
├── latest.log -> logs/error.log
├── logs
│ ├── app.log
│ ├── archive
│ │ └── 2026-08.log.gz
│ └── error.log
├── node_modules
│ └── lodash
│ └── index.js
├── README.md
├── scripts
│ ├── backup.sh
│ └── deploy.sh
├── setup.py
├── src
│ ├── app.py
│ ├── config.json
│ ├── __pycache__
│ │ └── app.cpython-311.pyc
│ ├── utils.py
│ └── widget.js
└── tests
├── test_app.py
└── test_utils.py
13 directories, 24 files
Basic usage
Given only a starting point, find lists everything below it,
including the starting point itself.
find src find src tests -type f
Several starting points can be given. Without one, GNU find
searches the current directory. The results come in the order in which the
file system returns directory entries, which is not alphabetical; pipe them
to sort when the order matters.
$ find src src src/config.json src/__pycache__ src/__pycache__/app.cpython-311.pyc src/utils.py src/widget.js src/app.py $ find src tests -type f src/config.json src/__pycache__/app.cpython-311.pyc src/utils.py src/widget.js src/app.py tests/test_app.py tests/test_utils.py
Searching by name
The -name test matches the file name against a shell-style
pattern, and -iname does the same ignoring case.
find . -name "*.py" find . -name "guide*" find . -iname "guide*"
The pattern can contain *, ?, and character
classes such as [0-9]. It is matched against the base name
only, not the directory part of the path. With -iname, the file
Guide-old.MD matches as well.
$ find . -name "*.py" ./setup.py ./tests/test_app.py ./tests/test_utils.py ./src/utils.py ./src/app.py $ find . -name "guide*" ./docs/guide.md $ find . -iname "guide*" ./docs/Guide-old.MD ./docs/guide.md
Quoting patterns
Patterns must be quoted, otherwise the shell expands them before
find runs.
find . -name *.py find . -name *.txt find . -name "*.txt"
The first command looks correct but is wrong: the shell replaces
*.py with setup.py, the only match in the current
directory, so find searches only for that name. When the
pattern matches two files, find receives an extra argument and
reports an error. With quotes, find gets the pattern itself and
works as intended.
$ find . -name *.py ./setup.py $ touch notes.txt todo.txt $ find . -name *.txt find: paths must precede expression: `todo.txt' find: possible unquoted pattern after predicate `-name'? $ find . -name "*.txt" ./notes.txt ./todo.txt
Searching by type
The -type test selects files by type: f for
regular files, d for directories, and l for
symbolic links.
find . -type d find . -type l find src -type f -name "*.py"
Other types are b and c (block and character
devices), p (named pipes), and s (sockets). GNU
find also accepts a list, such as -type f,l.
$ find . -type d . ./logs ./logs/archive ./scripts ./docs ./docs/images ./data ./tests ./.git ./src ./src/__pycache__ ./node_modules ./node_modules/lodash ./build $ find . -type l ./broken.lnk ./latest.log $ find src -type f -name "*.py" src/utils.py src/app.py
Paths and regular expressions
The -path test matches a pattern against the whole path, and
-regex matches it against a regular expression.
find . -path "*/tests/*" find . -regex ".*/test_[a-z]+\.py" find . -regextype posix-extended -regex ".*\.(md|MD)$"
In -path patterns, * also matches slashes. The
regular expression must match the entire path, which is why it starts with
.*. The default regex syntax is Emacs-style; the
-regextype option selects others, such as
posix-extended, which supports alternation with parentheses.
$ find . -path "*/tests/*" ./tests/test_app.py ./tests/test_utils.py $ find . -regex ".*/test_[a-z]+\.py" ./tests/test_app.py ./tests/test_utils.py $ find . -regextype posix-extended -regex ".*\.(md|MD)$" ./README.md ./docs/Guide-old.MD ./docs/guide.md
Limiting the depth
The -maxdepth and -mindepth options limit how deep
find descends.
find . -maxdepth 1 -type f find . -mindepth 3 -type f
Depth 0 is the starting point itself, and depth 1 its direct contents. The
first command lists the files in the current directory only, including the
hidden .env. The second shows only files at least three levels
deep. These are global options, so put them right after the starting
points.
$ find . -maxdepth 1 -type f ./setup.py ./README.md ./.env $ find . -mindepth 3 -type f ./logs/archive/2026-08.log.gz ./docs/images/logo.png ./docs/images/diagram.svg ./src/__pycache__/app.cpython-311.pyc ./node_modules/lodash/index.js
Searching by size
The -size test compares the file size. A + prefix
means greater than and - less than. Units are c
(bytes), k, M, and G.
find . -type f -size +100k find . -type f -size +1M find . -type f -size -1M find docs/images -type f -size -2M find docs/images -type f -size -1500k
There is a trap: before comparing, find rounds the file size up
to whole units. With -size -1M, every non-empty file counts as
at least 1M, so only the empty file matches. Likewise, the 1.5 MB image is
rounded up to 2M and does not match -size -2M. Use smaller units
such as k or c for precise limits.
$ find . -type f -size +100k ./docs/images/logo.png ./data/sales.csv $ find . -type f -size +1M ./docs/images/logo.png $ find . -type f -size -1M ./data/empty.csv $ find docs/images -type f -size -2M docs/images/diagram.svg $ find docs/images -type f -size -1500k docs/images/logo.png docs/images/diagram.svg
Empty files and directories
The -empty test matches empty regular files and directories.
find . -empty find . -type f -empty
Combined with -delete, this cleans up empty directories left
after a build or an extraction.
$ find . -empty ./data/empty.csv ./build $ find . -type f -empty ./data/empty.csv
Searching by time
The -mtime test compares the modification time in days, and
-mmin in minutes. The -newer and
-newermt tests compare with a file or a date.
find . -type f -mtime -7 find . -type f -mtime +30 find . -type f -mmin -60 find . -newer src/app.py -type f find . -type f -newermt '5 days ago' ! -newermt '1 hour ago'
-mtime -7 finds files modified less than 7 days ago, and
+30 files older than 30 days. The age is counted in whole
24-hour periods with the fraction dropped, so -mtime +30 means
at least 31 days. -newer finds files modified after a reference
file, and -newermt accepts any date string understood by
date; the last command finds files modified between five days
and one hour ago. The -atime and -ctime tests work
with the access and status change times.
$ find . -type f -mtime -7 ./logs/error.log ./tests/test_utils.py ./src/utils.py ./src/app.py $ find . -type f -mtime +30 ./logs/app.log ./logs/archive/2026-08.log.gz $ find . -type f -mmin -60 ./tests/test_utils.py $ find . -newer src/app.py -type f ./tests/test_utils.py ./src/utils.py $ find . -type f -newermt '5 days ago' ! -newermt '1 hour ago' ./logs/error.log ./src/utils.py ./src/app.py
Owner and permissions
The -user and -group tests match the owner, and
-perm matches permission bits.
find . -user root find . -type f -perm -o+w find scripts -perm 755 find . -type f -perm /u+x
-perm has three forms. Without a prefix, the mode must match
exactly; the scripts directory matches too, as it also has mode
755. With -, all the given bits must be set, so
-perm -o+w finds world-writable files, a common security check.
With /, any of the bits is enough; -perm /u+x finds
files executable by their owner. The -nouser test finds files
whose owner no longer exists.
$ find . -user root ./data/sales.csv $ find . -type f -perm -o+w ./scripts/backup.sh $ find scripts -perm 755 scripts scripts/deploy.sh $ find . -type f -perm /u+x ./scripts/backup.sh ./scripts/deploy.sh
Combining tests
Tests written one after another must all be true; this is an implicit AND.
The -o operator means OR, and ! (or
-not) negates a test.
find . -name "*.py" -name "test_*" find . -name "*.md" -o -name "*.json" find . -type f ! -name "*.py" -path "./src/*" find . -type f \( -name '*.log' -o -name '*.gz' \)
The third command lists the files in src that are not Python
sources. Parentheses group tests; they must be escaped or quoted, because
they are special to the shell. In the last command, -type f
applies to both alternatives.
$ find . -name "*.py" -name "test_*" ./tests/test_app.py ./tests/test_utils.py $ find . -name "*.md" -o -name "*.json" ./README.md ./docs/guide.md ./src/config.json $ find . -type f ! -name "*.py" -path "./src/*" ./src/config.json ./src/__pycache__/app.cpython-311.pyc ./src/widget.js $ find . -type f \( -name '*.log' -o -name '*.gz' \) ./logs/app.log ./logs/archive/2026-08.log.gz ./logs/error.log
Operator precedence
AND binds more tightly than OR. This matters when an action such as
-print is part of the expression.
find . -name "*.md" -o -name "*.json" -print find . \( -name '*.md' -o -name '*.json' \) -print
The first command is read as -name "*.md" -o (-name "*.json" -print),
so only the JSON file is printed. When the expression contains no action,
find adds -print around the whole expression, which
is why the earlier example without -print worked. With
parentheses, the action applies to both alternatives.
$ find . -name "*.md" -o -name "*.json" -print ./src/config.json $ find . \( -name '*.md' -o -name '*.json' \) -print ./README.md ./docs/guide.md ./src/config.json
Excluding directories
The -prune action prevents find from descending
into a directory. It is used to skip large or irrelevant trees such as
node_modules or .git.
find . -name node_modules -prune -o -name "*.js" -print find . \( -name node_modules -o -name .git -o -name __pycache__ \) -prune -o -type f -print | sort
The idiom reads: if the name is node_modules, prune it;
otherwise, if the name matches *.js, print it. The explicit
-print is required, otherwise the pruned directory itself would be
printed as well. Unlike -not -path "./node_modules/*", which
still walks through the directory and filters the results, pruning skips
the directory entirely and is much faster.
$ find . -name "*.js" ./src/widget.js ./node_modules/lodash/index.js $ find . -name node_modules -prune -o -name "*.js" -print ./src/widget.js $ find . \( -name node_modules -o -name .git -o -name __pycache__ \) -prune -o -type f -print | sort ./data/empty.csv ./data/sales.csv ./docs/guide.md ./docs/Guide-old.MD ./docs/images/diagram.svg ./docs/images/logo.png ./.env ./logs/app.log ./logs/archive/2026-08.log.gz ./logs/error.log ./README.md ./scripts/backup.sh ./scripts/deploy.sh ./setup.py ./src/app.py ./src/config.json ./src/utils.py ./src/widget.js ./tests/test_app.py ./tests/test_utils.py
Custom output
The -printf action prints formatted information about each
file, and -ls prints a listing similar to ls -l.
find src -type f -printf '%f %s bytes\n' find logs -type f -printf '%TY-%Tm-%Td %p\n' find scripts -ls
%f is the file name, %s the size in bytes,
%p the path, and %T followed by a letter formats
the modification time, here the year, month, and day. Other directives
include %u (owner), %m (octal mode), and
%d (depth). Unlike -print, -printf
adds no newline, so we write \n.
$ find src -type f -printf '%f %s bytes\n' config.json 16 bytes app.cpython-311.pyc 4000 bytes utils.py 47 bytes widget.js 18 bytes app.py 46 bytes $ find logs -type f -printf '%TY-%Tm-%Td %p\n' 2026-09-13 logs/app.log 2026-08-14 logs/archive/2026-08.log.gz 2026-10-21 logs/error.log $ find scripts -ls 2628397 4 drwxr-xr-x 2 jano jano 4096 Oct 13 13:02 scripts 2628427 4 -rwxrwxrwx 1 jano jano 24 Oct 13 13:02 scripts/backup.sh 2628426 4 -rwxr-xr-x 1 jano jano 24 Oct 13 13:02 scripts/deploy.sh
Running commands with -exec
The -exec action runs a command for the found files. The
{} placeholder is replaced with the file path.
find . -name "*.py" -exec wc -l {} \;
find . -name "*.py" -exec wc -l {} +
When the command ends with \;, it runs once per file. When it
ends with +, find passes as many files as possible
to a single invocation, which is much faster for many files; here
wc also prints a total. The semicolon is escaped so that the
shell passes it to find.
$ find . -name "*.py" -exec wc -l {} \;
1 ./setup.py
2 ./tests/test_app.py
2 ./tests/test_utils.py
2 ./src/utils.py
3 ./src/app.py
$ find . -name "*.py" -exec wc -l {} +
1 ./setup.py
2 ./tests/test_app.py
2 ./tests/test_utils.py
2 ./src/utils.py
3 ./src/app.py
10 total
Combining find and grep
find selects files and grep
searches their contents, which is a common combination.
find . -name "*.py" -exec grep -l TODO {} +
find . -name "*.py" -exec grep -Hn TODO {} +
The first command lists the Python files that contain TODO; the
second prints the matching lines with file names and line numbers. For
simple cases, grep -rn --include="*.py" TODO . does the same,
but find offers far more ways to select the files.
$ find . -name "*.py" -exec grep -l TODO {} +
./src/utils.py
./src/app.py
$ find . -name "*.py" -exec grep -Hn TODO {} +
./src/utils.py:2: return a + b # TODO: types
./src/app.py:2:# TODO: add logging
-execdir and -ok
The -execdir action runs the command from the directory
containing the file. The -ok action asks for confirmation before
each command.
find . -name "*.sh" -execdir pwd \;
find . -name "*.pyc" -ok rm {} \;
-execdir is safer than -exec for commands that
modify files, because file names cannot be misinterpreted relative to
another directory. With -ok, we answered y and the
compiled file was removed, as the final search confirms.
$ find . -name "*.sh" -execdir pwd \;
/home/jano/work/project/scripts
/home/jano/work/project/scripts
$ find . -name "*.pyc" -ok rm {} \;
< rm ... ./src/__pycache__/app.cpython-311.pyc > ? y
$ find . -name "*.pyc"
Deleting files
The -delete action removes the found files. Always run the same
command without -delete first to see what would be deleted.
find /tmp/oldlogs -type f -mtime +30 find /tmp/oldlogs -type f -mtime +30 -delete
We work on a copy of the logs made with cp -a, which preserves
the timestamps. The log files older than 30 days are removed. Place
-delete at the end of the expression: since actions are part of
the expression, find . -delete -name "*.log" would delete
everything before testing the name.
$ cp -a logs /tmp/oldlogs $ find /tmp/oldlogs -type f -mtime +30 /tmp/oldlogs/app.log /tmp/oldlogs/archive/2026-08.log.gz $ find /tmp/oldlogs -type f -mtime +30 -delete $ find /tmp/oldlogs /tmp/oldlogs /tmp/oldlogs/archive /tmp/oldlogs/error.log
find and xargs
The output of find is often passed to
xargs. File names with spaces require
NUL-separated output.
find docs -name "*.md" | xargs wc -l find docs -name "*.md" -print0 | xargs -0 wc -l
In the first command, xargs splits meeting notes.md
into two words, and wc fails. The -print0 action
separates names with NUL characters, which cannot occur in file names, and
xargs -0 reads them correctly. xargs -P can also run
commands in parallel.
$ touch "docs/meeting notes.md" $ find docs -name "*.md" | xargs wc -l wc: docs/meeting: No such file or directory wc: notes.md: No such file or directory 300 docs/guide.md 300 total $ find docs -name "*.md" -print0 | xargs -0 wc -l 0 docs/meeting notes.md 300 docs/guide.md 300 total
Symbolic links
By default, find does not follow symbolic links. The
-L option follows them, and the -xtype test checks
the type of the link target.
find . -type l find -L . -name "*.log" -type f find . -xtype l
With -L, the link latest.log is treated as the
regular file it points to. find . -xtype l lists broken links:
a link whose target does not exist is still a link after following. The
-xdev option keeps find on one file system.
$ find . -type l ./broken.lnk ./latest.log $ find -L . -name "*.log" -type f ./logs/app.log ./logs/error.log ./latest.log $ find . -xtype l ./broken.lnk
Permission errors
When searching system directories as a regular user, find
reports directories it cannot read.
find /etc -name "shadow*" find /etc -name "shadow*" 2>/dev/null
The error messages go to standard error, so redirecting it to
/dev/null leaves only the results. Note that the matching files
are listed even though we cannot read them; listing a name requires only
read permission on the directory. Use sudo find to search
everywhere.
$ find /etc -name "shadow*" /etc/shadow /etc/shadow- find: ‘/etc/ssl/private’: Permission denied $ find /etc -name "shadow*" 2>/dev/null /etc/shadow /etc/shadow-
Practical recipes
A few useful combinations with other commands.
find . -type f -printf '%s %p\n' | sort -rn | head -n 3 find . -type f -name '*.*' -not -path './.git/*' -not -path './node_modules/*' | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -n 5 find . -type f -mmin -1440 -not -path './.git/*'
The first command lists the three largest files. The second counts files by extension. The third shows files changed in the last 24 hours, which is handy to see what was touched recently.
$ find . -type f -printf '%s %p\n' | sort -rn | head -n 3
1500000 ./docs/images/logo.png
348894 ./data/sales.csv
43893 ./logs/app.log
$ find . -type f -name '*.*' -not -path './.git/*' -not -path './node_modules/*' | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -n 5
5 py
3 md
2 sh
2 log
2 csv
$ find . -type f -mmin -1440 -not -path './.git/*'
./docs/meeting notes.md
./tests/test_utils.py
./src/utils.py
Best practices for find
- Quote patterns: Always quote
-nameand-pathpatterns. - Preview before acting: Run the search without
-deleteor-exec rmfirst. - Use + with -exec: Batch files into fewer command invocations.
- Handle any file name: Use
-print0withxargs -0, or-exec. - Prune large trees: Skip directories such as
.gitandnode_moduleswith-prune. - Mind rounding: Use small units with
-sizeand remember how-mtimecounts days.
Source
In this article, we have used the find command to search for
files by name, type, size, time, owner, and permissions, combined tests with
operators, excluded directories, formatted the output, and acted on the
results with -exec, -delete, and
xargs.
Author
List all Linux tutorials.