skip 0.2.1

Skip lines in a file
Documentation
use clap::Parser;
use std::fmt::Debug;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;

pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

/// Skip lines at the start of a file
#[derive(Parser, Debug)]
#[command(version)]
pub struct Cli {
    /// The number of lines (or tokens) to skip
    lines: usize,
    /// The file to read, or stdin if not given
    pub file: Option<PathBuf>,
    /// Skip until N lines matching this
    #[arg(short, long, conflicts_with = "token")]
    line: Option<String>,
    /// Skip lines until N tokens found
    #[arg(
        short,
        long,
        conflicts_with = "line",
        required_if_eq("ignore_extras", "true")
    )]
    token: Option<String>,
    /// Only count the first token on each line
    #[arg(short, long = "ignore-extras")]
    ignore_extras: bool,
}

pub fn skip(cli: &Cli, writer: &mut impl Write) -> Result<()> {
    let reader: Box<dyn BufRead> = match &cli.file {
        Some(ref file) => {
            let file = File::open(file)?;
            Box::new(BufReader::new(file))
        }
        None => Box::new(BufReader::new(std::io::stdin())),
    };
    if let Some(line) = &cli.line {
        skip_lines_matching(cli, reader, writer, line)
    } else if let Some(ref token) = cli.token {
        skip_tokens(cli, reader, writer, token)
    } else {
        skip_lines(cli, reader, writer)
    }
}

// skip a number of lines
fn skip_lines(cli: &Cli, reader: Box<dyn BufRead>, writer: &mut impl Write) -> Result<()> {
    for (counter, current_line) in reader.lines().map_while(Option::Some).flatten().enumerate() {
        if counter >= cli.lines {
            writeln!(writer, "{}", current_line)?;
        }
    }
    Ok(())
}

// skip until a number of matching lines seen
fn skip_lines_matching(
    cli: &Cli,
    reader: Box<dyn BufRead>,
    writer: &mut impl Write,
    line: &str,
) -> Result<()> {
    let mut counter = 0usize;
    for current_line in reader.lines().map_while(Option::Some).flatten() {
        if counter >= cli.lines {
            writeln!(writer, "{}", current_line)?;
        }
        if line == current_line {
            counter += 1;
        }
    }
    Ok(())
}

// skip until a number of tokens seen
// may or may not count each occurance of token on a line - see cli.ignore_extras
fn skip_tokens(
    cli: &Cli,
    reader: Box<dyn BufRead>,
    writer: &mut impl Write,
    token: &str,
) -> Result<()> {
    let mut counter = 0usize;

    for current_line in reader.lines().map_while(Option::Some).flatten() {
        if counter >= cli.lines {
            writeln!(writer, "{}", current_line)?;
        }
        if current_line.contains(token) {
            if cli.ignore_extras {
                counter += 1;
            } else {
                let occurances = current_line.matches(&token).count();
                counter += occurances;
            }
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn skip_one_line() -> Result<()> {
        //given
        let cli = Cli {
            lines: 1,
            file: Some(PathBuf::from("tests/two-lines.txt")),
            line: None,
            token: None,
            ignore_extras: false,
        };
        let mut lines = Vec::new();

        //when
        skip(&cli, &mut lines)?;

        //then
        assert_eq!(String::from_utf8(lines)?, "line 2\n");
        Ok(())
    }

    #[test]
    fn skip_two_lines() -> Result<()> {
        //given
        let cli = Cli {
            lines: 2,
            file: Some(PathBuf::from("tests/four-lines.txt")),
            line: None,
            token: None,
            ignore_extras: false,
        };
        let mut lines = Vec::new();

        //when
        skip(&cli, &mut lines)?;

        //then
        assert_eq!(String::from_utf8(lines)?, ["alpha", "gamma\n"].join("\n"));
        Ok(())
    }

    #[test]
    fn skip_two_matching_lines() -> Result<()> {
        //given
        let cli = Cli {
            lines: 2,
            file: Some(PathBuf::from("tests/four-lines.txt")),
            line: Some(String::from("alpha")),
            token: None,
            ignore_extras: false,
        };
        let mut lines = Vec::new();

        //when
        skip(&cli, &mut lines)?;

        //then
        assert_eq!(String::from_utf8(lines)?, "gamma\n");
        Ok(())
    }

    #[test]
    fn skip_three_matching_tokens() -> Result<()> {
        //given
        let cli = Cli {
            lines: 3,
            file: Some(PathBuf::from("tests/poem.txt")),
            line: None,
            token: Some(String::from("one")),
            ignore_extras: false,
        };
        let mut lines = Vec::new();

        //when
        skip(&cli, &mut lines)?;

        //then
        assert_eq!(
            String::from_utf8(lines)?,
            [
                "Or help one fainting robin",
                "Unto his nest again,",
                "I shall not live in vain.\n"
            ]
            .join("\n")
        );
        Ok(())
    }

    #[test]
    fn skip_three_matching_tokens_include_extras() -> Result<()> {
        //given
        let cli = Cli {
            lines: 4,
            file: Some(PathBuf::from("tests/lorem.txt")),
            line: None,
            token: Some(String::from("or")),
            ignore_extras: false,
        };
        let mut lines = Vec::new();

        //when
        skip(&cli, &mut lines)?;

        //then
        assert_eq!(
            String::from_utf8(lines)?,
            [
                //Lorem ipsum dolor sit amet,        -- +2 = 2
                //consectetur adipiscing elit,
                //sed do eiusmod tempor incididunt   -- +1 = 3
                //ut labore et dolore magna aliqua.  -- +2 = 5
                "Ut enim ad minim veniam,",
                "quis nostrud exercitation ullamco",
                "laboris nisi ut aliquip ex ea",
                "commodo consequat.\n"
            ]
            .join("\n")
        );
        Ok(())
    }

    #[test]
    fn skip_three_matching_tokens_ignore_extras() -> Result<()> {
        //given
        let cli = Cli {
            lines: 4,
            file: Some(PathBuf::from("tests/lorem.txt")),
            line: None,
            token: Some(String::from("or")),
            ignore_extras: true,
        };
        let mut lines = Vec::new();

        //when
        skip(&cli, &mut lines)?;

        //then
        assert_eq!(
            String::from_utf8(lines)?,
            [
                //Lorem ipsum dolor sit amet,        -- 1
                //consectetur adipiscing elit,
                //sed do eiusmod tempor incididunt   -- 2
                //ut labore et dolore magna aliqua.  -- 3
                //Ut enim ad minim veniam,
                //quis nostrud exercitation ullamco
                //laboris nisi ut aliquip ex ea      -- 4
                "commodo consequat.\n"
            ]
            .join("\n")
        );
        Ok(())
    }
}