How to Create a Bash File Finder

How to Create a Bash File Finder

How to Create a Bash File Finder

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

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

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

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

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

Optimization Tips

Troubleshooting

Common Mistakes to Avoid

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.

References

Exit mobile version