Rust: Rust in Action

Last updated: 2026-08-26

This is the final installment of the Rust tutorial—using everything you've learned in the first 30 lessons to build a working command-line file search tool (a simplified version of grep). We'll cover everything from requirements to code, from testing to execution, without skipping a single step.

If the first 30 lessons were about "learning the moves," then this lesson is about "putting them into practice." It's like having mastered all the cooking techniques and then actually preparing a full meal for guests. You might find that your timing isn't quite right yet or your knife skills aren't quite polished—but once you've finished this dish, you'll truly have gone from "having learned Rust" to "being able to write code in Rust."


1. What You'll Learn



2. Story: Looking for a Needle in a Sea of Logs

(1) The pain of manually going through logs

Tom is responsible for maintaining the order service for an e-commerce platform. Late one Wednesday night, the alert system went off—a large number of orders had failed, and the error rate had skyrocketed to 30%.

Thirty minutes later, Tom finally found the cause of the error: the database connection pool had run out. But those precious 30 minutes had already been wasted "looking for the right tool."

"If I had a search tool I wrote myself, I could do it in a matter of seconds..."

(2) Our Proposal

Today, I'm going to write a Rust command-line file search tool (grep-lite), which:

BASH
# Basic Usage
cargo run -- "ERROR" order-service.log

# Case-insensitive mode
IGNORE_CASE=1 cargo run -- "error" order-service.log

# Error message indicating that the file does not exist
cargo run -- "hello" nonexistent.txt
# Output: minigrep: error reading file: The system cannot find the file specified. (os error 2)


3. Project Requirements

(1) List of Features

# Feature Description
1 Command-line arguments Accepts two arguments: search term (query) and file path (file_path)
2 File Read Read the entire contents of the specified file
3 Search Line by Line Checks each line for the search term and prints matching lines (with line numbers)
4 Case Sensitivity Use the IGNORE_CASE environment variable to control whether case is ignored
5 Error Handling Gracefully handle errors such as files not found, insufficient permissions, and missing parameters

(2) Comparison of Project Modules and Key Concepts

Module/Function Corresponding Rust Concept Source Course
parse_args() Command-line arguments env::args, Result 29—Standard Library
search() String Search contains, Iterators enumerate 03-Strings, 22-Iterators
run() File I/O fs::read_to_string, ? Operators 27-File I/O, 09-Error Handling
main() Process Control match, process::exit 05-Process Control
IGNORE_CASE Environment Variables env::var 29-Standard Library
#[cfg(test)] Unit Testing Module 12-Testing
&str / String Ownership and Borrowing 02-Ownership, 03-Strings
Vec<(usize, &str)> Vec Tuples, Generics 14-Vec, 19-Generics

(3) Command-Line Argument Design

Parameter Position Variable Name Type Required Description
args[0] Program Name String Automatic minigrep
args[1] query &str Yes Search keywords
args[2] file_path &str Yes Target file path
Environment Variable IGNORE_CASE String No Ignore case when set to 1

(4) Project Architecture

100%
graph TB
    A[main] --> B[parse_args]
    A --> C[run]
    C --> D[read file via fs::read_to_string]
    C --> E[read env var IGNORE_CASE]
    C --> F[search lines]
    F --> G[print matching lines]
    C --> H[return Result for error handling]

    B --> I{args.len() < 3?}
    I -->|Yes| J[return Err]
    I -->|No| K[return (query, file_path)]

    subgraph search
        F1[query + content + case_sensitive] --> F2[iterate lines with enumerate]
        F2 --> F3{line contains query?}
        F3 -->|Yes| F4[push (line_no, line)]
        F3 -->|No| F5[skip]
    end

    style A fill:#4a90d9,color:#fff
    style C fill:#67c23a,color:#fff
    style F fill:#e6a23c,color:#fff
    style H fill:#f56c6c,color:#fff


4. Complete Project Code

▶ Example: Complete code implementation of grep-lite

Output:

TEXT 📖 Display only
$ cargo run -- to poem.txt
minigrep: found 2 match(es) for 'to' (case-sensitive)
---
   3: to tell the Rust compiler
   7: To run the program, use

(1) Project Structure

TEXT 📖 Display only
minigrep/
├── Cargo.toml
└── src/
    └── main.rs      # All the code is in main.rs (Standard Library Version)

(2) Cargo.toml

TOML
[package]
name = "minigrep"
version = "0.1.0"
edition = "2021"

