Build a Parallel File Search Tool

Build a recursive CLI file search tool in Rust with parallel processing.

Introduction

30 Second Summary

Every developer has needed to search through dozens of files for a specific piece of text. Maybe it's a TODO you left behind, an error message buried in logs, or a function name scattered across a codebase.

In this project, you will build a command-line file search tool in Rust that recursively searches directories for text patterns and prints matching lines with file paths and line numbers.

What You'll Build

A tool you can point at any directory on your machine and instantly see every line that matches your search pattern, complete with file paths and line numbers, just like running grep -r in a real terminal.

By the end of this project, you'll have:

  • A compiled CLI binary that searches files recursively, printing matches in the familiar filepath:line_number:matching_line format.
  • Robust error handling that gracefully skips unreadable files instead of crashing your program.
  • Parallel file processing using rayon, so searches complete faster by spreading work across your CPU cores.
  • Secret Mission: Add a --ignore-case flag for case-insensitive searching.

Are there any prerequisites?

This project assumes you have basic programming experience. You should already have the Rust toolchain (rustc and Cargo) installed, along with the Zed IDE.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up the Project

Every Rust project starts with Cargo, Rust's package manager and build tool. Cargo scaffolds the project structure, manages dependencies, and compiles your code.

In this step, you will create a new Rust project called rustgrep and add three dependencies that power the search tool: a CLI argument parser, a directory walker, and a parallelism library.

In this step, get ready to:
  • Create a new Rust project with Cargo.
  • Add the clap, walkdir, and rayon dependencies.
  • Verify everything compiles with zero errors.
