There’s a difference between “searching for a file by name” and what I’d call a proper file finder — something that lets me hunt files down by size, type, age, permissions, or content, not just a filename fragment. I built this as a follow-up to my simpler search tool once I realized how often I needed more advanced filtering — things like “find every file over 100MB I haven’t touched in six months” or “find every world-writable file in this directory tree.” This article covers building that more powerful finder from the ground up.
How This Differs From a Simple Search Tool
If you’ve read the Bash File Search Tool article in this series, you’ll notice some overlap — both are built on find. The difference is scope: the search tool is optimized for the common case (searching by name, fast), while this finder tool is built for flexible, multi-criteria filtering: size, age, permissions, ownership, and type, combined in whatever way you need.
Prerequisites
- Bash 4.0+
find,stat, anddu(all standard on virtually every Linux/macOS install)
Step 1: A Flexible Argument-Driven Finder
Rather than positional arguments, this tool works better with named flags, since we’re supporting many optional filters. Let’s build that parsing logic first.
#!/usr/bin/env bash
set -euo pipefail
DIR="."
NAME=""
MIN_SIZE=""
MAX_SIZE=""
OLDER_THAN=""
NEWER_THAN=""
TYPE=""
usage() {
cat <<EOF
Usage: $0 [options]
-d DIR Directory to search (default: current directory)
-n NAME Filename pattern (supports wildcards)
-s SIZE Minimum size (e.g. 100M, 1G)
-S SIZE Maximum size
-o DAYS Older than N days
-w DAYS Newer than (within) N days
-t TYPE f (file), d (directory), l (symlink)
EOF
exit 1
}
while getopts "d:n:s:S:o:w:t:h" opt; do
case "$opt" in
d) DIR="$OPTARG" ;;
n) NAME="$OPTARG" ;;
s) MIN_SIZE="$OPTARG" ;;
S) MAX_SIZE="$OPTARG" ;;
o) OLDER_THAN="$OPTARG" ;;
w) NEWER_THAN="$OPTARG" ;;
t) TYPE="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
Understanding getopts
getopts "d:n:s:S:o:w:t:h"is Bash’s built-in flag parser. Each letter represents an accepted flag; a colon after a letter (liked:) means that flag requires an argument, while a bare letter (likeh) is a standalone toggle flag.- Inside the
whileloop,$optholds the current flag letter and$OPTARGholds its argument value (if any) — this is the standard, POSIX-compliant way to parse command-line flags in a shell script, and it’s far more robust than manually parsing$1,$2, etc. when you have many optional flags. usage()is defined as a function with a heredoc (cat <<EOF ... EOF), which is the cleanest way to print multi-line help text in Bash without a long chain ofechostatements.
Step 2: Building the find Command Dynamically
Now let’s translate our parsed options into an actual find invocation, built up piece by piece as a Bash array:
FIND_ARGS=("$DIR")
[[ -n "$TYPE" ]] && FIND_ARGS+=(-type "$TYPE")
[[ -n "$NAME" ]] && FIND_ARGS+=(-iname "$NAME")
[[ -n "$MIN_SIZE" ]] && FIND_ARGS+=(-size "+${MIN_SIZE}")
[[ -n "$MAX_SIZE" ]] && FIND_ARGS+=(-size "-${MAX_SIZE}")
[[ -n "$OLDER_THAN" ]] && FIND_ARGS+=(-mtime "+${OLDER_THAN}")
[[ -n "$NEWER_THAN" ]] && FIND_ARGS+=(-mtime "-${NEWER_THAN}")
find "${FIND_ARGS[@]}" 2>/dev/null
What’s Happening Here
FIND_ARGS=("$DIR")initializes our array with just the search directory as the first element, since that’s always the first positional argumentfindexpects.- Each
[[ -n "$X" ]] && FIND_ARGS+=(...)line conditionally appends flags to the array only if the user actually supplied that option — this is a compact Bash idiom combining a test and an action on one line using&&. -size "+${MIN_SIZE}"— the+prefix means “greater than,” matchingfind‘s own size syntax (e.g.+100Mmeans files larger than 100 megabytes). Similarly,-size "-${MAX_SIZE}"with a-prefix means “less than.”-mtime "+${OLDER_THAN}"means “modified more than N days ago,” and-mtime "-${NEWER_THAN}"means “modified less than N days ago” — this mirrors the same+/-convention used for size.find "${FIND_ARGS[@]}"expands our dynamically built array into the actual command, with each element correctly quoted as a separate argument — critical for handling patterns or paths containing spaces.
Step 3: Adding Permission-Based Filtering
Finding files with dangerous permissions (like world-writable files) is a genuinely useful security auditing feature:
PERM_FILTER="${PERM_FILTER:-}"
case "$PERM_FILTER" in
world-writable)
FIND_ARGS+=(-perm -0002)
;;
world-readable)
FIND_ARGS+=(-perm -0004)
;;
setuid)
FIND_ARGS+=(-perm -4000)
;;
esac
-perm -0002 uses find‘s permission-matching syntax: the leading - means “all of these permission bits must be set” (as opposed to + in older find versions, or no prefix for an exact match). 0002 is the octal representation of “write permission for others” — a classic security red flag when found on sensitive files.
Step 4: Formatting Output With Details
Raw paths are fine, but for a finder tool I want size, permissions, and modified date visible immediately:
echo "Search results:"
echo "---------------"
find "${FIND_ARGS[@]}" 2>/dev/null | while IFS= read -r path; do
if [[ -e "$path" ]]; then
perms=$(stat -c "%A" "$path" 2>/dev/null || stat -f "%Sp" "$path" 2>/dev/null)
size=$(du -h "$path" 2>/dev/null | cut -f1)
modified=$(stat -c "%y" "$path" 2>/dev/null | cut -d'.' -f1 || stat -f "%Sm" "$path" 2>/dev/null)
printf "%-10s %-8s %-20s %s\n" "$perms" "$size" "$modified" "$path"
fi
done
stat -c "%A" prints permissions in the familiar rwxr-xr-x style on Linux, with stat -f "%Sp" as the macOS/BSD fallback — the same cross-platform pattern we’ve used consistently throughout this series.
Full Combined Script
#!/usr/bin/env bash
set -euo pipefail
DIR="."
NAME=""
MIN_SIZE=""
MAX_SIZE=""
OLDER_THAN=""
NEWER_THAN=""
TYPE=""
PERM_FILTER=""
usage() {
cat <<EOF
Usage: $0 [options]
-d DIR Directory to search (default: .)
-n NAME Filename pattern
-s SIZE Minimum size (e.g. 100M)
-S SIZE Maximum size
-o DAYS Older than N days
-w DAYS Newer than N days
-t TYPE f, d, or l
-p PERM world-writable, world-readable, setuid
EOF
exit 1
}
while getopts "d:n:s:S:o:w:t:p:h" opt; do
case "$opt" in
d) DIR="$OPTARG" ;;
n) NAME="$OPTARG" ;;
s) MIN_SIZE="$OPTARG" ;;
S) MAX_SIZE="$OPTARG" ;;
o) OLDER_THAN="$OPTARG" ;;
w) NEWER_THAN="$OPTARG" ;;
t) TYPE="$OPTARG" ;;
p) PERM_FILTER="$OPTARG" ;;
h|*) usage ;;
esac
done
FIND_ARGS=("$DIR")
[[ -n "$TYPE" ]] && FIND_ARGS+=(-type "$TYPE")
[[ -n "$NAME" ]] && FIND_ARGS+=(-iname "$NAME")
[[ -n "$MIN_SIZE" ]] && FIND_ARGS+=(-size "+${MIN_SIZE}")
[[ -n "$MAX_SIZE" ]] && FIND_ARGS+=(-size "-${MAX_SIZE}")
[[ -n "$OLDER_THAN" ]] && FIND_ARGS+=(-mtime "+${OLDER_THAN}")
[[ -n "$NEWER_THAN" ]] && FIND_ARGS+=(-mtime "-${NEWER_THAN}")
case "$PERM_FILTER" in
world-writable) FIND_ARGS+=(-perm -0002) ;;
world-readable) FIND_ARGS+=(-perm -0004) ;;
setuid) FIND_ARGS+=(-perm -4000) ;;
esac
echo "Search results:"
echo "---------------"
find "${FIND_ARGS[@]}" 2>/dev/null | while IFS= read -r path; do
perms=$(stat -c "%A" "$path" 2>/dev/null || stat -f "%Sp" "$path" 2>/dev/null)
size=$(du -h "$path" 2>/dev/null | cut -f1)
printf "%-11s %-8s %s\n" "$perms" "$size" "$path"
done
Example usage:
./ffinder.sh -d /var/log -S 50M -o 30
./ffinder.sh -d /var/www -p world-writable
./ffinder.sh -n "*.bak" -o 90
Real-World Use Cases
- Disk cleanup audits — finding every file over a certain size that hasn’t been touched in months.
- Security audits — hunting for world-writable or setuid files across a system, a classic hardening check.
- Log management — finding logs older than your retention policy for cleanup or archival.
- Storage forensics — identifying which specific files are consuming unexpected disk space on a server.
Automation Ideas
Weekly report of large, old files as cleanup candidates:
0 9 * * 1 /usr/local/bin/ffinder.sh -d /home -S 500M -o 180 | mail -s "Weekly Large File Report" you@example.com
Security Considerations
- Permission-scanning features expose sensitive information about your filesystem — restrict who can run this tool and where its output is sent, especially the
world-writable/setuidaudit modes. - Never automatically delete files found by this tool without a human review step first — a finder should inform decisions, not make destructive ones on its own.
- Setuid file audits are genuinely security-critical: unexpected setuid binaries are a common privilege escalation vector, so treat any unexpected results from
-p setuidas worth investigating immediately.
Optimization Tips
- Combine multiple filters in a single
findinvocation (as we do) rather than chaining separatefindcalls with pipes — this avoids redundant filesystem traversal. - Use
-maxdepthwhen you know you don’t need to search deeply nested subdirectories, which can dramatically cut search time on large trees. - For repeated searches against a mostly-static filesystem, consider building a
locatedatabase withupdatedband usinglocatefor the initial fast pass, thenfindfor precise filtering.
Troubleshooting
-sizefilter doesn’t seem to match expected files — rememberfind‘s size units default to 512-byte blocks unless you specify a suffix likeMorG; always include the unit suffix to avoid confusion.- Permission filter returns nothing on a system you know has issues — double-check you’re running with sufficient privileges to traverse the directories in question; permission-denied directories are silently skipped.
- Script errors with “invalid option” —
getoptsrequires flags to come before any non-flag arguments; make sure you’re not mixing positional arguments into the flag list.
Common Mistakes to Avoid
- Forgetting
find‘s default size unit is 512-byte blocks, not bytes, if you omit a suffix. - Running permission audits as an unprivileged user and assuming a clean result means the whole system is clean — you may simply lack visibility into some directories.
- Not testing filter combinations individually before combining several at once, making it hard to debug why a search returns nothing.
Frequently Asked Questions
Can I search by owner or group? Yes — extend the script with -user and -group flags mapped to find‘s native -user USERNAME and -group GROUPNAME options, following the same pattern used for the other filters.
How do I search file contents in addition to metadata? Pipe results into grep -l "search term": find ... -type f | xargs grep -l "search term".
Is this safe to run against a live production system? Yes, find itself is read-only and safe. Just be mindful of I/O load on very large filesystems, and avoid running exhaustive searches during peak traffic hours on busy production servers.
Summary
We built a flexible Bash file finder using getopts for clean flag-based argument parsing, dynamically assembling a find command from whichever filters the user actually specifies — by size, age, type, and permissions — with formatted, informative output. The core skill this article reinforces is building command-line tools with proper flag parsing rather than fragile positional arguments, which scales far better once a tool grows past two or three options.