Find Mastery

Overview

find recursively searches directory trees for files matching criteria. It’s essential for system administration, security auditing, and automation.

Understanding find’s Structure

From find --help:

find [-H] [-L] [-P] [-Olevel] [-D debugopts] [path...] [expression]
Component Description

path…​

Where to search. Defaults to . (current directory). This is why your search fails if you’re in the wrong directory!

expression

What to find. Combines: options, tests, and actions.

Expression Components (from man page)

Type Purpose Examples

Options

Control search behavior (always true)

-maxdepth, -mindepth, -xdev, -mount

Tests

Filter files (return true/false)

-name, -type, -mtime, -size, -perm

Actions

Do something with matches

-print, -delete, -exec, -printf

Operators

Combine expressions

-a (AND), -o (OR), ! (NOT), \( \) (grouping)

Options (-maxdepth) must come BEFORE tests (-name). This is why find -name "*.log" -maxdepth 2 fails!

Basic Syntax

find [path...] [expression]
find /var/log -name "*.log"

Troubleshooting: Why find Doesn’t Find

Problem 1: Wrong Starting Directory

The most common issue - you’re searching in the wrong place.

# You're in domus-captures but searching for a file in domus-ise-linux
pwd
# /home/user/atelier/_bibliotheca/domus-captures

find . -name "linux-ad-auth-dacl*"
# Returns nothing - file doesn't exist here!

# Solution: Search from the right directory
find /home/user/atelier/_bibliotheca/domus-ise-linux -name "linux-ad-auth-dacl*"
# Found!

# Or search all bibliotheca repos
find /home/user/atelier/_bibliotheca -name "linux-ad-auth-dacl*"

Problem 2: Antora xrefs Tell You Where

When you see an xref like:

xref:ise-linux::runbooks/linux-ad-auth-dacl.adoc[Linux AD Auth dACL]

Parse it:

xref:ise-linux::runbooks/linux-ad-auth-dacl.adoc
     ^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |          |
     |          Path under pages/
     |
     Component name = domus-ise-linux repo

So search in:

find ~/atelier/_bibliotheca/domus-ise-linux -name "linux-ad-auth-dacl*"

Problem 3: maxdepth Too Shallow

# Won't find files buried deep
find . -maxdepth 3 -name "*.adoc"

# Antora structure is typically 7+ levels deep:
# docs/asciidoc/modules/ROOT/pages/runbooks/file.adoc
#  1     2        3       4     5       6       7

# Use -maxdepth 10 or omit it entirely
find . -name "*.adoc"

Problem 4: Case Sensitivity

# Won't match README.adoc
find . -name "readme*"

# Use -iname for case-insensitive
find . -iname "readme*"

Problem 5: Quotes Matter

# Shell expands *.log BEFORE find sees it (wrong)
find . -name *.log

# Quotes prevent shell expansion (correct)
find . -name "*.log"

Search All Domus Repos

# Define search root
BIBLIOTHECA="$HOME/atelier/_bibliotheca"

# Find file across all repos
find "$BIBLIOTHECA" -name "linux-ad-auth-dacl*"

# Find all nav.adoc files
find "$BIBLIOTHECA"/domus-* -name "nav.adoc"

# Find all antora.yml files
find "$BIBLIOTHECA"/domus-* -name "antora.yml" -type f

Search Specific Repos

# Multiple specific repos
find ~/atelier/_bibliotheca/domus-{ise-linux,infra-ops,linux-ops} -name "*.adoc" | wc -l

# Exclude build directories
find ~/atelier/_bibliotheca/domus-docs -name "*.adoc" -not -path "*/build/*"

Quick Repo File Counts

# Count .adoc files per repo
for repo in ~/atelier/_bibliotheca/domus-*/; do
  count=$(find "$repo" -name "*.adoc" -type f | wc -l)
  printf "%-30s %d\n" "$(basename "$repo")" "$count"
done

Path Options

# Current directory
find . -name "*.txt"

# Specific directory
find /var/log -name "*.log"

# Multiple directories
find /home /tmp -name "*.conf"

# All mounted filesystems
find / -name "pattern" 2>/dev/null

Search by Name

Basic Name Matching

# Exact name
find . -name "config.yaml"

# Case insensitive
find . -iname "readme*"

# Wildcards
find . -name "*.log"
find . -name "config.*"
find . -name "*backup*"

# Character class
find . -name "log[0-9].txt"

# Single character wildcard
find . -name "file?.txt"

Path Matching

# Match full path
find . -path "*/config/*.yaml"

# Case insensitive path
find . -ipath "*backup*"

# Regex matching (GNU find)
find . -regex ".*\.\(log\|txt\)"

# Extended regex
find . -regextype posix-extended -regex ".*\.(log|txt)"

Search by Type

