How to Create a Bash File Search Tool

How to Create a Bash File Search Tool

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 --version to check)
  • Basic familiarity with the terminal
  • find, grep, and stat installed (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:

  1. Search by filename (partial match, case-insensitive)
  2. Optionally restrict the search to a specific directory
  3. Optionally filter by file extension
  4. Show file size and last modified date in the output
  5. 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. Using env bash instead of hardcoding /bin/bash makes 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. -e makes the script exit immediately if any command fails, -u treats unset variables as errors instead of silently substituting empty strings, and -o pipefail makes 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. -iname does a case-insensitive name match, the asterisks act as wildcards around the query, and -type f restricts 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, while stat -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. Setting IFS= (empty) prevents word-splitting on whitespace, and -r prevents backslash characters from being interpreted, so filenames with spaces or backslashes are handled correctly.
  • printf gives us aligned columns instead of the ragged output you’d get from echo.

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 .conf or .yaml files across a project when I’ve forgotten exactly where I put something.
  • Log auditing — combined with the extension filter, I can pull up every .log file modified in the last week.
  • Onboarding new servers — running a quick search across /etc to 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 find handles most special characters safely, if you ever extend this script to pass the query into eval or a subshell command, you open yourself up to command injection. Avoid eval entirely in scripts like this.
  • Watch out for symlink loops. By default find doesn’t follow symbolic links, which is the safer choice. Only add -L if 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/null is 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 via updatedb) for a first-pass search, then use find for precision on a smaller subset.
  • Limit search depth with -maxdepth when you know results won’t be deeply nested — this can dramatically speed up large searches:
find "$SEARCH_DIR" -maxdepth 3 -iname "*${QUERY}*"
  • Use -prune to skip directories you never want to search, like .git or node_modules:
find "$SEARCH_DIR" -path "*/node_modules" -prune -o -iname "*${QUERY}*" -print

Troubleshooting Common Issues

  • “Permission denied” spam in the output — this happens when find tries to enter directories you don’t have access to. Redirect stderr with 2>/dev/null or run with appropriate permissions.
  • Script says “command not found” — make sure you ran chmod +x on 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 find is case-sensitive unless you use -iname.
  • Script exits unexpectedly — this is often caused by set -e combined with a command that returns a non-zero exit code even when nothing is technically wrong, such as grep finding no matches. Wrap such commands in if statements or append || true where appropriate.

Common Mistakes to Avoid

  • Forgetting to quote variables ($QUERY instead of "$QUERY"), which breaks on filenames with spaces.
  • Using ls output for scripting instead of find — this is a classic Bash anti-pattern because ls output isn’t reliably parseable.
  • Not validating user input before running searches, which can lead to confusing errors.
  • Hardcoding paths like /home/username instead 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.

References

Total
1
Shares

Leave a Reply

Previous Post
How to Create a Bash File Recovery Tool

How to Create a Bash File Recovery Tool

Next Post
How to Create a Bash File Sorting Tool

How to Create a Bash File Sorting Tool

Related Posts