Compare commits
10 commits
c7002aef34
...
de79955778
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de79955778 | ||
|
|
ea6c75a0a8 | ||
|
|
a1c13d4342 | ||
|
|
0f57d037e0 | ||
|
|
0e20aff140 | ||
|
|
8452eae0f4 | ||
|
|
ad6dc24d7e | ||
|
|
3f1a5c5ef3 | ||
|
|
f8181ee583 | ||
|
|
3a7df31b08 |
5 changed files with 144 additions and 44 deletions
23
src/cli.rs
Normal file
23
src/cli.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
pub struct Cli {
|
||||||
|
pub(crate) pattern: String,
|
||||||
|
|
||||||
|
#[arg(value_name = "FILES", num_args = 1..)]
|
||||||
|
pub(crate) file_paths: Vec<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub(crate) ignore_case: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CliOptions {
|
||||||
|
pub ignore_case: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
pub fn get_options(&self) -> CliOptions {
|
||||||
|
CliOptions { ignore_case: self.ignore_case }
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main.rs
38
src/main.rs
|
|
@ -1,23 +1,35 @@
|
||||||
use clap::Parser;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
use crate::{cli::Cli, matches::Matches, parser::process_files};
|
||||||
|
mod cli;
|
||||||
mod matches;
|
mod matches;
|
||||||
mod parser;
|
mod parser;
|
||||||
|
|
||||||
#[derive(Parser)]
|
use clap::Parser;
|
||||||
struct Cli {
|
|
||||||
pattern: String,
|
fn main() -> ExitCode {
|
||||||
file_path: String,
|
match run() {
|
||||||
|
Ok(matches) if matches.is_empty() => ExitCode::FAILURE,
|
||||||
|
Ok(matches) => {
|
||||||
|
println!("{}", matches);
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("{}", error);
|
||||||
|
ExitCode::from(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn run() -> anyhow::Result<Matches> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
let lines_iter = parser::read_lines(&cli.file_path)?;
|
// println!("{:?} {}", cli.file_paths, cli.pattern);
|
||||||
let matches = parser::match_pattern(lines_iter, &cli.pattern, &cli.file_path);
|
|
||||||
|
|
||||||
println!("Pattern: {}", cli.pattern);
|
let matches = process_files(&cli.file_paths, &cli.pattern, &cli.get_options())?;
|
||||||
println!("File path: {}", cli.file_path);
|
if !matches.is_empty() {
|
||||||
println!("---");
|
println!("Matches for pattern {}:", cli.pattern);
|
||||||
println!("{}", matches?);
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(matches)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,33 @@
|
||||||
use std::fmt::Display;
|
use std::{fmt::Display, path::PathBuf};
|
||||||
|
|
||||||
pub struct Match {
|
pub struct Match {
|
||||||
pub file_name: String,
|
pub file_path: PathBuf,
|
||||||
pub line_number: usize,
|
pub line_number: usize,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Matches {
|
pub struct Matches {
|
||||||
pub items: Vec<Match>,
|
pub items: Vec<Match>,
|
||||||
pub pattern: String,
|
}
|
||||||
|
|
||||||
|
impl Matches {
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.items.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(&mut self, matches: &mut Self) {
|
||||||
|
self.items.append(&mut matches.items);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Match {
|
impl Display for Match {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}: line {}: {}", self.file_name, self.line_number, self.text)
|
write!(f, "{}: line {}: {}", self.file_path.display(), self.line_number, self.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Matches {
|
impl Display for Matches {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
writeln!(f, "Matches for pattern {}:", self.pattern)?;
|
|
||||||
|
|
||||||
for (idx, item) in self.items.iter().enumerate() {
|
for (idx, item) in self.items.iter().enumerate() {
|
||||||
if idx > 0 {
|
if idx > 0 {
|
||||||
writeln!(f)?;
|
writeln!(f)?;
|
||||||
|
|
|
||||||
106
src/parser.rs
106
src/parser.rs
|
|
@ -1,56 +1,112 @@
|
||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{BufRead, BufReader},
|
io::{BufRead, BufReader},
|
||||||
path::Path,
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use regex::Regex;
|
use regex::{Regex, RegexBuilder};
|
||||||
|
|
||||||
use crate::matches::{Match, Matches};
|
use crate::{
|
||||||
|
cli::CliOptions,
|
||||||
|
matches::{Match, Matches},
|
||||||
|
};
|
||||||
|
|
||||||
pub fn read_lines(
|
pub fn process_files(
|
||||||
file_path: &str,
|
file_paths: &Vec<PathBuf>,
|
||||||
) -> anyhow::Result<impl Iterator<Item = std::io::Result<String>>, anyhow::Error> {
|
pattern: &str,
|
||||||
if file_path.is_empty() {
|
cli_options: &CliOptions,
|
||||||
return Err(anyhow::anyhow!("File path cannot be empty"));
|
) -> anyhow::Result<Matches, anyhow::Error> {
|
||||||
|
let reg = RegexBuilder::new(pattern)
|
||||||
|
.case_insensitive(cli_options.ignore_case)
|
||||||
|
.build()
|
||||||
|
.with_context(|| format!("invalid regex: {:?}", pattern))?;
|
||||||
|
|
||||||
|
let mut matches = Matches { items: vec![] };
|
||||||
|
|
||||||
|
// fails and stops everything when one file fails
|
||||||
|
for file_path in file_paths {
|
||||||
|
let lines_iter = read_lines(file_path)?;
|
||||||
|
let mut _matches = match_pattern(lines_iter, pattern, file_path, ®)?;
|
||||||
|
matches.append(&mut _matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
let file =
|
// let lines_iter = read_lines(file_path)?;
|
||||||
File::open(file_path).with_context(|| format!("failed to open file: {file_path}"))?;
|
// let matches = match_pattern(lines_iter, pattern, file_path)?;
|
||||||
|
|
||||||
|
Ok(matches)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_lines(
|
||||||
|
file_path: &Path,
|
||||||
|
) -> anyhow::Result<impl Iterator<Item = std::io::Result<String>>, anyhow::Error> {
|
||||||
|
let file = File::open(file_path)
|
||||||
|
.with_context(|| format!("failed to open file: {}", file_path.display()))?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
|
|
||||||
Ok(reader.lines())
|
Ok(reader.lines())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_file_name_from_path(file_path: &str) -> String {
|
// fn get_file_name_from_path(file_path: &Path) -> String {
|
||||||
match Path::new(file_path).file_name() {
|
// match file_path.file_name() {
|
||||||
Some(name) => name.to_string_lossy().to_string(),
|
// Some(name) => name.to_string_lossy().to_string(),
|
||||||
None => String::from("unknown"),
|
// None => String::from("unknown"),
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub fn match_pattern(
|
pub fn match_pattern(
|
||||||
iter: impl Iterator<Item = std::io::Result<String>>,
|
iter: impl Iterator<Item = std::io::Result<String>>,
|
||||||
pattern: &str,
|
pattern: &str,
|
||||||
file_path: &str,
|
file_path: &Path,
|
||||||
|
regex: &Regex,
|
||||||
) -> anyhow::Result<Matches, anyhow::Error> {
|
) -> anyhow::Result<Matches, anyhow::Error> {
|
||||||
let reg = Regex::new(pattern).with_context(|| format!("invalid regex: {:?}", pattern))?;
|
|
||||||
let mut matches = Vec::new();
|
let mut matches = Vec::new();
|
||||||
|
|
||||||
for (idx, line) in iter.enumerate() {
|
for (idx, line) in iter.enumerate() {
|
||||||
let line = line?;
|
let line = line?;
|
||||||
|
|
||||||
if reg.is_match(&line) {
|
if regex.is_match(&line) {
|
||||||
let m = Match {
|
let m = Match { line_number: idx + 1, text: line, file_path: PathBuf::from(file_path) };
|
||||||
line_number: idx + 1,
|
|
||||||
text: line,
|
|
||||||
file_name: get_file_name_from_path(file_path),
|
|
||||||
};
|
|
||||||
|
|
||||||
matches.push(m);
|
matches.push(m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Matches { items: matches, pattern: String::from(pattern) })
|
Ok(Matches { items: matches })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_file_name_from_path() {
|
||||||
|
let path = Path::new("/path/to/file.txt");
|
||||||
|
let file_name = get_file_name_from_path(path);
|
||||||
|
assert_eq!(file_name, "file.txt");
|
||||||
|
|
||||||
|
let path = Path::new("file.txt");
|
||||||
|
let file_name = get_file_name_from_path(path);
|
||||||
|
assert_eq!(file_name, "file.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matches_lines_and_tracks_line_numbers() {
|
||||||
|
let input: Vec<std::io::Result<String>> =
|
||||||
|
vec![Ok("hola".into()), Ok("no".into()), Ok("hola again".into())];
|
||||||
|
|
||||||
|
let reg = Regex::new("hola").unwrap();
|
||||||
|
|
||||||
|
let result =
|
||||||
|
match_pattern(input.into_iter(), r"^hola", Path::new("/tmp/test.txt"), ®).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.items.len(), 2);
|
||||||
|
|
||||||
|
assert_eq!(result.items[0].text, "hola");
|
||||||
|
assert_eq!(result.items[0].line_number, 1);
|
||||||
|
assert_eq!(result.items[0].file_name, "test.txt");
|
||||||
|
|
||||||
|
assert_eq!(result.items[1].text, "hola again");
|
||||||
|
assert_eq!(result.items[1].line_number, 3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
test.txt
2
test.txt
|
|
@ -1,2 +1,4 @@
|
||||||
hola
|
hola
|
||||||
no
|
no
|
||||||
|
hola again
|
||||||
|
yep it's me
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue