coreutiles 0.1.0

Core utils in Rust
Documentation
use std::fs::File;
use std::io::{self, stdin, Read};

use clap::Parser;

use std::{borrow::Cow, io::{BufRead, BufReader}};

pub struct Cursor<'a, R: Read> {
    reader: BufReader<R>,
    line: usize,
    case_sensitive: bool,
    query: Cow<'a,str>,
}

impl<'a,R: Read> Cursor<'a,R> {
    pub fn new(reader: R, case_sensitive: bool, query: &'a str) -> Self {
        let reader = BufReader::new(reader);
        let mut query = Cow::Borrowed(query);
        if !case_sensitive {
            query.to_mut().make_ascii_lowercase();
        }
        Self { reader, line: 0, case_sensitive, query }
    }
}

impl<R: Read> Iterator for Cursor<'_, R> {
    type Item = Match;

    fn next(&mut self) -> Option<Match> {
        self.line += 1;

        let mut buf = String::new();
        let n = self.reader.read_line(&mut buf).unwrap_or(0);
        if n == 0 {
            return None;
        }
        if buf.ends_with('\n') {
            buf.pop();
            if buf.ends_with('\r') {
                buf.pop();
            }
        }
        if !self.case_sensitive {
            buf.make_ascii_lowercase();
        }

        buf.find(self.query.as_ref())
            .map(|start| Match::new(buf, self.line, start))
            .or_else(|| self.next())
    }
}

#[derive(Debug,PartialEq)]
pub struct Match {
    text: String,
    line: usize,
    start: usize,
}

impl Match {
    pub fn new(text: String, line: usize, start: usize) -> Self {
        Self { text, line, start }
    }
    pub fn print(&self, show_ctx: bool, name: &str) {
        if show_ctx {
            print!("{name} [{},{}]: ", self.line, self.start);
        }
        println!("{}", self.text);
    }
}

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Config {
    /// Query
    query: String,
    /// Search case sensitive
    #[arg(short = 'i', long)]
    case_sensitive: bool,
    /// Show context
    #[arg(short, long)]
    show_ctx: bool,
    file_paths: Vec<String>,
}

pub fn run() -> crate::Result<()>{
    let conf = Config::parse();
    if conf.file_paths.is_empty() {
        search(&conf, stdin(), "stdin")?;
    }
    for filename in &conf.file_paths {
        match filename.as_str() {
            "-" => search(&conf, stdin(), "stdin")?,
            filename => {
                let file = File::open(filename)?;
                search(&conf, file, filename)?;
            }
        }
    }
    Ok(())
}

fn search(conf: &Config, reader: impl Read, name: &str) -> io::Result<()> {
    let cursor = Cursor::new(reader, conf.case_sensitive, &conf.query);
    cursor.for_each(|m| m.print(conf.show_ctx, name));
    Ok(())
}