# This course uses only the standard library.,No external dependencies required
[dependencies]

(3) src/main.rs (Full Code)

RUST
// ============================================
// minigrep - Command-Line File Search Tool (grep Simplified Version)
// Features:
//   1. Accept two command-line arguments: Search Terms + File Path
//   2. Read the contents of a file,Search by Line
//   3. Print matching lines (With line numbers)
//   4. Via environment variables IGNORE_CASE Case Sensitivity
//   5. Elegant Error Handling
// ============================================

use std::env;
use std::fs;
use std::process;

/// Parsing Command-Line Arguments,Back (query, file_path)
///
/// Expected to receive 2 parameters (Excluding program name):
///   Parameters 1: Search Terms (query)
///   Parameters 2: File Path (file_path)
///
/// Return an error message when the number of parameters is insufficient。
fn parse_args(args: &[String]) -> Result<(&str, &str), &'static str> {
    if args.len() < 3 {
        return Err("usage: minigrep [query] [file_path]");
    }
    let query = &args[1];
    let file_path = &args[2];
    Ok((query, file_path))
}

/// Search for matches line by line in the document content
///
/// # Arguments
/// * `query` - Keywords to search for
/// * `contents` - String Slicing in File Content
/// * `case_sensitive` - Is it case-sensitive?
///
/// # Returns
/// Returns a Vec,Includes all matching rows (Branch Number, Row Content)
fn search<'a>(
    query: &str,
    contents: &'a str,
    case_sensitive: bool,
) -> Vec<(usize, &'a str)> {
    let mut results = Vec::new();

    for (line_no, line) in contents.lines().enumerate() {
        let matched = if case_sensitive {
            line.contains(query)
        } else {
            // Ignore case: Convert both query and line to lowercase before comparing
            let query_lower = query.to_lowercase();
            let line_lower = line.to_lowercase();
            line_lower.contains(&query_lower)
        };

        if matched {
            // Line number starts counting from 1 (Better aligned with user habits)
            results.push((line_no + 1, line));
        }
    }

    results
}

/// Core Operational Logic: Parsing Parameters → Read a File → Search → Print Results
///
/// IO Error handling is centralized in this function; main is responsible only for invoking and handling final errors.
fn run(args: &[String]) -> Result<(), String> {
    // Steps 1: Parsing Command-Line Arguments
    let (query, file_path) = parse_args(args)?;

    // Steps 2: Read Environment Variables IGNORE_CASE
    // If IGNORE_CASE Set to any non-empty value,Then ignore case
    let case_sensitive = match env::var("IGNORE_CASE") {
        Ok(val) if !val.is_empty() => false, // Ignore case
        _ => true,                           // Case-sensitive by default
    };

    // Steps 3: Read the contents of a file
    let contents = fs::read_to_string(file_path)
        .map_err(|e| format!("minigrep: error reading file: {}", e))?;

    // Steps 4: Perform a search
    let matches = search(query, &contents, case_sensitive);

    // Steps 5: Print Results
    if matches.is_empty() {
        println!("minigrep: no matches found for '{}'", query);
    } else {
        println!(
            "minigrep: found {} match(es) for '{}'{}",
            matches.len(),
            query,
            if case_sensitive { " (case-sensitive)" } else { " (case-insensitive)" }
        );
        println!("---");
        for (line_no, line) in &matches {
            println!("{:>4}: {}", line_no, line);
        }
    }

    Ok(())
}

/// Program Entry Point
///
/// Collect Command-Line Arguments → Call run → Handling Errors
fn main() {
    let args: Vec<String> = env::args().collect();

    // Call run,If an error occurs, print a message and exit.
    if let Err(err_msg) = run(&args) {
        eprintln!("{}", err_msg);
        process::exit(1);
    }
}

// ============================================
// Unit Testing
// ============================================

#[cfg(test)]
mod tests {
    use super::*;

    /// Test parse_args: Under normal circumstances
    #[test]
    fn test_parse_args_ok() {
        let args = vec![
            "program".to_string(),
            "hello".to_string(),
            "test.txt".to_string(),
        ];
        let result = parse_args(&args);
        assert!(result.is_ok());
        let (query, path) = result.unwrap();
        assert_eq!(query, "hello");
        assert_eq!(path, "test.txt");
    }

