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
Resultfor 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_exeandPathmanipulation.