# Regular files
find . -type f

# Directories
find . -type d

# Symbolic links
find . -type l

# Block devices
find . -type b

# Character devices
find . -type c

# Named pipes (FIFO)
find . -type p

# Sockets
find . -type s

Search by Time

Modification Time (-mtime, -mmin)

# Modified in last 24 hours
find . -mtime 0

# Modified more than 7 days ago
find . -mtime +7

# Modified exactly 7 days ago
find . -mtime 7

# Modified less than 7 days ago
find . -mtime -7

# Modified in last 30 minutes
find . -mmin -30

# Modified more than 60 minutes ago
find . -mmin +60

Access Time (-atime, -amin)

# Accessed in last 24 hours
find . -atime 0

# Not accessed in 90 days
find . -atime +90

Change Time (-ctime, -cmin)

# Inode changed in last 24 hours
find . -ctime 0

# Inode changed more than 7 days ago
find . -ctime +7

Comparison Methods

# Newer than reference file
find . -newer reference_file

# Older than reference file
find . -not -newer reference_file

# Newer than timestamp
find . -newermt "2026-02-01"

# Between dates
find . -newermt "2026-02-01" ! -newermt "2026-02-08"

Search by Size

# Exactly 1MB
find . -size 1M

# More than 100MB
find . -size +100M

# Less than 1KB
find . -size -1k

# Empty files
find . -empty

# Size units: c (bytes), k (KB), M (MB), G (GB)
find . -size +1G

Search by Permissions

Exact Permissions

# Exact match
find . -perm 644
find . -perm 0755

At Least These Permissions (-mode)

# At least readable by all
find . -perm -444

# At least executable by owner
find . -perm -100

# World writable
find . -perm -002

Any of These Permissions (/mode)

# Any of these set
find . -perm /222

# SUID or SGID
find . -perm /6000

Special Permissions

# SUID files
find . -perm -4000

# SGID files
find . -perm -2000

# Sticky bit
find . -perm -1000

# SUID or SGID (security audit)
find / -type f \( -perm -4000 -o -perm -2000 \) 2>/dev/null

Search by Owner

# By username
find . -user admin

# By UID
find . -uid 1000

# By group name
find . -group wheel

# By GID
find . -gid 100

# No owner (orphaned)
find . -nouser

# No group
find . -nogroup

Combining Conditions

AND (default)

# Both conditions must match
find . -name "*.log" -mtime -7

# Explicit AND
find . -name "*.log" -a -mtime -7

OR

# Either condition
find . -name "*.log" -o -name "*.txt"

# With grouping (IMPORTANT: use \( \))
find . \( -name "*.log" -o -name "*.txt" \) -mtime -7

NOT

# Negation
find . -not -name "*.log"
find . ! -name "*.log"

# Exclude directories
find . -not -path "*/node_modules/*"

Complex Example

# Log files modified in last week, not in backup dir
find /var/log \
  -type f \
  -name "*.log" \
  -mtime -7 \
  ! -path "*/backup/*"

Actions

Print (default)

# Default action
find . -name "*.txt"

# Explicit print
find . -name "*.txt" -print

# Null-terminated (for xargs)
find . -name "*.txt" -print0

# Custom format
find . -name "*.txt" -printf "%p %s\n"

Printf Format

Format Description

%p

Full path

%f

Filename only

%h

Directory path

%s

Size in bytes

%k

Size in KB

%m

Permissions (octal)

%M

Permissions (symbolic)

%u

Owner username

%g

Group name

%T+

Modification time

%A+

Access time

%C+

Change time

\n

Newline

\t

Tab

# Custom output format
find . -type f -printf "%M %u:%g %s %p\n"

# CSV output
find . -type f -printf "%p,%s,%T+\n"

Execute Commands

# Execute for each file (one at a time)
find . -name "*.log" -exec rm {} \;

# Execute with multiple files (batch)
find . -name "*.log" -exec rm {} +

# Prompt before execute
find . -name "*.log" -ok rm {} \;

# Execute with dirname
find . -name "*.log" -execdir gzip {} \;

Delete

# Delete files
find /tmp -type f -mtime +30 -delete

# Delete empty directories
find . -type d -empty -delete
Always test with -print first before using -delete!

Depth Control

# Maximum depth
find . -maxdepth 2 -name "*.log"

# Minimum depth
find . -mindepth 2 -name "*.log"

# Exactly at depth 3
find . -mindepth 3 -maxdepth 3 -type f

Filesystem Boundaries

# Stay on same filesystem
find / -xdev -name "*.conf"

# Don't cross mount points
find /home -mount -type f

Production Examples

Cleanup and Maintenance

# Delete old log files
find /var/log -name "*.log" -mtime +30 -delete

# Compress old logs
find /var/log -name "*.log" -mtime +7 -exec gzip {} \;

