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>>;
#[derive(Parser, Debug)]
#[command(version)]
pub struct Cli {
lines: usize,
pub file: Option<PathBuf>,
#[arg(short, long, conflicts_with = "token")]
line: Option<String>,
#[arg(
short,
long,
conflicts_with = "line",
required_if_eq("ignore_extras", "true")
)]
token: Option<String>,
#[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)
}
}
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(())
}
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(())
}
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<()> {
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();
skip(&cli, &mut lines)?;
assert_eq!(String::from_utf8(lines)?, "line 2\n");
Ok(())
}
#[test]
fn skip_two_lines() -> Result<()> {
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();
skip(&cli, &mut lines)?;
assert_eq!(String::from_utf8(lines)?, ["alpha", "gamma\n"].join("\n"));
Ok(())
}
#[test]
fn skip_two_matching_lines() -> Result<()> {
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();
skip(&cli, &mut lines)?;
assert_eq!(String::from_utf8(lines)?, "gamma\n");
Ok(())
}
#[test]
fn skip_three_matching_tokens() -> Result<()> {
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();
skip(&cli, &mut lines)?;
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<()> {
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();
skip(&cli, &mut lines)?;
assert_eq!(
String::from_utf8(lines)?,
[
"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<()> {
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();
skip(&cli, &mut lines)?;
assert_eq!(
String::from_utf8(lines)?,
[
"commodo consequat.\n"
]
.join("\n")
);
Ok(())
}
}