ZetCode

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

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

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

Source

GNU find manual

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Linux tutorials.