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:
getoptsparses command-line flags (-d,-e,-s,-m), storing each value in$OPTARG.- We build up a
findcommand as a Bash array (find_command), appending options only for the filters the user actually specified. - 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
findwalks 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 Nis calculated based on 24-hour periods relative to the current time, using the file’s last-modified timestamp stored in its inode metadata.getoptsis a Bash builtin that parses short-form command-line options (like-d value) and automatically populates$OPTARGwith the value following each recognized flag, while advancing an internal counter ($OPTIND) so repeated calls process subsequent options correctly.- Using
find ... -print0combined withread -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
.logfiles older than 30 days to free up disk space.
find /var/log -type f -name "*.log" -mtime +30 -delete
- Media file organization: Move all
.jpgfiles 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 ... -print0combined withread -r -d ''over plainfor 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
-deleteormv) 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-printfirst 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
finddo as much filtering as possible using its own flags (-size,-mtime,-name) rather than piping results through multiple additional commands likegreporawk, sincefind‘s built-in filters are evaluated efficiently at the filesystem level. - Combine multiple conditions directly in a single
findcall instead of runningfindseveral times and merging the results. - For very large directory trees, consider limiting recursion depth with
-maxdepthif you don’t need to search every nested subdirectory.
Troubleshooting Common Issues
- Script finds nothing even though matching files exist — double check your
findsyntax;-nameis case-sensitive by default (use-inamefor case-insensitive matching). - Filenames with spaces break the loop — switch to
find ... -print0withwhile IFS= read -r -d ''instead of a plainforloop. - “Permission denied” errors cluttering output — redirect stderr with
2>/dev/null, or better, log those errors to a separate file for review. getoptsnot recognizing flags — make sure each flag that expects a value has a colon after it in thegetoptsstring (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
-deleteto afindcommand 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.