Linux & Bash · Part I · Module 14 · Finding Files and Searching Their Contents
find: Locating Files by Name, Type, Time and Size
Five chapters have asked which files contain something. This one asks which files exist — matched by name, type, age, size or depth, and combined with the ordinary logical operators. The syntax is unusual and the reason to learn it is that nothing else can express the question.
Intermediate19 min readLinuxPart IFundamentalsSearchingfind
Part I · Module 14 · Chapter 14.6 · find: Locating Files by Name, Type, Time and Size
A different question from the last five chapters, and a tool shaped around it.
1. The Shape of the Command
$ find . -name '*.log'
./build/gen.log
./new.log
./old.log
./sub/deep.logfind WHERE CONDITIONS — a starting directory, then the conditions a file must satisfy. Every matching path is printed, recursively, with no depth limit by default.
The order is fixed and catches people: the path comes first. find -name '*.log' . is wrong.
Conditions combine by simply writing them one after another:
$ find . -type f -name '*.log' -size +1kAdjacent conditions mean "and". A file must be a regular file, and be named *.log, and be over a kilobyte.
2. By Name
find . -name '*.log' # case-sensitive
find . -iname '*.LOG' # case-insensitive
find . -name 'run_*' # prefix
find . -name '*_tb.sv' # suffixThe pattern is a glob (Chapter 3.5), not a regular expression — the same language --include uses in Chapter 14.4, and a different one from Chapter 14.2. * means any run of characters here.
-name matches the base name only. To match against the whole path, use -path:
find . -path '*/rtl/*.v' # .v files under any rtl directory
find . -path '*/build/*' # anything under a build directoryAnd -regex matches the whole path with a regular expression, in the basic dialect by default (Chapter 14.3) — find -E on BSD and -regextype on GNU switch dialects. It is rarely worth it; -path covers most of what people reach for it for.
3. By Type
$ find . -type f | wc -l
6
$ find . -type d
.
./build
./sub| Type | Matches |
|---|---|
f | A regular file |
d | A directory |
l | A symbolic link |
p | A named pipe |
s | A socket |
-type f belongs in almost every find you write. Without it, a directory named logs.log matches -name '*.log', and anything you then do to the results treats a directory as a file.
-type l is the one for auditing links (Chapter 6.3):
find . -type l # every symbolic link
find . -type l ! -exec test -e {} \; -print # the dangling ones4. By Time
The most useful conditions and the most confusing arithmetic.
$ find . -name '*.log' -mtime -1
./build/gen.log
./new.log
./sub/deep.log
$ find . -name '*.log' -mtime +30
./old.log| Written | Means |
|---|---|
-mtime -1 | Modified less than 1 day ago |
-mtime +30 | Modified more than 30 days ago |
-mtime 7 | Modified exactly 7 days ago — that 24-hour window |
The three timestamps (Chapter 2.5 met them):
| Condition | Timestamp |
|---|---|
-mtime / -mmin | Modified — contents changed |
-atime / -amin | Accessed — contents read |
-ctime / -cmin | Changed — metadata changed, e.g. permissions |
-mtime is what you want nearly always. -atime is unreliable because many systems mount filesystems with access-time updates disabled for performance.
5. By Size
$ find . -size +100k -type f
./big.binThe suffix is the unit, and the sign is the comparison:
| Suffix | Unit |
|---|---|
c | Bytes |
k | Kilobytes |
M | Megabytes |
G | Gigabytes |
| (none) | 512-byte blocks |
find . -size +100M # larger than 100 megabytes
find . -size -1k # smaller than 1 kilobyte
find . -size 0 # exactly zero blocks — but see below
find . -empty # empty files or directories6. Combining Conditions
$ find . -maxdepth 1 -type f ! -name '*.log'
./big.bin
./recent.txt| Operator | Means |
|---|---|
| (adjacent) | And — -a is the explicit form |
-o | Or |
! | Not |
\( \) | Grouping |
$ find . -maxdepth 1 \( -name '*.txt' -o -name '*.bin' \)
./big.bin
./recent.txtThe parentheses need escaping because they are shell metacharacters (Chapter 9.3) — \( and \), or '(' and ')'. Without them the operators bind in a way you did not intend, because -o has lower precedence than the implicit and.
find . -name '*.log' -o -name '*.tmp' -delete # DANGEROUS — see below
find . \( -name '*.log' -o -name '*.tmp' \) -delete # correct7. Controlling the Walk
$ find . -maxdepth 1 -name '*.log'
./new.log
./old.log-maxdepth N limits how deep it descends and -mindepth N skips the shallow levels. -maxdepth 1 means "this directory only", which is the common one.
Both must appear before other conditions to work reliably — they are options rather than tests, and GNU find warns when they appear late.
Pruning
$ find . -name build -prune -o -name '*.log' -print
./new.log
./old.log
./sub/deep.log-prune tells find not to descend into a matching directory. The build tree is skipped entirely rather than searched and filtered.
The shape is always the same and worth memorising rather than deriving:
find . -name DIR -prune -o CONDITIONS -printThree things make it work:
-prunesucceeds, so without the-oeverything would stop there.-omeans the rest only applies to things that were not pruned.-printis required. Normallyfindprints by default, but adding any action — and-pruneis an action — switches the default off for the whole expression.
Several at once:
find . \( -name .git -o -name build -o -name work \) -prune -o -name '*.v' -print8. What It Prints
By default find prints each matching path, one per line. Three other actions:
find . -name '*.log' -print # the default, stated explicitly
find . -name '*.log' -print0 # separated by null bytes — Chapter 14.7
find . -name '*.log' -ls # a long listing, like ls -l
find . -name '*.log' -printf '%s\t%p\n' # chosen fields — GNU only-printf is GNU-only and absent from BSD find. It is genuinely useful when available — -printf '%s\t%p\n' gives size and path, tab-separated, which is Chapter 13.1's machine-readable format rather than something to parse.
The portable equivalent is -exec stat, which Chapter 14.7 covers.
9. In Practice
# the one you will type most
find . -type f -name '*.sv'
# this directory only
find . -maxdepth 1 -type f -name '*.log'
# recently changed source, skipping the build tree
find . -name build -prune -o -type f -name '*.v' -mmin -60 -print
# large files eating the disk
find /scratch -type f -size +100M -ls 2>/dev/null
# everything a run produced, by comparison with a marker
touch /tmp/run.stamp
./run_sim.sh
find . -newer /tmp/run.stamp -type f
# empty files, which usually mean a failed step
find runs/ -type f -empty
# dangling symbolic links
find . -type l ! -exec test -e {} \; -print
# every directory containing a filelist
find . -name 'filelist.f' -exec dirname {} \;The marker-file pattern in the middle is the one worth adopting. touch a stamp, do the work, find -newer answers "what did that just produce?" exactly, with none of -mtime's arithmetic.
Practice
mkdir -p /tmp/fd/{sub,build} && cd /tmp/fd
touch -t 202501010000 old.log
touch new.log recent.txt sub/deep.log build/gen.log
dd if=/dev/zero of=big.bin bs=1k count=200 2>/dev/null
ln -s nowhere dangling.link
touch empty.txt1 — By name and type.
find . -name '*.log'
find . -type f | wc -l
find . -type d
find . -type l2 — Hit the quoting trap.
find . -maxdepth 1 -name '*.log'
find . -maxdepth 1 -name *.logTwo results versus one. Explain it before reading §2 again.
3 — Limit the depth.
find . -name '*.log'
find . -maxdepth 1 -name '*.log'4 — By size, and meet the rounding.
find . -size +100k -type f
find . -size -1k -type f
find . -size -1024c -type f
find . -type f -emptyThe second returns nothing. The third and fourth find the small files.
5 — By time.
find . -name '*.log' -mtime -1
find . -name '*.log' -mtime +30
find . -mmin -5 -type f
touch /tmp/fd/marker; sleep 1; touch brandnew.txt
find . -newer /tmp/fd/marker -type f6 — Combine with not and or.
find . -maxdepth 1 -type f ! -name '*.log'
find . -maxdepth 1 \( -name '*.txt' -o -name '*.bin' \)7 — Prune rather than filter.
find . -name '*.log'
find . -name build -prune -o -name '*.log' -print
find . -name '*.log' ! -path '*/build/*'The last two agree; only one avoids walking build.
8 — Watch the -o precedence trap, safely.
find . -maxdepth 1 -name '*.log' -o -name '*.txt' -print
find . -maxdepth 1 \( -name '*.log' -o -name '*.txt' \) -printThe first prints only the .txt files with their paths — the .log branch has no action attached.
9 — Find the dangling link.
find . -type l ! -exec test -e {} \; -print10 — Clean up. cd /tmp && rm -rf /tmp/fd
Check Your Understanding
1. Why must -name '*.log' be quoted, and why does the failure depend on where you are standing?
2. What does -mtime +1 actually match, and what is the better way to say "in the last day"?
3. find . -size -1k returns nothing on a directory full of small files. Why?
4. find . -name '*.log' -o -name '*.tmp' -delete — what does it delete?
5. What is the difference between -prune and ! -path '*/build/*'?
6. Why does -prune need an explicit -print on the other branch?
10. Remember
find WHERE CONDITIONS— the path comes first, and adjacent conditions mean and.- It asks the filesystem directly, which is why it can express age, size and type where parsing
lscannot. - Quote every
-namepattern. Unquoted, the shell expands it first and the bug depends on where you were standing. -namematches the base name;-pathmatches the whole path.-type fbelongs in almost everyfindyou write.-mtimecounts whole days rounded down, so-mtime +1skips the 24-to-48-hour band. Use-mminfor short spans.-newer markeris the exact way to ask "what changed since?" Touch a stamp, do the work, compare.-sizeneeds a suffix — a bare number means 512-byte blocks — and rounds up, so-size -1kmatches nothing.-emptyis the right test for empty, not-size 0.!negates,-ois or, and\( \)groups — the parentheses need escaping.-owith an action is dangerous without grouping, because the implicit and binds tighter.-maxdepth 1means this directory only, and depth options must come first.- Prune, do not filter.
-prunenever enters the directory;! -pathwalks it and discards. - Any action switches off the default print, which is why the prune idiom needs
-print. -printfis GNU-only.- The
-mtimeoff-by-one stops being academic whenfindfeeds a deletion — Chapter 22.4 §5 measures a purge rule that keeps the file it was written to remove. - A generated filelist is a
findaway, and-printfis a GNU extension — Chapter 26.3 measures it failing in a script while working at the prompt, because the prompt'sfindisbfs.
Finding the files is half of it. The next chapter runs something on them — safely, including the names with spaces in, which is where the obvious version quietly breaks.
Where this fits
Part of the Linux & Bash curriculum.