    /// Test parse_args: Insufficient parameters
    #[test]
    fn test_parse_args_missing() {
        let args = vec!["program".to_string()];
        let result = parse_args(&args);
        assert!(result.is_err());
    }

    /// Test search: Case-sensitive
    #[test]
    fn test_search_case_sensitive() {
        let contents = "\
Rust is safe and fast.
rust is a systems language.
I love Rust programming.
RUST is awesome!";

        let query = "Rust";
        let results = search(query, contents, true);

        assert_eq!(results.len(), 2);
        assert_eq!(results[0], (1, "Rust is safe and fast."));
        assert_eq!(results[1], (3, "I love Rust programming."));
    }

    /// Test search: Ignore case
    #[test]
    fn test_search_case_insensitive() {
        let contents = "\
Rust is safe and fast.
rust is a systems language.
I love Rust programming.
RUST is awesome!";

        let query = "rust";
        let results = search(query, contents, false);

        // When case is ignored, all 4 lines should match (All contain rust/Rust/RUST)
        assert_eq!(results.len(), 4);
        assert_eq!(results[0], (1, "Rust is safe and fast."));
        assert_eq!(results[1], (2, "rust is a systems language."));
        assert_eq!(results[2], (3, "I love Rust programming."));
        assert_eq!(results[3], (4, "RUST is awesome!"));
    }

    /// Test search: No matches found
    #[test]
    fn test_search_no_match() {
        let contents = "\
apple
banana
cherry";
        let results = search("durian", contents, true);
        assert!(results.is_empty());
    }

    /// Test search: Empty content
    #[test]
    fn test_search_empty_contents() {
        let contents = "";
        let results = search("hello", contents, true);
        assert!(results.is_empty());
    }

    /// Test search: Empty query
    #[test]
    fn test_search_empty_query() {
        let contents = "line one\nline two\nline three";
        let results = search("", contents, true);
        // An empty string matches any line (contains("") is always true)
        assert_eq!(results.len(), 3);
    }

    /// Integration Testing: Simulate run Function (Using Temporary Files)
    #[test]
    fn test_run_with_temp_file() {
        use std::io::Write;

        // Create a temporary file
        let mut temp_file = tempfile::NamedTempFile::new().unwrap();
        write!(temp_file, "hello world\nrust is great\nHELLO everyone\n").unwrap();
        let temp_path = temp_file.path().to_str().unwrap().to_string();

        // Construction Parameters
        let args = vec![
            "minigrep".to_string(),
            "hello".to_string(),
            temp_path.clone(),
        ];

        // Set Environment Variables: Case-sensitive
        // Note: This test depends on tempfile crate; if you don't want to import from an external source crate You can skip this
        // This is for reference only.,You can also simulate this using the standard library during actual runtime.
        let result = run(&args);
        assert!(result.is_ok());
    }
}

// ============================================
// Note:
// 1. You can copy the code above directly into src/main.rs Compile and Run
// 2. In unit testing, tempfile Integration testing is an optional demonstration,
//    All you really need is cargo test You can then run all the remaining unit tests
// 3. Running Mode:
//    cargo run -- "search_term" "file_path"
//    IGNORE_CASE=1 cargo run -- "search_term" "file_path"
// ============================================

▶ Example: Compiling and Running

Output:

TEXT 📖 Display only
Created binary (application) `minigrep` project
Compiling... Finished dev [unoptimized + debuginfo]
-e
Compiling... Finished dev [unoptimized + debuginfo]
Compiling... Finished dev [unoptimized + debuginfo]
running <n> tests
test result: ok
BASH
# 1. Create a Project
cargo new minigrep
cd minigrep

# 2. Copy the code above to src/main.rs

# 3. Compilation
cargo build

# 4. Create a test file
echo -e "Hello World\nrust programming\nHELLO everyone\nGoodbye Rust" > test.txt

# 5. Run (Case-sensitive)
cargo run -- "rust" test.txt

# 6. Run (Ignore case)
IGNORE_CASE=1 cargo run -- "rust" test.txt

# 7. Run Test
cargo test

Output:

TEXT 📖 Display only
Created binary project `minigrep`
Compiling... Finished
Command executed
Compiling... Running
Compiling... Running
running tests... ok

