Command-Line Text Search Tool in Rust with Streaming Support

This walk-through demonstrates a small CLI utility that locates lines containing a user-supplied keyword in a text file. It automatically switches between two strategies:

  • For files ≤ 1 MB the entire content is loaded into memory.
  • For larger files it streams line-by-line to avoid high memory usage.

The tool also understands --help and reoslves relative file names by looking in the current directory and then in src/.

Project layout


src/
 ├─ main.rs   # CLI glue
 └─ lib.rs    # configuration + search logic

main.rs

use std::env;
use minigrep::{Config, run};

fn main() {
    let args: Vec<String> = env::args().collect();

    let cfg = match Config::from_args(&args) {
        Ok(c)  => c,
        Err(e) => {
            eprintln!("Error: {}", e);
            std::process::exit(1);
        }
    };

    if cfg.help_requested {
        Config::print_usage();
        return;
    }

    if let Err(e) = run(cfg) {
        eprintln!("Application error: {}", e);
        std::process::exit(1);
    }
}

lib.rs

use std::{
    env, fs,
    io::{self, BufRead, BufReader},
    path::{Path, PathBuf},
};

#[derive(Debug)]
pub struct Config {
    pub needle: String,
    pub haystack: PathBuf,
    pub help_requested: bool,
}

impl Config {
    pub fn from_args(args: &[String]) -> Result<Self, &'static str> {
        match args.len() {
            1 => return Err("missing keyword and file"),
            2 if args[1] == "--help" => {
                return Ok(Config {
                    needle: String::new(),
                    haystack: PathBuf::new(),
                    help_requested: true,
                });
            }
            2 => return Err("missing file path"),
            _ => {}
        }

        let keyword = args[1].clone();
        let file_arg = args[2].clone();

        let path = resolve_path(&file_arg)?;
        Ok(Config {
            needle: keyword,
            haystack: path,
            help_requested: false,
        })
    }

    pub fn print_usage() {
        println!(
            r#"
Usage: minigrep <keyword> <file>
       minigrep --help

Arguments:
  keyword   text to search for
  file      file to inspect (absolute or relative)

Examples:
  minigrep 宋 poems.txt
  minigrep 宋 C:\docs\poems.txt
"#
        );
    }
}

fn resolve_path(input: &str) -> Result<PathBuf, &'static str> {
    let candidate = Path::new(input);
    if candidate.is_file() {
        return Ok(candidate.to_path_buf());
    }

    // try relative to executable
    if let Ok(exe_dir) = env::current_exe()
        .as_ref()
        .and_then(|p| p.parent().ok_or(()))
    {
        for base in &[exe_dir, &exe_dir.join("src")] {
            let full = base.join(input);
            if full.is_file() {
                return Ok(full);
            }
        }
    }
    Err("file not found")
}

pub fn run(cfg: Config) -> io::Result<()> {
    let meta = fs::metadata(&cfg.haystack)?;
    if meta.len() > 1_048_576 {
        search_stream(&cfg.haystack, &cfg.needle)
    } else {
        search_slurp(&cfg.haystack, &cfg.needle)
    }
}

fn search_slurp(path: &Path, keyword: &str) -> io::Result<()> {
    let text = fs::read_to_string(path)?;
    for line in text.lines() {
        if line.contains(keyword) {
            println!("{}", line);
        }
    }
    Ok(())
}

fn search_stream(path: &Path, keyword: &str) -> io::Result<()> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    for line in reader.lines() {
        let line = line?;
        if line.contains(keyword) {
            println!("{}", line);
        }
    }
    Ok(())
}

Trying it out


$ cargo run -- --help
$ cargo run 宋 src/poems.txt
$ cargo run 宋 large_novel.txt

The first command prints the built-in help. The second scans a small file entirely in memory. The third streams a multi-megabyte file without exhausting RAM.

Key takeaways

  • Using Result for fallible operations keeps error handling explicit.
  • Splitting the binary (main.rs) from the library (lib.rs) improves testability.
  • Buffered I/O (BufReader) is a zero-cost abstraction for efficient line-wise processing.
  • Relative path resolution illustrates practical use of std::env::current_exe and Path manipulation.

Tags: rust CLI IO bufreader filesystem

Posted on Sun, 17 May 2026 04:21:16 +0000 by WBSKI