# Delete empty directories
find . -type d -empty -delete

# Delete temp files older than 24h
find /tmp -type f -mtime +1 -delete

# Remove broken symlinks
find . -xtype l -delete

Security Auditing

# World-writable files
find / -type f -perm -002 2>/dev/null

# World-writable directories (sticky bit ok)
find / -type d -perm -002 ! -perm -1000 2>/dev/null

# SUID/SGID files
find / -type f \( -perm -4000 -o -perm -2000 \) -ls 2>/dev/null

# Files with no owner
find / -nouser -o -nogroup 2>/dev/null

# Recently modified system files
find /etc -type f -mtime -1

# Hidden files in unusual places
find /var /tmp -name ".*" -type f 2>/dev/null

# Executable files in /tmp
find /tmp -type f -perm -100 2>/dev/null

Disk Space Analysis

# Large files (> 100MB)
find / -type f -size +100M -ls 2>/dev/null

# Top 10 largest files
find / -type f -printf "%s %p\n" 2>/dev/null | sort -rn | head -10

# Disk usage by directory
find . -maxdepth 1 -type d -exec du -sh {} \; | sort -rh

# Core dumps
find / -name "core" -o -name "core.*" 2>/dev/null

# Old backup files
find / -name "*.bak" -mtime +30 2>/dev/null

Code and Config Management

# Find all Python files
find . -name "*.py" -type f

# Find configs
find /etc -name "*.conf" -type f

# Search code for pattern
find . -name "*.py" -exec grep -l "TODO" {} \;

# Fix permissions on scripts
find . -name "*.sh" -exec chmod 755 {} \;

# Find files excluding directories
find . -name "*.js" -not -path "*/node_modules/*"

# Count lines of code
find . -name "*.py" -exec wc -l {} + | tail -1

Backup Operations

# Files modified since last backup
find . -newer /var/backup/last_backup -type f

# Copy recently modified
find . -mtime -1 -type f -exec cp {} /backup/ \;

# Create file list for backup
find /home -type f -printf "%p\n" > backup_list.txt

# Sync with rsync
find . -mtime -1 -type f -print0 | xargs -0 -I {} rsync -av {} /backup/

With xargs

# Basic usage
find . -name "*.log" | xargs rm

# Handle spaces in filenames
find . -name "*.log" -print0 | xargs -0 rm

# Parallel execution
find . -name "*.png" -print0 | xargs -0 -P 4 -I {} convert {} {}.jpg

# Limit arguments per command
find . -name "*.txt" -print0 | xargs -0 -n 10 echo

# Run command for each
find . -name "*.sh" | xargs -I {} chmod +x {}

Performance Tips

# Stop at first match
find . -name "target" -print -quit

# Limit depth first
find . -maxdepth 3 -name "*.log"

# Prune directories (more efficient than -not -path)
find . -path ./node_modules -prune -o -name "*.js" -print

# Use locate for known filenames (much faster)
locate "*.conf"

Quick Reference

# By name
find . -name "*.log"                    # Name pattern
find . -iname "*.log"                   # Case insensitive

# By type
find . -type f                          # Files only
find . -type d                          # Directories only

# By time
find . -mtime -7                        # Modified < 7 days
find . -mtime +30                       # Modified > 30 days
find . -mmin -60                        # Modified < 60 min

# By size
find . -size +100M                      # > 100 MB
find . -size -1k                        # < 1 KB
find . -empty                           # Empty files

# By permissions
find . -perm 644                        # Exact mode
find . -perm -100                       # At least executable
find . -perm /002                       # Any writable

# Actions
find . -name "*.log" -delete            # Delete
find . -name "*.sh" -exec chmod +x {} \; # Execute each
find . -name "*.txt" -exec cat {} +     # Execute batch

# Combining
find . -name "*.log" -mtime +7          # AND (default)
find . -name "*.log" -o -name "*.txt"   # OR
find . ! -name "*.log"                  # NOT

Mental Model: How find Works

Evaluation Order

find processes each file through your expression left-to-right:

find . -type f -name "*.log" -mtime -7 -print

For each file:
  1. Is it a regular file? (-type f)
     └─ No? → Skip to next file (short-circuit)
     └─ Yes? → Continue...

  2. Does name match *.log? (-name)
     └─ No? → Skip to next file
     └─ Yes? → Continue...

  3. Modified in last 7 days? (-mtime -7)
     └─ No? → Skip
     └─ Yes? → Continue...

  4. Print it (-print)

Time Notation: The +/- Confusion

-mtime n    Exactly n days ago (24-hour periods)
-mtime +n   MORE than n days ago (older)
-mtime -n   LESS than n days ago (newer)