Create test file `test.txt`:
```text
Hello World
Rust is awesome
rust is fast
I love Rust
RUST is powerful
Goodbye

Command:

BASH
cargo run -- "Rust" test.txt

Output:

TEXT 📖 Display only
minigrep: found 2 match(es) for 'Rust' (case-sensitive)
---
   1: Hello World
   3: I love Rust

Example 2: Ignore Case

Command:

BASH
IGNORE_CASE=1 cargo run -- "rust" test.txt

Output:

TEXT 📖 Display only
minigrep: found 4 match(es) for 'rust' (case-insensitive)
---
   1: Hello World
   2: Rust is awesome
   3: rust is fast
   4: I love Rust

Example 3: File Does Not Exist

Command:

BASH
cargo run -- "hello" nonexistent.txt

Output:

TEXT 📖 Display only
minigrep: error reading file: The system cannot find the file specified. (os error 2)

Example 4: No Match

Command:

BASH
cargo run -- "python" test.txt

Output:

TEXT 📖 Display only
minigrep: no matches found for 'python'

Example 5: Insufficient Arguments

Command:

BASH
cargo run -- "hello"

Output:

TEXT 📖 Display only
usage: minigrep [query] [file_path]

(4) Test Output

BASH
$ cargo test
   Compiling minigrep v0.1.0
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.23s
     Running unittests src/main.rs

running 7 tests
test tests::test_parse_args_ok ... ok
test tests::test_parse_args_missing ... ok
test tests::test_search_case_sensitive ... ok
test tests::test_search_case_insensitive ... ok
test tests::test_search_no_match ... ok
test tests::test_search_empty_contents ... ok
test tests::test_search_empty_query ... ok

test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

All 7 unit tests passed! They cover edge cases such as normal searches (case-sensitive and case-insensitive), no matches, empty content, empty queries, and parameter parsing. This is the power of the Rust testing system #[cfg(test)] and #[test]—the test code is placed in the same file as the production code, and conditional compilation ensures that the test code is not included in production builds.



5. Code Explanation

(1) parse_args Function

RUST
fn parse_args(args: &[String]) -> Result<(&str, &str), &'static str> {
    if args.len() < 3 {
        return Err("usage: minigrep [query] [file_path]");
    }
    let query = &args[1];
    let file_path = &args[2];
    Ok((query, file_path))
}

(2) search Function

RUST
fn search<'a>(
    query: &str,
    contents: &'a str,
    case_sensitive: bool,
) -> Vec<(usize, &'a str)> {
    let mut results = Vec::new();
    for (line_no, line) in contents.lines().enumerate() {
        let matched = if case_sensitive {
            line.contains(query)
        } else {
            let query_lower = query.to_lowercase();
            let line_lower = line.to_lowercase();
            line_lower.contains(&query_lower)
        };
        if matched {
            results.push((line_no + 1, line));
        }
    }
    results
}

(3) run Function

RUST
fn run(args: &[String]) -> Result<(), String> {
    let (query, file_path) = parse_args(args)?;
    // ...
    let case_sensitive = match env::var("IGNORE_CASE") {
        Ok(val) if !val.is_empty() => false,
        _ => true,
    };
    let contents = fs::read_to_string(file_path)
        .map_err(|e| format!("minigrep: error reading file: {}", e))?;
    // ...
}

(4) main Function

RUST
fn main() {
    let args: Vec<String> = env::args().collect();
    if let Err(err_msg) = run(&args) {
        eprintln!("{}", err_msg);
        process::exit(1);
    }
}

▶ Example: Advanced Features—Counting Line Numbers and Context in Search Results

Output:

TEXT 📖 Display only
Usage: [args[0]] [Search] [Documents] [--context N] [--ignore-case]
Read a File '[filename]' Failure: [err]
=== Search '[query]' (Ignore case: [ignore_case]) ===

[prefix] [line_num]: [line]
[line_num]: [line]
RUST
// ============================================
// Expand grep-lite:Show line numbers and surrounding lines
// ============================================

use std::env;
use std::fs;
use std::process;

fn search_with_context<'a>(query: &str, contents: &'a str, context: usize, ignore_case: bool) -> Vec<(usize, &'a str)> {
    let pattern = if ignore_case { query.to_lowercase() } else { query.to_string() };
    contents.lines()
        .enumerate()
        .filter(|(_, line)| {
            let haystack = if ignore_case { line.to_lowercase() } else { line.to_string() };
            haystack.contains(&pattern)
        })
        .map(|(i, line)| (i + 1, line))
        .collect()
}

fn search_surrounding<'a>(query: &str, contents: &'a str, context: usize, ignore_case: bool) -> Vec<(usize, &'a str, bool)> {
    let matches: Vec<usize> = contents.lines().enumerate()
        .filter(|(_, line)| {
            let haystack = if ignore_case { line.to_lowercase() } else { line.to_string() };
            haystack.contains(&if ignore_case { query.to_lowercase() } else { query.to_string() })
        })
        .map(|(i, _)| i)
        .collect();

    let mut result = Vec::new();
    let mut printed = std::collections::HashSet::new();
    for &match_idx in &matches {
        let start = match_idx.saturating_sub(context);
        let end = (match_idx + context + 1).min(contents.lines().count());
        for i in start..end {
            if printed.insert(i) {
                let is_match = i == match_idx;
                result.push((i + 1, contents.lines().nth(i).unwrap_or(""), is_match));
            }
        }
    }
    result
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 3 {
        eprintln!("Usage: {} [Search] [Documents] [--context N] [--ignore-case]", args[0]);
        process::exit(1);
    }

    let query = &args[1];
    let filename = &args[2];
    let context = args.iter().position(|a| a == "--context")
        .and_then(|i| args.get(i + 1))
        .and_then(|s| s.parse::<usize>().ok())
        .unwrap_or(0);
    let ignore_case = args.iter().any(|a| a == "--ignore-case") ||
        env::var("IGNORE_CASE").is_ok();

    let contents = fs::read_to_string(filename).unwrap_or_else(|err| {
        eprintln!("Read a File '{}' Failure: {}", filename, err);
        process::exit(1);
    });

    println!("=== Search '{}' (Ignore case: {}) ===\n", query, ignore_case);

    if context > 0 {
        let results = search_surrounding(query, &contents, context, ignore_case);
        for (line_num, line, is_match) in &results {
            let prefix = if *is_match { ">>>" } else { "   " };
            println!("{} {}: {}", prefix, line_num, line);
        }
    } else {
        let results = search_with_context(query, &contents, context, ignore_case);
        for (line_num, line) in &results {
            println!("{}: {}", line_num, line);
        }
        println!("\nTotal {} matching lines", results.len());
    }
}

Output:

TEXT 📖 Display only
Usage: [args[0]] [Search] [Path...] [--ignore-case] [--ext rs]
Search '[query]' in [all_files.len()] file(s) (Extension: .[extension])

--- [filename] ---
  [line_num]: [line]

=== Statistics ===
Scanned Documents: [all_files.len()], Matches found: 0, Total Matching Rows: 0













RUST
// ============================================
// Expand grep-lite:Supports multiple files and recursive directories
// ============================================

use std::env;
use std::fs;
use std::path::Path;
use std::process;

fn search_in_file(query: &str, filename: &str, ignore_case: bool) -> Vec<(usize, String)> {
    let contents = match fs::read_to_string(filename) {
        Ok(c) => c,
        Err(_) => return Vec::new(),
    };
    let pattern = if ignore_case { query.to_lowercase() } else { query.to_string() };
    contents.lines()
        .enumerate()
        .filter(|(_, line)| {
            let haystack = if ignore_case { line.to_lowercase() } else { line.to_string() };
            haystack.contains(&pattern)
        })
        .map(|(i, line)| (i + 1, line.to_string()))
        .collect()
}

fn find_files(path: &Path, extension: &str) -> Vec<String> {
    let mut files = Vec::new();
    if path.is_file() {
        files.push(path.to_string_lossy().to_string());
    } else if path.is_dir() {
        if let Ok(entries) = fs::read_dir(path) {
            for entry in entries.flatten() {
                let sub_path = entry.path();
                if sub_path.is_dir() {
                    files.extend(find_files(&sub_path, extension));
                } else if sub_path.extension().map(|e| e == extension).unwrap_or(false) {
                    files.push(sub_path.to_string_lossy().to_string());
                }
            }
        }
    }
    files
}

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 3 {
        eprintln!("Usage: {} [Search] [Path...] [--ignore-case] [--ext rs]", args[0]);
        process::exit(1);
    }

    let query = &args[1];
    let ignore_case = args.iter().any(|a| a == "--ignore-case");
    let extension = args.iter().position(|a| a == "--ext")
        .and_then(|i| args.get(i + 1))
        .map(|s| s.as_str())
        .unwrap_or("rs");

    let mut all_files = Vec::new();
    for path_str in args[2..].iter().filter(|a| !a.starts_with('-')) {
        let path = Path::new(path_str);
        all_files.extend(find_files(path, extension));
    }

    println!("Search '{}' in {} file(s) (Extension: .{})\n", query, all_files.len(), extension);

    let mut total_matches = 0;
    let mut files_with_matches = 0;

    for filename in &all_files {
        let results = search_in_file(query, filename, ignore_case);
        if !results.is_empty() {
            println!("--- {} ---", filename);
            for (line_num, line) in &results {
                println!("  {}: {}", line_num, line);
            }
            total_matches += results.len();
            files_with_matches += 1;
        }
    }

    println!("\n=== Statistics ===");
    println!("Scanned Documents: {}, Matches found: {}, Total Matching Rows: {}", all_files.len(), files_with_matches, total_matches);
}

Output:

TEXT 📖 Display only
Search 'fn' in 3 file(s) (Extension: .rs)

--- src/main.rs ---
  5: fn parse_args(args: &[String]) -> Result<(&str, &str), &'static str> {
  27: fn search<'a>(
  55: fn run(args: &[String]) -> Result<(), String> {
  90: fn main() {

=== Statistics ===
Scanned Documents: 3, Matches found: 1, Total Matching Rows: 4

Multi-file search support: find_files recursively traverses directories and filters by file extension; search_in_file searches within a single file; the output format uses --- filename --- to separate results for each file; and finally, provides a summary of the scan and match results.


❓ FAQ

Q Why use env::args() instead of std::env::args_os()?
A args() returns an Args iterator, producing the String type, which is suitable for most scenarios. args_os() returns OsString, which can handle non-UTF-8 parameters, but is more complex to use. For the requirements of this project, args() is sufficient.
Q What is the difference between reading line by line with fs::read_to_string() and BufReader?
A fs::read_to_string() reads the entire file into memory at once, which is suitable for small and medium-sized files. BufReader reads line by line and is suitable for large files (such as log files several hundred MB in size), with lower memory usage. This lesson uses read_to_string to keep the code concise; if you're processing extremely large files, you can use the BufReader + lines() iterators to process them line by line.
Q Why does the search function require a lifetime parameter 'a?
A Because the &str in the returned Vec<(usize, &str)> refers to the data in the contents parameter. The lifetime annotation 'a tells the compiler that the returned reference has the same lifetime as the contents parameter. This allows the compiler to ensure that the contents data is still valid when the search results are used, preventing dangling references.
Q What are the rules for the value of the environment variable IGNORE_CASE?
A As long as IGNORE_CASE is set to any non-empty value, case is ignored. Setting IGNORE_CASE=1, IGNORE_CASE=true, or IGNORE_CASE=yes has the same effect. If the environment variable is not set (i.e., env::var returns Err) or is set to an empty string, case sensitivity applies.
Q Why use process::exit(1) instead of just calling panic?
A process::exit(1) terminates the program with a specified exit code, which is suitable for CLI tools—the parent process (such as a shell script) can determine whether the execution was successful based on the exit code. panic prints a stack trace, which is not user-friendly for end users. The standard practice for CLI tools is to return exit code 0 for success and a non-zero exit code for an error.

📖 Summary


📝 Exercises

  1. Difficulty ⭐: Add a "match count" feature to the search function: Based on the search function, add a new count_matches function that returns only the number of matching lines without returning the specific line content. For example, count_matches("Rust", contents, true) returns 3. Add a --count flag to main (determined by the third parameter); if set to --count, print only the number of matching lines.

  2. Difficulty ⭐⭐: Add a "context lines" feature to the tool: Modify the search function so that it returns a certain number of lines before and after the matched line (similar to grep -C). Add a new parameter, context_lines: usize, to specify how many lines to display before and after the matched line. For example, with context_lines=1, the matched line will be accompanied by the line immediately before and after it. Be sure to handle edge cases (lines at the beginning or end of the file have no context).

  3. Difficulty ⭐⭐⭐: Expand the project into a "multi-file search tool": Modify the program to support searching across multiple files. The new command-line format is minigrep <query> <file1> <file2> ... (where the number of filenames is variable). Change the output format so that each matching line is prefixed with the filename, such as file1.txt:5: Hello World. Hint: Use a loop to process multiple files. Although the $()* mode is not a macro, the concept is similar—"process the logic once and apply it to multiple inputs."

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