If you’ve ever spent ten minutes clicking through folders trying to find a single file, you already know why I decided to build my own file search tool in Bash. Linux gives you find and locate out of the box, but neither of them is exactly friendly when you’re in a hurry or when you want a repeatable, opinionated way of searching across your entire system. So I built a small, focused search tool as a Bash script, and in this article I’m walking you through exactly how I did it — from the absolute basics to the more advanced tricks that make the script genuinely useful in daily work.
By the end of this guide you’ll understand not just how to copy-paste a script, but how every line of it actually works.
Why Build a Custom Search Tool When find Already Exists?
That’s a fair question. find is powerful, but it’s also verbose. I got tired of typing:
find /home/user -iname "*invoice*" -type f -mtime -30
every single time I needed to search for something. I wanted a tool where I could just type:
fsearch invoice
and get sensible results immediately, with color-coded output, file size, and last-modified date. That’s the itch this project scratches.
Prerequisites
Before we start, make sure you have:
- A Linux or macOS machine (WSL works fine on Windows)
- Bash 4.0 or newer (
bash --versionto check) - Basic familiarity with the terminal
find,grep, andstatinstalled (these ship with virtually every distro)
Step 1: Planning the Script’s Behavior
Before writing code, I always sketch out what I want the tool to do. For this search tool, my requirements were:
- Search by filename (partial match, case-insensitive)
- Optionally restrict the search to a specific directory
- Optionally filter by file extension
- Show file size and last modified date in the output
- Handle the “no results found” case gracefully
Step 2: Writing the Basic Script
Let’s start simple and build up from there. Create a new file:
touch fsearch.sh
chmod +x fsearch.sh
Now open it in your editor of choice and add this:
#!/usr/bin/env bash
# fsearch.sh - a simple file search tool
set -euo pipefail
QUERY="$1"
SEARCH_DIR="${2:-.}"
find "$SEARCH_DIR" -iname "*${QUERY}*" -type f 2>/dev/null
Run it like this:
./fsearch.sh report ~/Documents
Breaking Down Every Line
#!/usr/bin/env bash— this is the shebang line. It tells the operating system which interpreter to use to run the script. Usingenv bashinstead of hardcoding/bin/bashmakes the script more portable across systems where Bash might live in a different location.set -euo pipefail— this is one of the most important lines in any serious Bash script.-emakes the script exit immediately if any command fails,-utreats unset variables as errors instead of silently substituting empty strings, and-o pipefailmakes a pipeline fail if any command within it fails, not just the last one.QUERY="$1"— captures the first command-line argument, which is the search term.SEARCH_DIR="${2:-.}"— this is parameter expansion with a default value. If the second argument isn’t supplied, it defaults to.(the current directory).find "$SEARCH_DIR" -iname "*${QUERY}*" -type f— this is the actual search.-inamedoes a case-insensitive name match, the asterisks act as wildcards around the query, and-type frestricts results to regular files (not directories).2>/dev/null— redirects error output (like “Permission denied” messages) to the void so they don’t clutter your results.
Step 3: Adding Better Output Formatting
Raw file paths are fine, but I wanted more context. Let’s enhance it:
#!/usr/bin/env bash
set -euo pipefail
QUERY="${1:-}"
SEARCH_DIR="${2:-.}"
if [[ -z "$QUERY" ]]; then
echo "Usage: $0 <search-term> [directory]"
exit 1
fi
echo "Searching for '$QUERY' in '$SEARCH_DIR'..."
echo "----------------------------------------"
RESULTS=$(find "$SEARCH_DIR" -iname "*${QUERY}*" -type f 2>/dev/null)
if [[ -z "$RESULTS" ]]; then
echo "No files found matching '$QUERY'."
exit 0
fi
while IFS= read -r file; do
size=$(stat -c "%s" "$file" 2>/dev/null || stat -f "%z" "$file" 2>/dev/null)
modified=$(stat -c "%y" "$file" 2>/dev/null || stat -f "%Sm" "$file" 2>/dev/null)
printf "%-50s %10s bytes %s\n" "$file" "$size" "$modified"
done <<< "$RESULTS"
What’s New Here
- The
if [[ -z "$QUERY" ]]block checks whether the query is empty and prints usage instructions if so — this is basic input validation, and every good script should have it. stat -c "%s"retrieves the file size in bytes on Linux, whilestat -f "%z"is the macOS/BSD equivalent — I chained them with||so the script works on both platforms.while IFS= read -r file; do ... done <<< "$RESULTS"loops over each line of the results. SettingIFS=(empty) prevents word-splitting on whitespace, and-rprevents backslash characters from being interpreted, so filenames with spaces or backslashes are handled correctly.printfgives us aligned columns instead of the ragged output you’d get fromecho.
Step 4: Adding Extension Filtering
Sometimes you only want .pdf files or .log files. Let’s add that as an optional third argument:
EXTENSION="${3:-}"
if [[ -n "$EXTENSION" ]]; then
RESULTS=$(find "$SEARCH_DIR" -iname "*${QUERY}*.${EXTENSION}" -type f 2>/dev/null)
else
RESULTS=$(find "$SEARCH_DIR" -iname "*${QUERY}*" -type f 2>/dev/null)
fi
Now you can run:
./fsearch.sh invoice ~/Documents pdf
and it will only return PDF files whose names contain “invoice.”
Step 5: Making It a Global Command
Copy-pasting a script path every time gets old. I like to drop useful scripts into /usr/local/bin so they’re available system-wide:
sudo cp fsearch.sh /usr/local/bin/fsearch
sudo chmod +x /usr/local/bin/fsearch
Now you can just type fsearch invoice from anywhere in your terminal.
Real-World Use Cases
I use this tool constantly for a few specific scenarios:
- Cleaning up downloads folders — quickly finding old installer files or duplicate PDFs before deleting them.
- Locating configuration files — searching for
.confor.yamlfiles across a project when I’ve forgotten exactly where I put something. - Log auditing — combined with the extension filter, I can pull up every
.logfile modified in the last week. - Onboarding new servers — running a quick search across
/etcto confirm certain config files exist before deploying software.
Automating the Search Tool
You can wire this into cron for scheduled reports. For example, to email yourself a daily list of new PDFs added to a shared folder:
0 8 * * * /usr/local/bin/fsearch "" /shared/folder pdf | mail -s "Daily PDF Report" you@example.com
This runs the search every morning at 8 AM and mails you the results.
Security Considerations
A few things I always keep in mind when writing search scripts like this:
- Never run search tools as root unless absolutely necessary. Searching as an unprivileged user avoids accidentally exposing files you shouldn’t have access to in the first place.
- Sanitize your inputs. Although
findhandles most special characters safely, if you ever extend this script to pass the query intoevalor a subshell command, you open yourself up to command injection. Avoidevalentirely in scripts like this. - Watch out for symlink loops. By default
finddoesn’t follow symbolic links, which is the safer choice. Only add-Lif you specifically need to follow links, and be aware it can cause infinite loops on badly configured filesystems. - Redirect stderr carefully. Suppressing “Permission denied” errors with
2>/dev/nullis convenient, but don’t do this while debugging — you might miss a genuine problem.
Optimization Tips
- If you’re searching a huge filesystem repeatedly, consider using
locate(backed by a prebuilt database viaupdatedb) for a first-pass search, then usefindfor precision on a smaller subset. - Limit search depth with
-maxdepthwhen you know results won’t be deeply nested — this can dramatically speed up large searches:
find "$SEARCH_DIR" -maxdepth 3 -iname "*${QUERY}*"
- Use
-pruneto skip directories you never want to search, like.gitornode_modules:
find "$SEARCH_DIR" -path "*/node_modules" -prune -o -iname "*${QUERY}*" -print
Troubleshooting Common Issues
- “Permission denied” spam in the output — this happens when
findtries to enter directories you don’t have access to. Redirect stderr with2>/dev/nullor run with appropriate permissions. - Script says “command not found” — make sure you ran
chmod +xon the script and that its location is in your$PATH. - No results even though the file exists — double check you’re not accidentally restricting by extension, and confirm the search directory is correct. Also remember
findis case-sensitive unless you use-iname. - Script exits unexpectedly — this is often caused by
set -ecombined with a command that returns a non-zero exit code even when nothing is technically wrong, such asgrepfinding no matches. Wrap such commands inifstatements or append|| truewhere appropriate.
Common Mistakes to Avoid
- Forgetting to quote variables (
$QUERYinstead of"$QUERY"), which breaks on filenames with spaces. - Using
lsoutput for scripting instead offind— this is a classic Bash anti-pattern becauselsoutput isn’t reliably parseable. - Not validating user input before running searches, which can lead to confusing errors.
- Hardcoding paths like
/home/usernameinstead of using$HOME, which breaks portability.
Frequently Asked Questions
Can this script search file contents, not just filenames? Not in its current form — it only searches names. To search file contents, combine it with grep -r "$QUERY" "$SEARCH_DIR".
Does this work on macOS? Yes, with the caveat that stat syntax differs slightly between GNU (Linux) and BSD (macOS) — I’ve accounted for that with the fallback logic shown above.
How do I make the search recursive only one level deep? Add -maxdepth 1 to the find command.
Can I search multiple directories at once? Yes — find accepts multiple paths: find dir1 dir2 dir3 -iname "*query*".
Summary
Building your own Bash file search tool isn’t about reinventing find — it’s about wrapping it in exactly the interface and defaults you want. We started with a one-line script and built it up into something with input validation, formatted output, extension filtering, and real error handling. Along the way we covered the reasoning behind set -euo pipefail, safe variable quoting, and cross-platform stat handling — all fundamentals that will serve you well in any Bash project going forward.