Timeline:
  <─────────── older ───────────  now
  ├─────┼─────┼─────┼─────┼─────┤
  +4    +3    +2    +1     0    now

  -mtime +3  = files in the "+4" zone (older than 3 days)
  -mtime -3  = files in "0,+1,+2" zones (newer than 3 days)
  -mtime 3   = files in exactly the "+3" zone

Permission Notation: The -// Confusion

-perm 644       Exactly 644 (rw-r--r--)
-perm -644      At LEAST these bits (may have more)
-perm /644      ANY of these bits set

Example: Find writable files
-perm -002      World-writable (all files with o+w)
-perm /222      Writable by anyone (u+w OR g+w OR o+w)

Antora-Specific Patterns

Navigate Antora Structure

# Antora directory structure
# repo/docs/asciidoc/modules/ROOT/
#   ├── pages/          ← Content files
#   ├── partials/       ← Reusable snippets
#   ├── examples/       ← Include examples
#   ├── images/         ← Images
#   ├── attachments/    ← Downloadable files
#   └── nav.adoc        ← Navigation

# Find all page files
find ~/atelier/_bibliotheca/domus-* -path "*/pages/*.adoc" -type f

# Find all nav.adoc files
find ~/atelier/_bibliotheca/domus-* -name "nav.adoc" -path "*/modules/*"

# Find all partials
find ~/atelier/_bibliotheca/domus-* -path "*/partials/*.adoc"

# Find all example files
find ~/atelier/_bibliotheca/domus-* -path "*/examples/*" -type f

Find by Component

# Map xref component to directory
# xref:ise-linux::  → domus-ise-linux
# xref:infra-ops:: → domus-infra-ops
# xref:linux-ops:: → domus-linux-ops
# xref:netapi::    → domus-netapi-docs

# Find all runbooks across components
find ~/atelier/_bibliotheca/domus-* -path "*/runbooks/*.adoc" -type f

# Find file by xref path
# xref:ise-linux::runbooks/linux-ad-auth-dacl.adoc
find ~/atelier/_bibliotheca/domus-ise-linux -path "*runbooks/linux-ad-auth-dacl*"

Content Search Patterns

# Find files containing cross-component xrefs (potential issues)
find ~/atelier/_bibliotheca/domus-* -name "*.adoc" -exec grep -l 'xref:[a-z-]*::' {} \;

# Find files with :toc: directive (shouldn't exist in Antora)
find ~/atelier/_bibliotheca/domus-* -path "*/pages/*.adoc" -exec grep -l '^:toc:' {} \;

# Find D2 diagram source files
find ~/atelier/_bibliotheca/domus-* -name "*.d2" -type f

# Find large images (> 500KB)
find ~/atelier/_bibliotheca/domus-* -path "*/images/*" -size +500k -type f

Build Artifact Cleanup

# Find build directories
find ~/atelier/_bibliotheca/domus-* -type d -name "build"

# Find node_modules (shouldn't be committed)
find ~/atelier/_bibliotheca/domus-* -type d -name "node_modules"

# Find cache directories
find ~/atelier/_bibliotheca/domus-* -type d -name ".cache"

# Dry run: what would be cleaned
find ~/atelier/_bibliotheca/domus-* \( -name "build" -o -name "node_modules" -o -name ".cache" \) -type d -print

# Actually clean (careful!)
# find ~/atelier/_bibliotheca/domus-* \( -name "build" -o -name "node_modules" \) -type d -exec rm -rf {} +

Reading Man Pages: find(1)

The find man page is massive. Here’s how to navigate it:

# Full man page
man find

# Search within man page (press / then type)
/EXAMPLES        # Jump to examples section
/-mtime          # Find -mtime documentation
/-exec           # Find -exec documentation

# Quick reference sections to read:
# EXPRESSIONS    - How tests/actions combine
# OPERATORS      - AND, OR, NOT logic
# EXAMPLES       - Real-world patterns

Key man page sections:

TESTS
    -name pattern      Match filename (shell glob, not regex)
    -iname pattern     Case-insensitive name
    -path pattern      Match full path
    -type c            File type (f=file, d=dir, l=link)
    -mtime n           Modified n*24 hours ago
    -size n[cwbkMG]    File size (c=bytes, k=KB, M=MB, G=GB)
    -perm mode         Permission bits
    -user name         Owner
    -newer file        Newer than reference file

ACTIONS
    -print             Output pathname (default)
    -print0            Null-terminated (for xargs -0)
    -printf format     Formatted output
    -exec cmd {} \;    Run cmd for each file
    -exec cmd {} +     Run cmd with multiple files (faster)
    -delete            Remove file
    -ls                Long listing format

OPERATORS
    \( expr \)         Grouping (escape parens!)
    ! expr             NOT
    expr -a expr       AND (default between tests)
    expr -o expr       OR