use clap::Parser; use std::fmt::Debug; use std::fs::File; use std::io::{BufRead, BufReader, Write}; use std::path::PathBuf; pub type Result = std::result::Result>; /// 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, /// Skip until N lines matching this #[arg(short, long, conflicts_with = "token")] line: Option, /// Skip lines until N tokens found #[arg( short, long, conflicts_with = "line", required_if_eq("ignore_extras", "true") )] token: Option, /// 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 = 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, writer: &mut impl Write) -> Result<()> { let mut counter = 0usize; for current_line in reader.lines() { if let Ok(current_line) = current_line { if counter >= cli.lines { writeln!(writer, "{}", current_line)?; } counter += 1; } } Ok(()) } // skip until a number of matching lines seen fn skip_lines_matching( cli: &Cli, reader: Box, writer: &mut impl Write, line: &str, ) -> Result<()> { let mut counter = 0usize; for current_line in reader.lines() { if let Ok(current_line) = current_line { 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, writer: &mut impl Write, token: &str, ) -> Result<()> { let mut counter = 0usize; for current_line in reader.lines() { if let Ok(current_line) = current_line { 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)?, vec!["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)?, vec![ "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)?, vec![ //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)?, vec![ //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(()) } }