Create the Rust project
  • Open a terminal panel in Zed by pressing Ctrl+` (macOS and Windows).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your Rust project and move into it by running these commands:
cargo new rustgrep
cd rustgrep

You should see a message confirming that Cargo created a new binary package.

What did cargo new do?

Cargo created a rustgrep folder on your Desktop containing a Cargo.toml (your project manifest) and a src/main.rs file with a starter "Hello, world!" program.

Seeing 'command not found'?

This means Cargo is not on your system PATH. Confirm Rust is installed by checking your Rust toolchain version with rustc --version. If that also fails, reinstall Rust from the official site at rustup.rs.

help me fix 'cargo: command not found' in my terminal

Add the project dependencies

Your search tool needs three external crates (Rust libraries) to handle CLI arguments, directory traversal, and parallel processing.

  • Add all three dependencies by running these commands one at a time:
cargo add clap --features derive
cargo add walkdir
cargo add rayon

Each command prints a confirmation showing the crate and version being added to your project.

What are these three crates?

  • clap parses command-line arguments. The derive feature lets you define your CLI interface as a Rust struct with attributes.
  • walkdir recursively walks directory trees, handling symlinks and depth limits for you.
  • rayon adds data parallelism. It lets you swap a sequential .iter() for a parallel .par_iter() to process files across multiple CPU cores.

Seeing an error when adding a crate?

Make sure you are inside the rustgrep directory (the folder with Cargo.toml). Run ls and confirm you see Cargo.toml listed.

help me fix an error when running cargo add

Verify the project compiles
  • Open the rustgrep project folder in Zed by clicking File in the top menu bar, then Open Folder.
  • Navigate to your Desktop and select the rustgrep folder, then click Open.
  • In the file explorer on the left, click Cargo.toml to open it.

Confirm you see all three dependencies listed under [dependencies].

✔️ I see clap, rayon, and walkdir listed

Your Cargo.toml is correct. Time to compile the project.

ⓧ I'd like to double check the full code

Your Cargo.toml should look exactly like this:

[package]
name = "rustgrep"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4.6.1", features = ["derive"] }
rayon = "1.12.0"
walkdir = "2.5.0"

If anything is different, you can edit Cargo.toml directly to match this. Save the file after making changes.

Before you run the build, what do you think will happen? Will Cargo download anything, or just compile your code?

  • In your terminal, build the project by running:
cargo build

Cargo downloads the crate source code, compiles all three dependencies plus your project, and finishes with no errors. You should see a "Compiling" line for each crate and a final "Finished" message.

Seeing compilation errors?

Make sure your Cargo.toml matches the version shown in the double-check tab above. A typo in the features array or version string is the most common cause.

Also confirm your Rust version is 1.85 or higher by running rustc --version in your terminal. The dependencies require at least 1.85.

help me fix cargo build errors in my Rust project

Your project is scaffolded and all dependencies compile cleanly. Next up, you will write the core search logic that finds matching lines inside a file.

Search a Single File

Your rustgrep tool needs to do one thing well: find lines in a file that match a search pattern. Before tackling directory traversal or command-line arguments, you'll get the core search logic working against a single file.

This step focuses on reading a file, scanning each line for a target word, and printing matches with line numbers. Once this works, everything else in the project builds on top of it.

In this step, get ready to:
  • Create a test file with sample content to search through.
  • Write the core search logic that finds and prints matching lines.
  • Run the program and see results in the terminal.
Create a test file

Before writing any search code, you need something to search. A small text file with a mix of matching and non-matching lines is perfect for testing.

  • Create test.txt in your project root by running this command:
cat > test.txt << 'EOF'
this is a test file
some random line
this line contains hello
another line
hello world is a classic
no match here
EOF

What does this command do?

The cat > test.txt << 'EOF' pattern is called a heredoc. It writes everything between the two EOF markers into test.txt.

  • Confirm the file was created by running:
ls test.txt

You should see test.txt printed in the output.

Don't see the file?

Make sure your terminal is in the rustgrep folder. Run pwd to check your current directory.

Help me troubleshoot this

Write the search logic

With a test file in place, you can write the logic that opens it, scans each line, and prints any line containing your search pattern.

  • In Zed, open src/main.rs and replace all of its contents with this code:
use std::fs;

fn main() {
    // Read the entire test file into a string
    let content = fs::read_to_string("test.txt").unwrap();

    // Iterate over each line with its index
    for (line_num, line) in content.lines().enumerate() {
        // Print the line if it contains the search pattern
        if line.contains("hello") {
            println!("{}:{}", line_num + 1, line);
        }
    }
}

What does this code do?

  • fs::read_to_string("test.txt") reads the entire file into a String. It returns a Result because the read could fail (file missing, no permission).
  • .unwrap() extracts the Ok value from the Result. If the file read fails, the program panics and crashes. This is a deliberate shortcut for now.
  • .lines().enumerate() splits the string into lines and pairs each line with its zero-based index.
  • line_num + 1 converts the zero-based index to a human-friendly 1-based line number.
  • .contains("hello") checks whether the line has the pattern anywhere in it.
  • Save src/main.rs.
Run the search

Your code reads test.txt and prints lines that contain "hello". Before you run it, think about how many lines in your test file actually contain that word.

  • Run your program:
cargo run

You should see two matching lines printed with their line numbers:

3:this line contains hello
5:hello world is a classic

Line 3 and line 5 both contain "hello", so they appear in the output. Every other line is silently skipped.

Seeing a file not found error?

Cargo runs your program from the project root directory. Make sure test.txt is in the rustgrep folder (not inside src/).

Run ls test.txt from your terminal to confirm the file is there.

Help me debug this

✔️ Awesome, I've got everything!

Great. Double check you've saved src/main.rs and confirm both lines appear in your terminal output.

ⓧ I'd like to double check the full code

Here is the complete src/main.rs at this point:

use std::fs;

fn main() {
    let content = fs::read_to_string("test.txt").unwrap();
    for (line_num, line) in content.lines().enumerate() {
        if line.contains("hello") {
            println!("{}:{}", line_num + 1, line);
        }
    }
}

And here is test.txt:

this is a test file
some random line
this line contains hello
another line
hello world is a classic
no match here

Your core search logic works. Right now the file path and pattern are hardcoded, which means you'd have to edit the source every time you want to search something different. Next up, you'll add proper command-line arguments and directory walking to turn this into a real tool.

Add CLI Parsing and Directory Walking

Your hardcoded search on test.txt works, but a real CLI tool needs to accept any pattern and any path as arguments.

In this step, you will wire up proper CLI argument parsing with clap and recursive directory traversal with walkdir. This turns your script into a tool that can search entire directory trees.

In this step, get ready to:
  • Define CLI arguments using clap's derive macros.
  • Extract search logic into a reusable function.
  • Walk entire directory trees with walkdir.
Define the Args struct with clap

Instead of manually parsing command-line arguments with positional indexing, clap lets you declare a Rust struct and derive the parsing logic automatically. Each struct field becomes a CLI argument.

  • In Zed, open src/main.rs.
  • Replace the entire file contents with the following imports and Args struct:
use std::fs;
use std::path::Path;

// clap handles CLI argument parsing via derive macros
use clap::Parser;
// walkdir provides recursive directory traversal
use walkdir::WalkDir;

// Derive Parser to auto-generate arg parsing from this struct
#[derive(Parser, Debug)]
#[command(version, about = "A mini grep tool built in Rust")]
struct Args {
    /// The pattern to search for
    pattern: String,

    /// The path to search (file or directory)
    path: String,
}

What does this code do?

The #[derive(Parser, Debug)] attribute tells clap to generate all the CLI parsing code from your struct definition. Each field becomes a positional argument automatically.

The #[command(version, about = "...")] attribute generates --version and --help flags for free. The /// doc comments on each field become the help text a user sees when they run --help.

Add the search function and directory walking

Now you need a search_file function that takes any path and any pattern, plus a main that uses WalkDir to find every file in a directory tree.

  • Below the Args struct, add the following search_file function and main function:
fn search_file(path: &Path, pattern: &str) {
    // Read the entire file into a string
    let content = fs::read_to_string(path).unwrap();
    // Check each line for the search pattern
    for (line_num, line) in content.lines().enumerate() {
        if line.contains(pattern) {
            // Print in grep-style format: filepath:line_number:line
            println!("{}:{}:{}", path.display(), line_num + 1, line);
        }
    }
}

fn main() {
    // Parse CLI arguments into our Args struct
    let args = Args::parse();

    // Walk the directory tree, unwrapping each entry
    WalkDir::new(&args.path)
        .into_iter()
        .map(|e| e.unwrap())
        .filter(|entry| entry.file_type().is_file())
        .for_each(|entry| search_file(entry.path(), &args.pattern));
}

What does this code do?

  • search_file takes a &Path (a borrowed reference to a file path) and a &str pattern. It reads the file, checks each line, and prints matches in the format filepath:line_number:line.
  • WalkDir::new(&args.path) creates a recursive directory iterator starting at the given path.
  • .map(|e| e.unwrap()) unwraps each directory entry. If WalkDir encounters an error (like a permission-denied directory), this will panic.
  • .filter(|entry| entry.file_type().is_file()) skips directories so you only search actual files.
  • .for_each(...) calls search_file on every file it finds.
  • Save src/main.rs.

Why the -- before arguments?

When you use cargo run, the -- separator tells Cargo that everything after it is meant for your program, not for Cargo itself. Without it, Cargo would try to interpret your search pattern and path as its own flags.

  • Test the tool on your own source code by running:
cargo run -- "fn" ./src

You should see output like ./src/main.rs:17:fn search_file(path: &Path, pattern: &str) { showing every line in your source code that contains fn.

Not seeing any output?

  • Make sure you saved src/main.rs before running. Unsaved changes are not compiled.
  • Check that you have the -- separator between cargo run and your arguments. Without it, Cargo eats the arguments.
  • Confirm the Args struct has exactly two fields: pattern and path. Mismatched field count causes a parsing error.

Why isn't cargo run -- finding matches in my source files?

✔️ Awesome, I've got everything!

Great. Double check you have saved src/main.rs and that it compiles cleanly.

ⓧ I'd like to double check the full code

Your src/main.rs should have these sections in order. Scroll up and confirm each one is present:

  • Imports: std::fs, std::path::Path, clap::Parser, walkdir::WalkDir.
  • The Args struct with #[derive(Parser, Debug)], containing pattern: String and path: String.
  • The search_file function that takes &Path and &str, reads the file with .unwrap(), and prints matches.
  • The main function that calls Args::parse() and uses WalkDir with .map(|e| e.unwrap()) and .for_each(...).
Test on a system directory

The tool works on your own project files. Now try it on a directory where some files have restricted permissions.

Before you run the next command, what do you think will happen when the tool hits a file it cannot read?

  • Run the tool on /etc by running:
cargo run -- "hello" /etc

The program panics. You will see a message containing thread 'main' panicked because .unwrap() hit a file it could not read.

Why did it crash?

The /etc directory contains files owned by root that your user cannot read. When fs::read_to_string fails on one of those files, it returns an Err. Calling .unwrap() on an Err immediately panics the entire program.

This is exactly why Rust forces you to handle Result types explicitly. The .unwrap() shortcut worked fine on test.txt (a file you own), but it is a landmine in any code that touches the real file system. The next step fixes this.

Your search tool now accepts any pattern and any path as arguments, and it can walk entire directory trees. The crash on unreadable files is a real problem. Next up, you will replace .unwrap() with proper error handling so the tool gracefully skips files it cannot read.

Handle Errors Gracefully

In the last step, you saw your tool panic the moment it hit a file it couldn't read. That crash is .unwrap() doing exactly what it's designed to do: aborting when it encounters an Err value.

Rust's type system gives you Result and Option specifically to handle failure cases without resorting to try/catch boilerplate. In this step, you'll replace those panicking .unwrap() calls with proper error handling that skips unreadable files gracefully.

In this step, get ready to:
  • Replace .unwrap() in search_file with a match expression that returns an empty Vec on error.
  • Replace .unwrap() in the WalkDir chain with .filter_map() to skip unreadable entries.
  • Confirm the tool runs on /etc without panicking.
Rewrite search_file to return matches

The search_file function currently calls .unwrap() on the file read and prints results directly. You'll change it to return a Vec<String> of matches and use match to handle the read failure, returning an empty vector instead of panicking.

  • In src/main.rs, find the entire search_file function and replace it with this:
fn search_file(path: &Path, pattern: &str) -> Vec<String> {
    // Try to read the file, return empty Vec if it fails
    let content = match fs::read_to_string(path) {
        Ok(content) => content,
        Err(_) => return Vec::new(),
    };

    let mut matches = Vec::new();

    // Check each line for the pattern
    for (line_num, line) in content.lines().enumerate() {
        if line.contains(pattern) {
            matches.push(format!("{}:{}:{}", path.display(), line_num + 1, line));
        }
    }

    matches
}

What does this code do?

This version makes two key changes from the original:

  • match fs::read_to_string(path) explicitly handles both outcomes. If the read succeeds (Ok), you get the content. If it fails (Err), the function immediately returns an empty Vec instead of crashing.
  • Instead of printing matches directly, the function collects them into a Vec<String> and returns them. This gives the caller control over what to do with the results.
  • Save src/main.rs.
  • Confirm the code compiles by running:
cargo build

The compiler accepts the code. Since main() still calls search_file inside .for_each() and discards the return value, the tool compiles but won't print matches yet.

Seeing a compile error?

  • Check that the function signature reads -> Vec<String> after the closing parenthesis of the parameters.
  • Make sure the match arms end with commas, not semicolons. Ok(content) => content, needs that trailing comma.
  • Confirm the last line of the function is just matches with no semicolon. Adding a semicolon would turn it into a statement instead of a return expression.

Need more help? Why won't my search_file function compile in Rust?

Update main to skip errors and collect results

Now you need to update main() to handle both problems: the WalkDir entries that fail (permission-denied directories, broken symlinks) and the new Vec<String> return type from search_file.

  • In src/main.rs, find the entire main function and replace it with this:
fn main() {
    let args = Args::parse();

    // Collect file paths, skipping any entries that error
    let results: Vec<String> = WalkDir::new(&args.path)
        .into_iter()
        .filter_map(|entry| entry.ok())
        .filter(|entry| entry.file_type().is_file())
        .flat_map(|entry| search_file(entry.path(), &args.pattern))
        .collect();

    for line in results {
        println!("{}", line);
    }
}

What does this code do?

  • .filter_map(|entry| entry.ok()) replaces .map(|e| e.unwrap()). It silently discards any Err entries (permission-denied directories, broken symlinks) and unwraps the Ok values in one step.
  • .flat_map(|entry| search_file(...)) calls search_file on each file and flattens all the returned Vec<String> results into a single iterator of match strings.
  • The final for loop prints each match. This separates data collection from output, which will make parallelism easier in the next step.

Why filter_map instead of a match?

You could write .filter_map(|entry| entry.ok()) as a match inside a .filter(), but filter_map is idiomatic Rust for "keep only the successes and unwrap them in one pass." You'll see this pattern everywhere in Rust codebases.

  • Save src/main.rs.

Before you run this on /etc, think about what you expect to happen. Last time the tool panicked on the first unreadable file. What do you think will happen now?

  • Test the fix by running:
cargo run -- "hello" /etc

The tool completes without panicking. You may see zero matches (if no readable files in /etc contain "hello") or a handful of results from files your user can read. Either way, no crash.

  • Confirm it still works on your own project files by running:
cargo run -- "fn" ./src

You should see matches from your main.rs file, like ./src/main.rs:17:fn search_file(...).

Still seeing a panic?

  • Make sure you replaced the entire main function, not just the .filter_map line. The old .for_each() is gone, replaced by .flat_map() and .collect().
  • Check that search_file uses match on fs::read_to_string(path), not .unwrap(). A single remaining .unwrap() on a file read will still cause a panic.
  • If you see matches printing but also a panic at the end, look for any .unwrap() calls left in your code.

Need more help? My rustgrep tool is still panicking on /etc even after I replaced unwrap

✔️ Awesome, I've got everything!

Great. Double check you've saved src/main.rs and both search_file and main are using the new error-handling versions.

ⓧ I'd like to double check the full code

Your src/main.rs has three sections. Scroll through and confirm each one matches:

  • The imports and Args struct at the top (unchanged from the previous step).
  • The search_file function with -> Vec<String> return type and a match on fs::read_to_string.
  • The main function using .filter_map(|entry| entry.ok()), .flat_map(...), .collect(), and a for loop to print results.

Your tool now handles the real world gracefully. Unreadable files, permission-denied directories, binary blobs. None of them crash the program. Next up, you'll make it fast by searching files in parallel across all your CPU cores.

Add Parallel Search with Rayon

Your tool now handles real-world file systems without panicking. It skips unreadable files gracefully and finds matches across entire directory trees.

The current version processes files one by one. On a directory with thousands of files, this is noticeably slow. Rayon lets you swap sequential iteration for parallel iteration with a single method change. No unsafe code, no manual thread management. This is the "fearless concurrency" that makes Rust unique.

In this step, get ready to:
  • Import rayon's parallel iterator traits.
  • Rewrite main() to collect file paths and process them in parallel.
  • Verify the tool runs faster on large directories.
Add the rayon import

Rayon provides a .par_iter() method that replaces .iter() on any Vec. To use it, you bring rayon's parallel iterator traits into scope.

  • In src/main.rs, find the import block at the top of the file:
use clap::Parser;
use walkdir::WalkDir;
  • Add use rayon::prelude::*; between those two lines, so the imports now look like this:
use clap::Parser;
use rayon::prelude::*;
use walkdir::WalkDir;
Rewrite main for parallel file processing

The sequential version streams files through WalkDir one at a time. For parallel processing, you first collect all file paths into a Vec, then hand that Vec to rayon's .par_iter() which distributes work across CPU cores automatically.

  • In src/main.rs, find the current main() function:
fn main() {
    let args = Args::parse();

    let results: Vec<String> = WalkDir::new(&args.path)
        .into_iter()
        .filter_map(|entry| entry.ok())
        .filter(|entry| entry.file_type().is_file())
        .flat_map(|entry| search_file(entry.path(), &args.pattern))
        .collect();

    for line in results {
        println!("{}", line);
    }
}
  • Replace the entire main() function with this parallel version:
fn main() {
    let args = Args::parse();
    let path = Path::new(&args.path);

    // Collect all file paths into a Vec for parallel processing
    let files: Vec<_> = if path.is_file() {
        vec![path.to_path_buf()]
    } else {
        WalkDir::new(path)
            .into_iter()
            .filter_map(|entry| entry.ok())
            .filter(|entry| entry.file_type().is_file())
            .map(|entry| entry.into_path())
            .collect()
    };

    // Search all files in parallel using rayon
    let results: Vec<Vec<String>> = files
        .par_iter()
        .map(|file| search_file(file, &args.pattern))
        .collect();

    // Print all matches
    for file_matches in results {
        for line in file_matches {
            println!("{}", line);
        }
    }
}

What does this code do?

This new version splits the work into two phases: collecting and processing.

  • Path::new(&args.path) creates a Path reference so you can check if the input is a single file or a directory.
  • The if path.is_file() branch handles single-file searches directly. The else branch walks the directory tree and collects all file paths into a Vec.
  • .par_iter() is rayon's drop-in replacement for .iter(). It distributes the file-searching work across all available CPU cores.
  • Each file produces its own Vec<String> of matches, so results is a Vec<Vec<String>>. The nested loop at the end flattens and prints them.

Why collect into a Vec first?

WalkDir's iterator is inherently sequential because it discovers files by traversing directories one level at a time. You cannot parallelize the discovery itself. By collecting all paths first, you give rayon a known-size collection it can split evenly across threads.

  • Save src/main.rs.
  • Verify the code compiles by running:
cargo build

You should see a successful compilation with no errors.

Seeing a compilation error?

  • Check that use rayon::prelude::*; is on its own line between the clap and walkdir imports.
  • Make sure you replaced the entire main() function, including its opening and closing braces.
  • Confirm search_file still exists above main() and hasn't been accidentally deleted.

Help me debug this compilation error

  • Test it still works on your own source directory by running:
cargo run -- "fn" ./src

You should see matches from your own main.rs file, including the fn search_file and fn main lines.

Verify parallel performance on a large directory

The real payoff of rayon shows up when there are many files to process. A single-threaded scan of a large directory takes noticeably longer than a parallel one.

Before you run the next command, predict: will the output be different from the sequential version, or just faster?

  • Run your tool on your home directory (or another large directory) by running:
cargo run -- "fn" ~/

The output content is the same as the sequential version. Only the speed changes. Results may appear in a different order because files are processed across multiple threads simultaneously.

Measure the difference

Want to see the speedup in numbers? Prefix your command with time to measure elapsed time:

time cargo run -- "fn" ~/

On a multi-core machine with many files, the parallel version typically finishes 2-4x faster than a sequential scan.

Not seeing any speed difference?

  • Try a larger directory. If ~/ doesn't have many files, try a directory with source code like a node_modules folder.
  • The first run after cargo build includes compilation time. Run the command twice and compare the second run.
  • On very small directories, the overhead of thread spawning can mask the benefit. Parallelism shines with hundreds or thousands of files.

Help me understand why my parallel search isn't faster

✔️ Awesome, I've got everything!

Great. Double check you've saved src/main.rs and the tool runs without errors.

ⓧ I'd like to double check the full code

Your src/main.rs has three sections. Scroll up and confirm each one is present in this order:

  • Imports at the top including use rayon::prelude::*; between clap and walkdir.
  • The Args struct with pattern and path fields (unchanged from before).
  • The search_file function returning Vec<String> (unchanged from before).
  • The new main() function that creates a Path, collects files into a Vec, then uses .par_iter() for parallel processing.

Here is the new main() function for reference:

fn main() {
    let args = Args::parse();
    let path = Path::new(&args.path);

    let files: Vec<_> = if path.is_file() {
        vec![path.to_path_buf()]
    } else {
        WalkDir::new(path)
            .into_iter()
            .filter_map(|entry| entry.ok())
            .filter(|entry| entry.file_type().is_file())
            .map(|entry| entry.into_path())
            .collect()
    };

    let results: Vec<Vec<String>> = files
        .par_iter()
        .map(|file| search_file(file, &args.pattern))
        .collect();

    for file_matches in results {
        for line in file_matches {
            println!("{}", line);
        }
    }
}

Your file search tool now harnesses every CPU core on your machine. You went from processing files one by one to searching them all simultaneously, with a single method swap. Next, the Secret Mission challenges you to add a case-insensitive search flag.

Secret mission

Add Case-Insensitive Search

Real grep supports -i for case-insensitive matching. Can you extend rustgrep with an --ignore-case flag that finds matches regardless of capitalization?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The rustgrep project folder on your Desktop (source code, compiled binaries in target/, and test files).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with your search tool or extending it with new features.

  • Your rustgrep folder stays on your Desktop with all source code and compiled binaries.
  • You can run cargo run from the project directory any time to search files.
  • Downloaded crate sources are cached in ~/.cargo/registry and shared across all Rust projects on your machine.

Pause - I'll come back to this later

Free up disk space by removing compiled artifacts while keeping your source code intact.

  • Navigate to your project folder and run the following command to remove the target/ directory (which can be hundreds of megabytes):
cd ~/Desktop/rustgrep
cargo clean
  • Your source code, Cargo.toml, and test.txt remain untouched.
  • When you return, run cargo build to recompile everything.

Delete - I don't want to use this again

Remove all project files and start fresh. This deletes your source code, compiled binaries, and test files permanently.

  • Delete the entire project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/rustgrep
  • On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\rustgrep
  • Confirm the folder is gone by running ls ~/Desktop (macOS/Linux) or dir %USERPROFILE%\Desktop (Windows). You should no longer see rustgrep in the listing.

That's a Wrap!

That's a Wrap!

Nice work. You just built a fully functional parallel file search tool in Rust, from a single-file reader all the way to a multi-threaded directory scanner.

You've learned how to:

  • Build a CLI file search tool using clap's derive macro for argument parsing and walkdir for recursive directory traversal.
  • Experience firsthand why .unwrap() is dangerous in real-world code, and replace it with idiomatic Result handling using match and filter_map.
  • Add parallel file processing with rayon by swapping a single method call from .iter() to .par_iter().
  • Secret Mission: Extended the tool with a --ignore-case flag for case-insensitive searching.

Ready to quiz yourself?