use regex::Regex;
use std::env;
use std::error::Error;
use std::fs;
#[derive(Debug)]
pub struct Config {
pub query: String,
pub file_paths: Vec<String>,
pub ignore_case: bool,
pub use_regex: bool,
pub show_line_numbers: bool,
}
#[derive(Debug, PartialEq)]
pub struct SearchResult<'a> {
pub file_path: &'a str,
pub line_number: usize,
pub line_content: &'a str,
}
impl Config {
pub fn build(mut args: impl Iterator<Item = String>) -> Result<Config, &'static str> {
args.next();
let query: String = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let file_paths: Vec<String> = args.collect();
if file_paths.is_empty() {
return Err("Didn't get a file path");
}
let ignore_case: bool = env::var("IGNORE_CASE").is_ok();
let use_regex: bool = env::var("USE_REGEX").is_ok();
let show_line_numbers: bool = env::var("SHOW_LINE_NUMBERS").is_ok();
Ok(Config {
query,
file_paths,
ignore_case,
use_regex,
show_line_numbers,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let mut found_errors = false;
let mut found_results = false;
for file_path in &config.file_paths {
let contents = match fs::read_to_string(file_path) {
Ok(contents) => contents,
Err(e) => {
eprintln!("Error reading file {}: {}", file_path, e);
found_errors = true;
continue;
}
};
let results = if config.use_regex {
search_with_regex(&config.query, &contents, file_path, config.ignore_case)?
} else if config.ignore_case {
search_case_insensitive(&config.query, &contents, file_path)
} else {
search(&config.query, &contents, file_path)
};
if !results.is_empty() {
found_results = true;
if config.file_paths.len() > 1 {
let filename = std::path::Path::new(file_path)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or(file_path);
println!("File: {}", filename);
}
for result in results {
if config.show_line_numbers {
println!(
"{}:{}: {}",
result.file_path, result.line_number, result.line_content
);
} else {
println!("{}", result.line_content);
}
}
if config.file_paths.len() > 1 {
println!();
}
}
}
if found_errors {
Err("One or more files could not be read".into())
} else if !found_results {
Err("No matches found".into())
} else {
Ok(())
}
}
pub fn search<'a>(query: &str, contents: &'a str, file_path: &'a str) -> Vec<SearchResult<'a>> {
contents
.lines()
.enumerate()
.filter_map(|(i, line)| {
if line.contains(query) {
Some(SearchResult {
file_path,
line_number: i + 1, line_content: line,
})
} else {
None
}
})
.collect()
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
file_path: &'a str,
) -> Vec<SearchResult<'a>> {
let query = query.to_lowercase();
contents
.lines()
.enumerate()
.filter_map(|(i, line)| {
if line.to_lowercase().contains(&query) {
Some(SearchResult {
file_path,
line_number: i + 1, line_content: line,
})
} else {
None
}
})
.collect()
}
pub fn search_with_regex<'a>(
pattern: &str,
contents: &'a str,
file_path: &'a str,
ignore_case: bool,
) -> Result<Vec<SearchResult<'a>>, Box<dyn Error>> {
let regex_options = format!("{}{}", if ignore_case { "(?i)" } else { "" }, pattern);
let regex = Regex::new(®ex_options)?;
Ok(contents
.lines()
.enumerate()
.filter_map(|(i, line)| {
if regex.is_match(line) {
Some(SearchResult {
file_path,
line_number: i + 1, line_content: line,
})
} else {
None
}
})
.collect())
}