skip/src/lib.rs
Paul Campbell ba6e2b0ab9
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
rewrite woodpecker-ci to publish to crates.io (#8)
Reviewed-on: #8
Co-authored-by: Paul Campbell <pcampbell@kemitix.net>
Co-committed-by: Paul Campbell <pcampbell@kemitix.net>
2024-02-26 10:36:03 +00:00

263 lines
6.9 KiB
Rust

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(())
}
}