skip/src/lib.rs

264 lines
6.9 KiB
Rust
Raw Normal View History

2023-03-20 21:33:10 +00:00
use clap::Parser;
2023-03-23 08:09:58 +00:00
use std::fmt::Debug;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
2023-03-22 06:52:40 +00:00
use std::path::PathBuf;
2023-03-20 21:33:10 +00:00
2023-03-23 08:09:58 +00:00
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
2023-03-23 15:14:10 +00:00
/// Skip lines at the start of a file
2023-03-22 06:52:40 +00:00
#[derive(Parser, Debug)]
2023-03-20 21:33:10 +00:00
#[command(version)]
pub struct Cli {
2023-03-23 15:14:10 +00:00
/// The number of lines (or tokens) to skip
2023-03-20 21:33:10 +00:00
lines: usize,
/// The file to read, or stdin if not given
pub file: Option<PathBuf>,
2023-03-20 21:33:10 +00:00
/// Skip until N lines matching this
2023-03-22 06:52:40 +00:00
#[arg(short, long, conflicts_with = "token")]
2023-03-20 21:33:10 +00:00
line: Option<String>,
/// Skip lines until N tokens found
2023-03-22 06:52:40 +00:00
#[arg(
short,
long,
conflicts_with = "line",
required_if_eq("ignore_extras", "true")
)]
2023-03-20 21:33:10 +00:00
token: Option<String>,
/// Only count the first token on each line
2023-03-22 06:52:40 +00:00
#[arg(short, long = "ignore-extras")]
2023-03-20 21:33:10 +00:00
ignore_extras: bool,
}
2023-03-23 08:09:58 +00:00
pub fn skip(cli: &Cli, writer: &mut impl Write) -> Result<()> {
let reader: Box<dyn BufRead> = match &cli.file {
2023-03-21 20:43:10 +00:00
Some(ref file) => {
let file = File::open(file)?;
Box::new(BufReader::new(file))
2023-03-22 06:52:40 +00:00
}
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 {
2023-03-23 14:49:56 +00:00
skip_tokens(cli, reader, writer, token)
} else {
2023-03-23 14:49:56 +00:00
skip_lines(cli, reader, writer)
2023-03-21 07:23:20 +00:00
}
2023-03-21 20:43:10 +00:00
}
2023-03-23 14:49:56 +00:00
// 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)?;
2023-03-21 20:43:10 +00:00
}
}
Ok(())
}
2023-03-23 14:49:56 +00:00
// skip until a number of matching lines seen
fn skip_lines_matching(
2023-03-23 08:09:58 +00:00
cli: &Cli,
reader: Box<dyn BufRead>,
2023-03-23 08:09:58 +00:00
writer: &mut impl Write,
line: &str,
) -> Result<()> {
2023-03-21 20:43:10 +00:00
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;
2023-03-21 20:43:10 +00:00
}
}
Ok(())
}
2023-03-21 07:23:20 +00:00
2023-03-23 14:49:56 +00:00
// 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<()> {
2023-03-21 20:43:10 +00:00
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;
2023-03-22 06:59:25 +00:00
}
2023-03-21 20:43:10 +00:00
}
}
2023-03-21 07:23:20 +00:00
Ok(())
2023-03-20 21:33:10 +00:00
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
2023-03-22 07:35:35 +00:00
fn skip_one_line() -> Result<()> {
2023-03-20 21:33:10 +00:00
//given
2023-03-22 06:52:40 +00:00
let cli = Cli {
lines: 1,
file: Some(PathBuf::from("tests/two-lines.txt")),
line: None,
token: None,
ignore_extras: false,
};
2023-03-23 08:09:58 +00:00
let mut lines = Vec::new();
2023-03-20 21:33:10 +00:00
//when
2023-03-23 08:09:58 +00:00
skip(&cli, &mut lines)?;
2023-03-20 21:33:10 +00:00
//then
2023-03-23 08:09:58 +00:00
assert_eq!(String::from_utf8(lines)?, "line 2\n");
2023-03-21 07:23:20 +00:00
Ok(())
}
#[test]
2023-03-22 07:35:35 +00:00
fn skip_two_lines() -> Result<()> {
2023-03-21 07:23:20 +00:00
//given
2023-03-22 06:52:40 +00:00
let cli = Cli {
lines: 2,
file: Some(PathBuf::from("tests/four-lines.txt")),
line: None,
token: None,
ignore_extras: false,
};
2023-03-23 08:09:58 +00:00
let mut lines = Vec::new();
2023-03-21 07:23:20 +00:00
//when
2023-03-23 08:09:58 +00:00
skip(&cli, &mut lines)?;
2023-03-21 07:23:20 +00:00
//then
assert_eq!(String::from_utf8(lines)?, ["alpha", "gamma\n"].join("\n"));
2023-03-21 07:23:20 +00:00
Ok(())
2023-03-20 21:33:10 +00:00
}
2023-03-21 20:43:10 +00:00
#[test]
2023-03-22 07:35:35 +00:00
fn skip_two_matching_lines() -> Result<()> {
2023-03-21 20:43:10 +00:00
//given
2023-03-22 06:52:40 +00:00
let cli = Cli {
lines: 2,
file: Some(PathBuf::from("tests/four-lines.txt")),
line: Some(String::from("alpha")),
token: None,
ignore_extras: false,
};
2023-03-23 08:09:58 +00:00
let mut lines = Vec::new();
2023-03-21 20:43:10 +00:00
//when
2023-03-23 08:09:58 +00:00
skip(&cli, &mut lines)?;
2023-03-21 20:43:10 +00:00
//then
2023-03-23 08:09:58 +00:00
assert_eq!(String::from_utf8(lines)?, "gamma\n");
2023-03-21 20:43:10 +00:00
Ok(())
}
2023-03-22 06:59:25 +00:00
#[test]
2023-03-22 07:35:35 +00:00
fn skip_three_matching_tokens() -> Result<()> {
2023-03-22 06:59:25 +00:00
//given
let cli = Cli {
lines: 3,
file: Some(PathBuf::from("tests/poem.txt")),
line: None,
token: Some(String::from("one")),
ignore_extras: false,
};
2023-03-23 08:09:58 +00:00
let mut lines = Vec::new();
2023-03-22 06:59:25 +00:00
//when
2023-03-23 08:09:58 +00:00
skip(&cli, &mut lines)?;
2023-03-22 06:59:25 +00:00
//then
assert_eq!(
2023-03-23 08:09:58 +00:00
String::from_utf8(lines)?,
[
2023-03-22 06:59:25 +00:00
"Or help one fainting robin",
"Unto his nest again,",
2023-03-23 08:09:58 +00:00
"I shall not live in vain.\n"
2023-03-22 06:59:25 +00:00
]
2023-03-23 08:09:58 +00:00
.join("\n")
2023-03-22 06:59:25 +00:00
);
Ok(())
}
#[test]
2023-03-22 07:35:35 +00:00
fn skip_three_matching_tokens_include_extras() -> Result<()> {
2023-03-22 06:59:25 +00:00
//given
let cli = Cli {
lines: 4,
file: Some(PathBuf::from("tests/lorem.txt")),
line: None,
token: Some(String::from("or")),
ignore_extras: false,
};
2023-03-23 08:09:58 +00:00
let mut lines = Vec::new();
2023-03-22 06:59:25 +00:00
//when
2023-03-23 08:09:58 +00:00
skip(&cli, &mut lines)?;
2023-03-22 06:59:25 +00:00
//then
assert_eq!(
2023-03-23 08:09:58 +00:00
String::from_utf8(lines)?,
[
2023-03-22 06:59:25 +00:00
//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",
2023-03-23 08:09:58 +00:00
"commodo consequat.\n"
2023-03-22 06:59:25 +00:00
]
2023-03-23 08:09:58 +00:00
.join("\n")
2023-03-22 06:59:25 +00:00
);
Ok(())
}
#[test]
2023-03-22 07:35:35 +00:00
fn skip_three_matching_tokens_ignore_extras() -> Result<()> {
2023-03-22 06:59:25 +00:00
//given
let cli = Cli {
lines: 4,
file: Some(PathBuf::from("tests/lorem.txt")),
line: None,
token: Some(String::from("or")),
ignore_extras: true,
};
2023-03-23 08:09:58 +00:00
let mut lines = Vec::new();
2023-03-22 06:59:25 +00:00
//when
2023-03-23 08:09:58 +00:00
skip(&cli, &mut lines)?;
2023-03-22 06:59:25 +00:00
//then
assert_eq!(
2023-03-23 08:09:58 +00:00
String::from_utf8(lines)?,
[
2023-03-22 06:59:25 +00:00
//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
2023-03-23 08:09:58 +00:00
"commodo consequat.\n"
2023-03-22 06:59:25 +00:00
]
2023-03-23 08:09:58 +00:00
.join("\n")
2023-03-22 06:59:25 +00:00
);
Ok(())
}
2023-03-20 21:33:10 +00:00
}