find in Depth: Filtering by Time, Size, Type and Running Actions

Intermediate
13 min

find in Depth: Filtering by Time, Size, Type and Running Actions

The searching lesson introduced find by name. In real work you rarely know the name — you know that a file is big, or old, or was changed after a deployment, or belongs to a user who has left. This lesson covers the tests that describe those situations, how to combine them with boolean logic, and how to run commands on every match safely and quickly.

The Shape of a find Command

find walks a directory tree and evaluates an expression against every entry. The expression is made of tests (which match or not) and actions (which do something). The default action is -print.

bash
find <start-dirs> [options] <tests> [actions] find . -type f -name "*.py" # from here, regular files ending in .py find /etc /opt -maxdepth 1 -type d # two start points, one level deep

Always quote patterns like "*.py"; otherwise the shell expands the wildcard before find sees it.

Filtering by Type, Name and Path

| Test | Matches | |---|---| | -type f / -type d / -type l | regular file / directory / symlink | | -name "*.log" | base name, case-sensitive glob | | -iname "readme*" | base name, case-insensitive | | -path "*/tests/*" | the whole path | | -regex ".*/IMG_[0-9]+\.jpg" | whole path, regex (Emacs syntax by default; add -regextype posix-extended for ERE) | | -empty | empty files or directories |

Filtering by Time

Every file carries three timestamps: modification (m, content changed), change (c, metadata such as permissions changed) and access (a, read). find exposes them in days and minutes. A number +n means "more than n", -n means "less than n", and a bare n means "exactly n".

bash
find ~/Downloads -type f -mtime +30 # not modified in the last 30 days find /var/log -type f -mmin -60 # modified within the last hour find /etc -newer /etc/hostname # modified after that reference file find . -type f -newermt "2026-09-01" # modified after a date

-mtime +7 counts whole 24-hour periods, so it matches files at least 8 days old. Use -mmin when precision matters.

Filtering by Size, Owner and Permissions

bash
find / -type f -size +1G 2>/dev/null # larger than 1 GiB (k, M, G suffixes) find . -type f -size -10k # smaller than 10 KiB find /home -user alice -group developers # owned by alice and in group developers find / -type f -perm -4000 2>/dev/null # setuid binaries (security audit) find /var/www -type f -perm /o=w # world-writable files find / -nouser 2>/dev/null # files whose owner no longer exists

The 2>/dev/null discards "Permission denied" noise when scanning system directories as a normal user.

Combining Tests

Tests written next to each other are ANDed. Use -o for OR, ! or -not for NOT, and escaped parentheses for grouping — parentheses must be escaped or quoted because they mean something to the shell.

bash
find . -type f \( -name "*.jpg" -o -name "*.png" \) find . -type f ! -name "*.md" find . -type f \( -name "*.log" -o -name "*.tmp" \) -mtime +14

Pruning Directories

-prune stops find from descending into a directory, which is essential for skipping node_modules, .git or mounted volumes.

bash
find . -path ./node_modules -prune -o -type f -name "*.js" -print find . -type d \( -name .git -o -name node_modules \) -prune -o -type f -print find / -xdev -type f -size +500M # do not cross into other mounted filesystems

When you use -prune you must supply an explicit -print on the other branch, otherwise the pruned directory itself is printed.

Running Actions on Matches

-exec runs a command for each match, with {} replaced by the file path. End the command with \; to run it once per file, or + to batch many files into one invocation (much faster, like xargs).

bash
find . -name "*.orig" -exec rm {} \; # one rm per file find . -name "*.orig" -exec rm {} + # one rm with many arguments find . -name "*.sh" -exec chmod +x {} + find . -type f -name "*.log" -exec gzip {} \; find . -type f -exec grep -l "TODO" {} + # files containing TODO find . -name "*.tmp" -ok rm {} \; # ask before each find . -name "*.tmp" -delete # built-in, fastest find . -type f -printf "%s %p\n" | sort -n | tail -5 # five largest files

-delete must come last in the expression; placed first it deletes everything. Test any destructive command by running it with -print instead of the action first.

Filenames with Spaces

Piping find output to another program breaks when names contain spaces or newlines. Use -print0 with xargs -0 so entries are separated by null bytes, which cannot appear in filenames.

bash
find . -type f -name "*.mp4" -print0 | xargs -0 du -ch | tail -1

Common Mistakes

  • Forgetting quotes around *.log, so the shell expands it to a single existing filename.
  • Writing -mtime -7 when you mean older than a week (that is +7).
  • Using -exec ... \; on thousands of files when + would be hundreds of times faster.
  • Putting -delete or -exec before the tests.
Quick Quiz
Question 1 of 3

Which expression finds files not modified in the last 90 days?

Key Takeaways

  • find tests describe files by type, name, time, size, owner and permissions; adjacent tests are ANDed.
  • +n means more than, -n less than; -mtime counts days and -mmin minutes.
  • Group with \( ... \), negate with !, and skip directories with -prune plus an explicit -print.
  • -exec ... {} + batches matches into one command; -delete is built in and must come last.
  • Use -print0 | xargs -0 whenever filenames might contain spaces.

Next lesson: grep in Depth and Regular Expressions — search inside files with patterns instead of fixed strings.

find in Depth: Filtering by Time, Size, Type and Running Actions - Linux & Command Line | CodeYourCraft | CodeYourCraft