How to Create a Bash File Filtering Tool

How to Create a Bash File Filtering Tool

One of the scripts I reach for most often isn’t fancy at all — it’s a simple tool that scans through a directory and filters files based on some criteria: extension, size, modification date, or content. Once you understand the building blocks, you can adapt this pattern to filter almost anything. In this guide, I’ll walk you through building a genuinely useful Bash file filtering tool from the ground up.

What Is a File Filtering Tool?

A file filtering tool scans a set of files (usually within a directory) and selects only the ones matching specific conditions — for example, all .log files larger than 10MB, or all files modified in the last 7 days. Instead of manually scrolling through a folder, you let the script do the sorting for you.

Step 1: Filtering by File Extension

Let’s start simple — a script that lists all files with a specific extension in a given directory.

#!/bin/bash

directory=$1
extension=$2

for file in "$directory"/*."$extension"
do
    if [ -f "$file" ]
    then
        echo "$file"
    fi
done

Sample run:

$ ./filter.sh /home/user/documents txt
/home/user/documents/notes.txt
/home/user/documents/todo.txt

How it works: The script takes two arguments — a directory and an extension — then uses a glob pattern (*.txt) to match files. The -f check makes sure we only report actual files (in case the pattern doesn’t match anything, avoiding a literal “no such file” string being printed).

Step 2: Filtering by File Size

Now let’s build a filter that finds files above a certain size, which is incredibly useful for cleaning up disk space.

#!/bin/bash

directory=$1
min_size_kb=$2

find "$directory" -type f -size +"${min_size_kb}"k

Sample run:

$ ./filter_by_size.sh /var/log 500
/var/log/syslog
/var/log/nginx/access.log

How it works: find is a much more powerful tool than a simple glob loop for this kind of task. The -size +500k option matches files larger than 500 kilobytes. The + sign means “greater than”; you could use - for “less than,” or no sign for “exactly.”

Step 3: Filtering by Modification Date

Finding recently modified (or old, stale) files is another common need.

#!/bin/bash

directory=$1
days=$2

find "$directory" -type f -mtime -"$days"

Sample run:

$ ./filter_by_date.sh /home/user/downloads 7
/home/user/downloads/report.pdf
/home/user/downloads/invoice.docx

How it works: -mtime -7 matches files modified within the last 7 days. Flip the sign (-mtime +7) to instead find files older than 7 days — perfect for identifying stale files to archive or delete.

Step 4: Filtering by Content (Grep-Based Filtering)

Sometimes you don’t care about metadata — you want files that actually contain certain text.

#!/bin/bash

directory=$1
search_term=$2

grep -rl "$search_term" "$directory"

Sample run:

$ ./filter_by_content.sh /home/user/projects "TODO"
/home/user/projects/app.py
/home/user/projects/notes.md

How it works: grep -r searches recursively through the directory, and -l tells grep to print only the filenames that contain a match, rather than every matching line.

Step 5: Combining Multiple Filters into One Tool

Now let’s build something more complete — a single script that combines extension, size, and date filters, controlled with command-line flags.

#!/bin/bash

# Usage: ./file_filter.sh -d DIRECTORY [-e EXTENSION] [-s MIN_SIZE_KB] [-m DAYS]

directory=""
extension=""
min_size=""
days=""

while getopts "d:e:s:m:" opt
do
    case $opt in
        d) directory=$OPTARG ;;
        e) extension=$OPTARG ;;
        s) min_size=$OPTARG ;;
        m) days=$OPTARG ;;
        *) echo "Usage: $0 -d DIRECTORY [-e EXTENSION] [-s MIN_SIZE_KB] [-m DAYS]"
           exit 1 ;;
    esac
done

if [ -z "$directory" ]
then
    echo "Error: Directory (-d) is required."
    exit 1
fi

find_command=(find "$directory" -type f)

if [ -n "$extension" ]
then
    find_command+=(-name "*.$extension")
fi

if [ -n "$min_size" ]
then
    find_command+=(-size +"${min_size}"k)
fi

if [ -n "$days" ]
then
    find_command+=(-mtime -"$days")
fi

echo "Running filter with the following criteria:"
echo "Directory: $directory"
[ -n "$extension" ] && echo "Extension: .$extension"
[ -n "$min_size" ] && echo "Minimum size: ${min_size}KB"
[ -n "$days" ] && echo "Modified within: $days days"
echo "------------------------------------------"

"${find_command[@]}"

Sample run:

$ ./file_filter.sh -d /var/log -e log -s 100 -m 30
Running filter with the following criteria:
Directory: /var/log
Extension: .log
Minimum size: 100KB
Modified within: 30 days
------------------------------------------
/var/log/nginx/access.log
/var/log/syslog

How it works:

  1. getopts parses command-line flags (-d, -e, -s, -m), storing each value in $OPTARG.
  2. We build up a find command as a Bash array (find_command), appending options only for the filters the user actually specified.
  3. Finally, "${find_command[@]}" expands the array back into a properly separated command and executes it.

Using an array here (rather than a plain string) is important — it avoids word-splitting and quoting issues that would otherwise cause the script to break on filenames or paths containing spaces.

Step 6: Adding an Action (Not Just Listing)

A filtering tool becomes even more useful when it can act on the results — for example, moving matched files somewhere else.

#!/bin/bash

directory=$1
extension=$2
destination=$3

mkdir -p "$destination"

find "$directory" -type f -name "*.$extension" -print0 | while IFS= read -r -d '' file
do
    mv "$file" "$destination"
    echo "Moved: $file"
done

Sample run:

$ ./move_filtered.sh /home/user/downloads pdf /home/user/documents/pdfs
Moved: /home/user/downloads/invoice.pdf
Moved: /home/user/downloads/report.pdf

How it works: -print0 and read -r -d '' together handle filenames safely, even ones containing spaces or unusual characters, by using a null byte as the delimiter instead of a newline — this is the safest way to loop over find results in Bash.

How This Works Internally

  • find walks the directory tree recursively at the filesystem level, evaluating each file against the conditions you provide (-name, -size, -mtime), and only outputs paths that satisfy all of them.
  • -mtime N is calculated based on 24-hour periods relative to the current time, using the file’s last-modified timestamp stored in its inode metadata.
  • getopts is a Bash builtin that parses short-form command-line options (like -d value) and automatically populates $OPTARG with the value following each recognized flag, while advancing an internal counter ($OPTIND) so repeated calls process subsequent options correctly.
  • Using find ... -print0 combined with read -d '' avoids the classic pitfall of splitting filenames on whitespace or newlines, since the null byte (\0) can’t legally appear in a filename, making it a safe universal delimiter.

Real-World Use Cases

  • Log rotation cleanup: Find and delete .log files older than 30 days to free up disk space.
find /var/log -type f -name "*.log" -mtime +30 -delete
  • Media file organization: Move all .jpg files from a Downloads folder into a Pictures folder.
find ~/Downloads -type f -name "*.jpg" -exec mv {} ~/Pictures/ \;
  • Finding large files consuming disk space:
find / -type f -size +1G 2>/dev/null
  • Auditing configuration files containing a specific setting:
grep -rl "DEBUG=true" /etc/myapp/

Best Practices

  • Always quote variables holding paths ("$directory", "$file") to handle spaces and special characters safely.
  • Prefer find ... -print0 combined with read -r -d '' over plain for file in $(find ...) loops, which can break on filenames containing spaces or newlines.
  • Provide clear usage instructions (Usage: script.sh -d DIRECTORY ...) so the tool is easy to use correctly.
  • Test destructive filters (like -delete or mv) with a dry run first — just print what would be affected before actually acting on it.

Security Considerations

  • Be extremely careful with any filter that leads to deletion (-delete, rm) — a bug in your filtering logic could delete far more than intended. Always test with -print first before adding -delete.
  • Validate directory paths before running recursive operations on them, especially if the path comes from user input, to avoid accidentally operating on / or other unintended locations.
  • When searching system-wide (find /), redirect permission-denied errors (2>/dev/null) rather than ignoring them silently in a way that hides real problems — log them separately if you need to review what was skipped.

Optimization Tips

  • Let find do as much filtering as possible using its own flags (-size, -mtime, -name) rather than piping results through multiple additional commands like grep or awk, since find‘s built-in filters are evaluated efficiently at the filesystem level.
  • Combine multiple conditions directly in a single find call instead of running find several times and merging the results.
  • For very large directory trees, consider limiting recursion depth with -maxdepth if you don’t need to search every nested subdirectory.

Troubleshooting Common Issues

  • Script finds nothing even though matching files exist — double check your find syntax; -name is case-sensitive by default (use -iname for case-insensitive matching).
  • Filenames with spaces break the loop — switch to find ... -print0 with while IFS= read -r -d '' instead of a plain for loop.
  • “Permission denied” errors cluttering output — redirect stderr with 2>/dev/null, or better, log those errors to a separate file for review.
  • getopts not recognizing flags — make sure each flag that expects a value has a colon after it in the getopts string (e.g., "d:e:s:m:"), and that you’re passing flags in the correct format (-d value, not -d=value).

Frequently Asked Questions

Q: What’s the difference between using find and a plain for loop with globbing for filtering files? A: find supports far richer criteria (size, modification time, permissions, depth) and handles filenames with special characters safely when combined with -print0; a glob-based for loop is simpler but more limited and more fragile with unusual filenames.

Q: How do I make my filtering tool recursive? A: find is recursive by default; a simple for file in dir/* loop is not, unless you add ** globbing (shopt -s globstar) or nest additional loops for subdirectories.

Q: Can I filter files by permission or ownership too? A: Yes — find supports -perm for permissions and -user/-group for ownership, which can be combined with the other filters shown above.

Q: How do I preview what a filter would delete without actually deleting anything? A: Run the find command with -print (or no action flag at all, which prints by default) before adding -delete, so you can review the matched files first.

Common Mistakes to Avoid

  • Using a plain for file in $(find ...) loop, which breaks on filenames containing spaces or newlines.
  • Adding -delete to a find command before verifying the filter is correct with a dry run.
  • Forgetting to quote variables holding file paths, leading to unexpected word-splitting.
  • Not handling the case where no files match the filter, which can cause confusing errors if the result is used elsewhere in the script.

Summary

Building a file filtering tool in Bash is really about combining the right building blocks: find for locating files by metadata (name, size, date), grep for content-based filtering, and getopts for a clean command-line interface. Once you combine these into a single flexible script — using arrays to safely build up find commands and -print0/read -d '' to handle filenames safely — you end up with a genuinely reusable tool you’ll find yourself reaching for again and again.

References

Total
1
Shares

Leave a Reply

Previous Post
How to Start Stop or Restart Nginx

How to Start, Stop, or Restart Nginx

Next Post
How to find the Apache Web Server version

How to find the Apache Web Server version

Related Posts