Build a File Search CLI Tool in Rust
Build a recursive file search CLI tool in Rust with colored terminal output.
Introduction
30 Second Summary
You know that moment when you need to find a specific line of code buried somewhere in a project folder? Instead of clicking through files one by one, developers use command-line search tools that scan everything in seconds.
In this project, you will build your own file search tool called rustgrep in Rust. It recursively searches directories for a text pattern and prints color-highlighted matches to your terminal.
What You'll Build
A command-line tool that finds text patterns inside files, with colored output, line numbers, and file extension filtering.
By the end of this project, you'll have:
- A compiled native binary that searches files without needing any runtime installed.
- A professional CLI interface with auto-generated help text, powered by clap's derive API.
- Recursive directory traversal that walks through nested folders and filters by file type, with colored terminal output.
- Secret Mission: add case-insensitive search and a match count summary.
Are there any prerequisites?
This project assumes basic programming experience. If you have written code in any language (C, Python, Java, TypeScript), you have enough background to follow along. No prior Rust experience is needed.
Define the CLI with Clap's Derive API
Your Rust project compiles, but right now it only prints "Hello, world!" and ignores anything you type on the command line. A proper CLI tool needs to understand the arguments a user passes to it.
In this step, you will define your command-line interface using clap's derive API. You will describe your arguments as a Rust struct, and clap will generate the parser, help text, and error messages automatically.
In this step, get ready to:
- Define a CLI argument struct with four fields.
- Test argument parsing and auto-generated help.
- Create a search module and wire up dispatch logic.
Define the CLI struct
Clap's derive API lets you describe your CLI as a plain Rust struct. Each field becomes an argument, and doc comments above each field become the help text users see when they run --help.
- Replace the entire contents of src/main.rs with this code:
use clap::Parser;
/// A fast file search tool built in Rust
#[derive(Parser, Debug)]
#[command(name = "rustgrep", version, about)]
struct Cli {
/// The pattern to search for
pattern: String,
/// The path to search in (file or directory)
#[arg(default_value = ".")]
path: String,
/// Filter by file extension (without the dot, e.g., "rs")
#[arg(short, long)]
ext: Option<String>,
/// Show line numbers in output
#[arg(short = 'n', long)]
line_numbers: bool,
}
fn main() {
let cli = Cli::parse();
println!("{:?}", cli);
}
What does this code do?
The #[derive(Parser, Debug)] macro tells clap to generate a full argument parser from the struct's fields. The Debug derive lets you print the struct with {:?} for testing.
- pattern is a required positional argument (no #[arg] attribute needed for positional args).
- path is also positional but defaults to "." (the current directory) if omitted.
- ext is an optional flag (Option<String>) with both short (-e) and long (--ext) forms.
- line_numbers is a boolean flag. The short = 'n' attribute pins it to -n instead of the auto-inferred -l.
The /// doc comments above each field are not just for developers. Clap reads them and displays them as argument descriptions in the help output.
- Save src/main.rs.
- Verify the code compiles by running:
cargo build
You should see a Compiling rustgrep message followed by Finished with no errors.
Seeing a compile error?
- Check that your Cargo.toml includes clap with the derive feature. Without it, #[derive(Parser)] will not work.
- Make sure use clap::Parser; is at the top of main.rs. The derive macro requires this import.
- help me debug a clap compile error in my Rust project
Wire up the main function
The temporary println!("{:?}", cli) in main lets you verify that clap is correctly parsing the arguments you type.
- Test parsing by running:
cargo run -- "hello" ./src -n
You should see output like Cli { pattern: "hello", path: "./src", ext: None, line_numbers: true }.
Why the double dash?
The -- separator tells cargo to stop interpreting flags for itself and pass everything after it to your binary. Without it, cargo would try to interpret -n as its own flag.
- Now test the auto-generated help by running:
cargo run -- --help
You should see a formatted help page listing your arguments and their descriptions, all generated from the doc comments you wrote above each field.
Declare the search module
The debug print confirmed parsing works. Now you will replace it with real dispatch logic that decides whether the user's path points to a file or a directory. You will also split the search logic into its own module to keep things organized.
- Create the search module file by running:
touch src/search.rs
What is a module in Rust?
A module is how Rust organizes code into separate files. When you write mod search; in main.rs, the compiler looks for a file called src/search.rs and makes its public items available as search::function_name.
- Add placeholder functions to src/search.rs so the project compiles while you build the real search logic in the next step:
use std::path::Path;
pub fn search_file(_path: &Path, _pattern: &str, _line_numbers: bool) {
// TODO: implement in the next step
}
pub fn search_directory(
_dir: &Path,
_pattern: &str,
_line_numbers: bool,
_ext_filter: Option<&str>,
) {
// TODO: implement in the next step
}
What are the underscores in the parameters?
Prefixing a parameter name with _ tells the Rust compiler you intentionally are not using it yet. Without the underscore, the compiler would produce unused-variable warnings for every parameter.
- Save src/search.rs.
- Now replace the entire contents of src/main.rs with the final version that imports the search module and dispatches based on the path type:
use clap::Parser;
use std::path::Path;
use std::process;
mod search;
/// A fast file search tool built in Rust
#[derive(Parser, Debug)]
#[command(name = "rustgrep", version, about)]
struct Cli {
/// The pattern to search for
pattern: String,
/// The path to search in (file or directory)
#[arg(default_value = ".")]
path: String,
/// Filter by file extension (without the dot, e.g., "rs")
#[arg(short, long)]
ext: Option<String>,
/// Show line numbers in output
#[arg(short = 'n', long)]
line_numbers: bool,
}
fn main() {
let cli = Cli::parse();
let path = Path::new(&cli.path);
if path.is_file() {
search::search_file(path, &cli.pattern, cli.line_numbers);
} else if path.is_dir() {
search::search_directory(path, &cli.pattern, cli.line_numbers, cli.ext.as_deref());
} else {
eprintln!("Error: '{}' is not a valid file or directory", cli.path);
process::exit(1);
}
}
What does the dispatch logic do?
- Path::new(&cli.path) converts the string argument into a Path reference that provides file system methods.
- path.is_file() and path.is_dir() check what the path points to.
- cli.ext.as_deref() converts Option<String> to Option<&str>. This avoids transferring ownership of the string to the function.
- The else branch handles invalid paths with an error message and a non-zero exit code.
- Save src/main.rs.
- Verify everything compiles and parses correctly by running:
cargo run -- "hello" .
The program should run and exit silently with no errors. The search functions are stubs, so there is no output yet.
- Confirm help still works by running:
cargo run -- --help
You should see the auto-generated help page with all four arguments listed.
Seeing "cannot find function" errors?
- Make sure src/search.rs exists and contains both pub fn search_file and pub fn search_directory. The functions must be pub (public) for main.rs to call them.
- Confirm mod search; appears near the top of main.rs (after the use statements). Without it, Rust does not know the file exists.
- Check the function signatures in search.rs match exactly what main.rs expects. The parameter types must align.
- help me fix module import errors in my Rust project
✔️ Awesome, I've got everything!
Great. Double check you have saved both src/main.rs and src/search.rs.
ⓧ I'd like to double check the full code
Here is the complete code for each file at the end of this step.
[package]
name = "rustgrep"
version = "0.1.0"
edition = "2024"
[dependencies]
clap = { version = "4.6", features = ["derive"] }
use clap::Parser;
use std::path::Path;
use std::process;
mod search;
/// A fast file search tool built in Rust
#[derive(Parser, Debug)]
#[command(name = "rustgrep", version, about)]
struct Cli {
/// The pattern to search for
pattern: String,
/// The path to search in (file or directory)
#[arg(default_value = ".")]
path: String,
/// Filter by file extension (without the dot, e.g., "rs")
#[arg(short, long)]
ext: Option<String>,
/// Show line numbers in output
#[arg(short = 'n', long)]
line_numbers: bool,
}
fn main() {
let cli = Cli::parse();
let path = Path::new(&cli.path);
if path.is_file() {
search::search_file(path, &cli.pattern, cli.line_numbers);
} else if path.is_dir() {
search::search_directory(path, &cli.pattern, cli.line_numbers, cli.ext.as_deref());
} else {
eprintln!("Error: '{}' is not a valid file or directory", cli.path);
process::exit(1);
}
}
use std::path::Path;
pub fn search_file(_path: &Path, _pattern: &str, _line_numbers: bool) {
// TODO: implement in the next step
}
pub fn search_directory(
_dir: &Path,
_pattern: &str,
_line_numbers: bool,
_ext_filter: Option<&str>,
) {
// TODO: implement in the next step
}
Your CLI now parses arguments, generates help text, and routes to the right search function based on the path type. Next up, you will implement the actual file search logic that finds matching lines inside a file.
Build the Single-File Search Function
Your CLI is wired up and parsing arguments correctly. Now it is time to give rustgrep the ability to actually search inside a file.
In this step, you will write the core search logic that opens a file, scans each line for your search pattern, and prints any matches. This is where Rust's ownership and borrowing system shows up in real code.
In this step, get ready to:
- Implement the search_file function with file reading and line iteration.
- See Rust's ownership and borrowing in action through references and Result types.
- Test the search by finding function definitions in your own source code.
Implement search_file
The search_file function needs to open a file, read its full contents, then check each line for the search pattern. If a line matches, it prints the line along with an optional line number.
- In VS Code, open src/search.rs from the Explorer sidebar.
- Replace its contents with the following code:
use std::fs;
use std::path::Path;
// Search a single file for lines that contain the pattern
pub fn search_file(path: &Path, pattern: &str, line_numbers: bool) {
// Read the file contents into a String, return early if unreadable
let content = match fs::read_to_string(path) {
Ok(content) => content,
Err(_) => return,
};
let mut found = false;
// Iterate over each line with its index
for (line_num, line) in content.lines().enumerate() {
if line.contains(pattern) {
// Print the file path as a header on the first match
if !found {
println!("\n{}", path.display());
found = true;
}
// Print with or without line numbers
if line_numbers {
println!(" {}: {}", line_num + 1, line);
} else {
println!(" {}", line);
}
}
}
}
What does this code do?
- fs::read_to_string(path) reads the entire file into a String. The match handles the Result type. If the file cannot be read, the function exits early with return.
- .lines().enumerate() splits the content by newline and pairs each line with its zero-based index.
- .contains(pattern) checks whether the search term appears anywhere in the line.
- The found flag ensures the file path header only prints once, even if multiple lines match.
Your main.rs already calls search::search_directory, but that function does not exist yet. You need to add a temporary placeholder so the code compiles.
- Add this temporary stub at the bottom of src/search.rs, below the search_file function:
pub fn search_directory(
_dir: &Path,
_pattern: &str,
_line_numbers: bool,
_ext_filter: Option<&str>,
) {
// TODO: implement in the next step
}
Why the underscore prefixes?
Rust's compiler warns about unused parameters. Prefixing a parameter name with _ tells the compiler you intentionally are not using it yet. You will replace this entire stub in the next step.
Understand ownership and borrowing in action
How does Rust manage memory in this function?
The function signature search_file(path: &Path, pattern: &str, line_numbers: bool) uses two borrowed references. The & symbol means the function borrows these values from the caller without taking ownership.
Inside the function, fs::read_to_string returns a Result<String, io::Error>. The String it produces is owned by this function. When the function ends, Rust automatically frees that memory.
The match expression handles both the success (Ok) and failure (Err) cases explicitly. This is Rust's approach to error handling. There are no hidden exceptions.
Test single-file search
The fastest way to test is to search your own source code for a pattern you know exists.
- Save src/search.rs.
- Run this command in your VS Code terminal to search main.rs for all lines containing fn:
cargo run -- "fn" src/main.rs -n
You should see output showing the file path followed by matching lines with line numbers. Each match displays the line number, a colon, then the line content.
Not seeing any output or getting a compile error?
- If you see an error mentioning search_directory, make sure you added the temporary stub function at the bottom of src/search.rs.
- If you see no matches, double-check the pattern is "fn" (with quotes) and the path is src/main.rs (not just main.rs).
- If you get a file not found error, make sure your terminal is inside the rustgrep folder.
Ask for help: My rustgrep search_file function is not compiling or producing output when I run cargo run -- "fn" src/main.rs -n
✔️ Awesome, I've got everything!
Great. Double check you have saved src/search.rs and your test command printed matching lines.
ⓧ I'd like to double check the full code
Here is the complete src/search.rs file for this step:
use std::fs;
use std::path::Path;
pub fn search_file(path: &Path, pattern: &str, line_numbers: bool) {
let content = match fs::read_to_string(path) {
Ok(content) => content,
Err(_) => return,
};
let mut found = false;
for (line_num, line) in content.lines().enumerate() {
if line.contains(pattern) {
if !found {
println!("\n{}", path.display());
found = true;
}
if line_numbers {
println!(" {}: {}", line_num + 1, line);
} else {
println!(" {}", line);
}
}
}
}
Don't forget the temporary stub
You also need the temporary search_directory stub at the bottom of this file for the code to compile. It will be replaced with the real implementation in Step 4.
Your search function works on a single file. Next up, you will make it walk entire directory trees recursively so you can search an entire project at once.
Add Recursive Directory Walking
Your search_file function can find patterns in a single file. That is useful, but real projects have dozens or hundreds of files nested in directories.
In this step, you will implement search_directory to make rustgrep walk an entire directory tree. It will recurse into subdirectories, filter by file extension, and search every matching file automatically.
In this step, get ready to:
- Implement recursive directory traversal with std::fs::read_dir.
- Filter files by extension before searching them.
- Test searching across all project files.
Implement the search_directory function
Your main.rs already calls search::search_directory when the path is a directory. The function does not exist yet, so the project will not compile. Time to fix that.
- In src/search.rs, add the following function below search_file:
pub fn search_directory(
dir: &Path,
pattern: &str,
line_numbers: bool,
ext_filter: Option<&str>,
) {
// Read all entries in the directory
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) => {
eprintln!("Error reading directory '{}': {}", dir.display(), e);
return;
}
};
// Process each entry one at a time
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(_) => continue,
};
let path = entry.path();
if path.is_dir() {
// Recurse into subdirectories
search_directory(&path, pattern, line_numbers, ext_filter);
} else if path.is_file() {
// Skip files that don't match the extension filter
if let Some(ext) = ext_filter {
match path.extension() {
Some(file_ext) if file_ext == ext => {}
_ => continue,
}
}
search_file(&path, pattern, line_numbers);
}
}
}
What does this code do?
This function receives a directory path and walks through every item inside it.
- fs::read_dir(dir) returns a Result containing an iterator over the directory entries. The match handles the case where the directory cannot be read (permission denied, for example).
- Each entry is also a Result because reading an individual entry can fail. If it does, continue skips to the next one.
- If an entry is a directory, the function calls itself recursively. If it is a file, it checks the extension filter and then calls search_file.
Why does the function borrow all its arguments?
The function takes &Path and &str references instead of owning the values. This means the caller keeps ownership and the function just reads the data.
This is critical for recursion. If the function took ownership of the pattern string, it could not pass it to itself on the next recursive call because the value would already be consumed.
- Save src/search.rs.
- Verify the project compiles by running:
cargo build
You should see a Compiling rustgrep message followed by Finished with no errors.
Seeing a compile error?
- Check that the function signature matches exactly. The fourth parameter must be ext_filter: Option<&str> to match the cli.ext.as_deref() call in main.rs.
- Make sure the function is marked pub. Without it, main.rs cannot call it from the search module.
- Confirm that the new function is placed below the closing } of search_file, not inside it.
Help me fix a compile error in my search_directory function
✔️ Awesome, I've got everything!
Great. Double check you have saved src/search.rs before moving on.
ⓧ I'd like to double check the full code
Here is the complete src/search.rs at this point:
use std::fs;
use std::path::Path;
pub fn search_file(path: &Path, pattern: &str, line_numbers: bool) {
let content = match fs::read_to_string(path) {
Ok(content) => content,
Err(_) => return,
};
let mut found = false;
for (line_num, line) in content.lines().enumerate() {
if line.contains(pattern) {
if !found {
println!("\n{}", path.display());
found = true;
}
if line_numbers {
println!(" {}: {}", line_num + 1, line);
} else {
println!(" {}", line);
}
}
}
}
pub fn search_directory(
dir: &Path,
pattern: &str,
line_numbers: bool,
ext_filter: Option<&str>,
) {
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) => {
eprintln!("Error reading directory '{}': {}", dir.display(), e);
return;
}
};
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(_) => continue,
};
let path = entry.path();
if path.is_dir() {
search_directory(&path, pattern, line_numbers, ext_filter);
} else if path.is_file() {
if let Some(ext) = ext_filter {
match path.extension() {
Some(file_ext) if file_ext == ext => {}
_ => continue,
}
}
search_file(&path, pattern, line_numbers);
}
}
}
Understand the extension filtering pattern
The extension filter is the trickiest part of the function. It uses several Rust patterns together, so let's break it down.
How does the extension filter work?
The filter only activates when the user passes the --ext flag. That is why the parameter is Option<&str>. When it is None, every file gets searched.
- if let Some(ext) = ext_filter unwraps the Option. If the user did not provide --ext, this entire block is skipped.
- path.extension() returns Option<&OsStr>. A file without an extension (like a Makefile) returns None.
- The match guard Some(file_ext) if file_ext == ext matches only when the file has an extension AND it equals the filter value. The empty body => {} means "do nothing, keep going."
- The wildcard arm _ => continue catches everything else (wrong extension or no extension) and skips the file entirely.
What is pattern matching?
Rust's match is like a switch statement that the compiler checks for completeness. You must handle every possible case, or the code will not compile. This prevents bugs where you forget an edge case.
Test recursive search
The function is ready. Let's search your own project files to confirm it works end to end.
- Search all .rs files for the word use by running:
cargo run -- "use" . --ext rs -n
You should see output listing matches from both src/main.rs and src/search.rs, with line numbers next to each matching line.
What should I see?
The output shows the file path, then each matching line indented below it. For main.rs you should see lines like use clap::Parser; and use std::path::Path; with their line numbers.
- Try searching without the extension filter to confirm all files are included:
cargo run -- "name" .
This should also find matches inside Cargo.toml (since it contains name = "rustgrep"), proving the search is not limited to .rs files when no filter is applied.
Not seeing any output?
- Make sure the second argument is . (a dot for the current directory). If you pass a path that does not exist, the tool prints an error and exits.
- Double check the quotes around the search pattern. The -- separator between cargo run and your arguments is required.
- If you used --ext .rs (with the dot), remove the dot. The filter expects just rs because path.extension() returns the extension without the dot.
Help me debug why my recursive search produces no output
Your tool now walks entire directory trees and filters files by extension. Next up, you will add colored terminal output to make matches stand out visually.
Add Colored Output
Your search tool now finds patterns recursively across files and directories.
The output is functional, but it is hard to scan. Plain white text makes it difficult to distinguish file paths from matched lines. In this step, you will add colored terminal output using ANSI escape codes so matches jump off the screen.
In this step, get ready to:
- Define ANSI color constants for your terminal output.
- Apply colors to file paths, line numbers, and matched text.
- Build an optimized release binary.
Define ANSI color constants
Terminals understand special character sequences called ANSI escape codes. When the terminal encounters one of these codes, it changes the color of the text that follows.
- In src/search.rs, find the line that says use std::path::Path; and add the following color constants below it:
// ANSI escape codes for colored terminal output
const RED: &str = "\x1b[31m";
const GREEN: &str = "\x1b[32m";
const CYAN: &str = "\x1b[36m";
const BOLD: &str = "\x1b[1m";
const RESET: &str = "\x1b[0m";
What are ANSI escape codes?
Each constant is a string that starts with \x1b[ (the escape character) followed by a number. The number tells the terminal which color to switch to.
The RESET code turns off all formatting. You place it after every colored section to prevent color from bleeding into the next line.
- Save src/search.rs.
- You won't see a change yet. The next substep uses these constants to color the output.
Apply colors to the search output
Now you will update search_file to wrap file paths in green, line numbers in cyan, and matched text in red.
- In src/search.rs, replace the entire search_file function with this updated version:
pub fn search_file(path: &Path, pattern: &str, line_numbers: bool) {
let content = match fs::read_to_string(path) {
Ok(content) => content,
Err(_) => return,
};
let mut found = false;
for (line_num, line) in content.lines().enumerate() {
if line.contains(pattern) {
// Print the file path in green+bold on first match
if !found {
println!("\n{}{}{}{}", BOLD, GREEN, path.display(), RESET);
found = true;
}
// Highlight the matched text in red
let highlighted = line.replace(pattern, &format!("{}{}{}", RED, pattern, RESET));
if line_numbers {
// Print line number in cyan
println!(" {}{}{}: {}", CYAN, line_num + 1, RESET, highlighted);
} else {
println!(" {}", highlighted);
}
}
}
}
What does this code do?
- println!("\n{}{}{}{}", BOLD, GREEN, path.display(), RESET) prints the file path in bold green text, then resets the color.
- line.replace(pattern, &format!("{}{}{}", RED, pattern, RESET)) finds every occurrence of the search term inside the line and wraps it in red.
- The line number gets wrapped in CYAN so it is visually distinct from the matched content.
- Every colored segment ends with RESET to prevent color from leaking into subsequent text.
- Save src/search.rs.
- Confirm the code compiles by running:
cargo build
You should see a successful compilation with no errors.
Seeing a compile error?
- Check that your color constants are defined between the use statements and the pub fn search_file line.
- Make sure each constant uses &str not String.
- Verify your format strings have the correct number of {} placeholders matching the number of arguments after the comma.
Why won't my search.rs compile after adding colors?
✔️ Awesome, I've got everything!
Great. Double check you have saved src/search.rs before moving on.
ⓧ I'd like to double check the full code
use std::fs;
use std::path::Path;
const RED: &str = "\x1b[31m";
const GREEN: &str = "\x1b[32m";
const CYAN: &str = "\x1b[36m";
const BOLD: &str = "\x1b[1m";
const RESET: &str = "\x1b[0m";
pub fn search_file(path: &Path, pattern: &str, line_numbers: bool) {
let content = match fs::read_to_string(path) {
Ok(content) => content,
Err(_) => return,
};
let mut found = false;
for (line_num, line) in content.lines().enumerate() {
if line.contains(pattern) {
if !found {
println!("\n{}{}{}{}", BOLD, GREEN, path.display(), RESET);
found = true;
}
let highlighted = line.replace(pattern, &format!("{}{}{}", RED, pattern, RESET));
if line_numbers {
println!(" {}{}{}: {}", CYAN, line_num + 1, RESET, highlighted);
} else {
println!(" {}", highlighted);
}
}
}
}
pub fn search_directory(
dir: &Path,
pattern: &str,
line_numbers: bool,
ext_filter: Option<&str>,
) {
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(e) => {
eprintln!("Error reading directory '{}': {}", dir.display(), e);
return;
}
};
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(_) => continue,
};
let path = entry.path();
if path.is_dir() {
search_directory(&path, pattern, line_numbers, ext_filter);
} else if path.is_file() {
if let Some(ext) = ext_filter {
match path.extension() {
Some(file_ext) if file_ext == ext => {}
_ => continue,
}
}
search_file(&path, pattern, line_numbers);
}
}
}
Test the final tool
Time to see your colors in action. You will search your own source code for function definitions.
- Run your tool against its own source code by running:
cargo run -- "fn" . --ext rs -n
You should see file paths printed in green, line numbers in cyan, and the word fn highlighted in red within each matching line.
Not seeing any colors?
- Make sure you are running the command inside the VS Code integrated terminal. Some external terminals on older Windows versions do not support ANSI codes.
- Check that your RESET constant is \x1b[0m (zero, not the letter O).
Why is my terminal not showing colored output from my Rust program?
Your tool is feature-complete. The last thing to do is compile an optimized release binary.
- Build the release binary by running:
cargo build --release
What is a release build?
During development, cargo build compiles quickly but produces a slower, larger binary. Adding --release tells the compiler to apply all optimizations.
The result is a standalone native binary that runs without any runtime dependencies. You can copy it to any machine with the same OS and run it directly.
- Verify the binary was created by running:
ls target/release/rustgrep.exe
You should see the file listed. This is your finished tool, ready to distribute.
Secret mission
Add Case-Insensitive Search and Match Summary
Your search tool works great, but it only finds exact case matches. Real-world tools like ripgrep support case-insensitive mode. Can you add a -i flag and a summary that reports total matches across all files?
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.
- The Rust toolchain (rustc, cargo, rustup) installed via rustup.
- The rust-analyzer VS Code extension.
Keep everything running
No action needed. Choose this if you plan to keep building Rust projects or want to extend rustgrep with more features.
- Your rustgrep project stays on your Desktop, ready to compile and run anytime.
- The Rust toolchain stays installed and available globally for future projects.
- The rust-analyzer extension stays active in VS Code for any Rust file you open.
Pause - I'll come back to this later
Free up disk space from compiled artifacts while keeping your source code and tools intact.
- Delete the build artifacts by running this command inside your rustgrep folder:
- cargo clean removes the target/ directory, which can be hundreds of megabytes.
- When you return, run cargo build to recompile everything.
Delete - I don't want to use this again
Remove all project files, the Rust toolchain, and the VS Code extension completely.
- Delete the project folder. On macOS/Linux:
- cd ~/Desktop rm -rf rustgrep
- On Windows:
- cd %USERPROFILE%\Desktop rmdir /s /q rustgrep
- Uninstall the Rust toolchain by running:
- rustup self uninstall
- This removes rustc, cargo, rustup, and all installed toolchains.
- Uninstall the rust-analyzer extension in VS Code. Open the Extensions panel (Ctrl+Shift+X), find rust-analyzer, and click Uninstall.
Nice Work!
Nice Work!
You just built a fully functional command-line file search tool from scratch in Rust. Your rustgrep binary can recursively search directories, filter by extension, highlight matches in color, and display line numbers.
You've learned how to:
- Build a professional CLI interface using clap's derive API with auto-generated help text, positional arguments, and optional flags.
- Apply Rust's ownership and borrowing system to safely pass file paths and content between functions, and handle errors gracefully with Result<T, E> and pattern matching.
- Implement recursive directory traversal with the standard library and compile your project to a native binary with colored ANSI terminal output.
- Secret Mission: added case-insensitive search and a match count summary that reports totals across all searched files.
Ready to quiz yourself?