use crate::csplit_error::CsplitError;
use regex::Regex;
use uucore::show_warning;
use uucore::translate;
#[derive(Debug)]
pub enum Pattern {
UpToLine(usize, ExecutePattern),
UpToMatch(Regex, i32, ExecutePattern),
SkipToMatch(Regex, i32, ExecutePattern),
}
impl std::fmt::Display for Pattern {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UpToLine(n, _) => write!(f, "{n}"),
Self::UpToMatch(regex, 0, _) => write!(f, "/{}/", regex.as_str()),
Self::UpToMatch(regex, offset, _) => write!(f, "/{}/{offset:+}", regex.as_str()),
Self::SkipToMatch(regex, 0, _) => write!(f, "%{}%", regex.as_str()),
Self::SkipToMatch(regex, offset, _) => write!(f, "%{}%{offset:+}", regex.as_str()),
}
}
}
#[derive(Debug)]
pub enum ExecutePattern {
Always,
Times(usize),
}
impl ExecutePattern {
pub fn iter(&self) -> ExecutePatternIter {
match self {
Self::Times(n) => ExecutePatternIter::new(Some(*n)),
Self::Always => ExecutePatternIter::new(None),
}
}
}
pub struct ExecutePatternIter {
max: Option<usize>,
cur: usize,
}
impl ExecutePatternIter {
fn new(max: Option<usize>) -> Self {
Self { max, cur: 0 }
}
}
impl Iterator for ExecutePatternIter {
type Item = (Option<usize>, usize);
fn next(&mut self) -> Option<(Option<usize>, usize)> {
if let Some(m) = self.max {
if self.cur == m {
None
} else {
self.cur += 1;
Some((self.max, self.cur))
}
} else {
self.cur += 1;
Some((None, self.cur))
}
}
}
pub fn get_patterns(args: &[&str]) -> Result<Vec<Pattern>, CsplitError> {
let patterns = extract_patterns(args)?;
validate_line_numbers(&patterns)?;
Ok(patterns)
}
fn extract_patterns(args: &[&str]) -> Result<Vec<Pattern>, CsplitError> {
let mut patterns = Vec::with_capacity(args.len());
let to_match_reg =
Regex::new(r"^(/(?P<UPTO>.+)/|%(?P<SKIPTO>.+)%)(?P<OFFSET>[\+-]?[0-9]+)?$").unwrap();
let execute_ntimes_reg = Regex::new(r"^\{(?P<TIMES>[0-9]+)|\*\}$").unwrap();
let mut iter = args.iter().copied().peekable();
while let Some(arg) = iter.next() {
let execute_ntimes = match iter.peek() {
None => ExecutePattern::Times(1),
Some(&next_item) => {
match execute_ntimes_reg.captures(next_item) {
None => ExecutePattern::Times(1),
Some(r) => {
iter.next();
if let Some(times) = r.name("TIMES") {
ExecutePattern::Times(times.as_str().parse::<usize>().unwrap() + 1)
} else {
ExecutePattern::Always
}
}
}
}
};
if let Some(captures) = to_match_reg.captures(arg) {
let offset = match captures.name("OFFSET") {
None => 0,
Some(m) => m.as_str().parse().unwrap(),
};
if let Some(up_to_match) = captures.name("UPTO") {
let pattern = Regex::new(up_to_match.as_str())
.map_err(|_| CsplitError::InvalidPattern(arg.to_owned()))?;
patterns.push(Pattern::UpToMatch(pattern, offset, execute_ntimes));
} else if let Some(skip_to_match) = captures.name("SKIPTO") {
let pattern = Regex::new(skip_to_match.as_str())
.map_err(|_| CsplitError::InvalidPattern(arg.to_owned()))?;
patterns.push(Pattern::SkipToMatch(pattern, offset, execute_ntimes));
}
} else if let Ok(line_number) = arg.parse::<usize>() {
patterns.push(Pattern::UpToLine(line_number, execute_ntimes));
} else {
return Err(CsplitError::InvalidPattern(arg.to_owned()));
}
}
Ok(patterns)
}
fn validate_line_numbers(patterns: &[Pattern]) -> Result<(), CsplitError> {
patterns
.iter()
.filter_map(|pattern| match pattern {
Pattern::UpToLine(line_number, _) => Some(line_number),
_ => None,
})
.try_fold(0, |prev_ln, ¤t_ln| match (prev_ln, current_ln) {
(_, 0) => Err(CsplitError::LineNumberIsZero),
(n, m) if n == m => {
show_warning!(
"{}",
translate!("csplit-warning-line-number-same-as-previous", "line_number" => n)
);
Ok(n)
}
(n, m) if n > m => Err(CsplitError::LineNumberSmallerThanPrevious(m, n)),
(_, m) => Ok(m),
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bad_pattern() {
let input = ["bad"];
assert!(get_patterns(&input).is_err());
}
#[test]
fn up_to_line_pattern() {
let input = ["24", "42", "{*}", "50", "{4}"];
let patterns = get_patterns(&input).unwrap();
assert_eq!(patterns.len(), 3);
match patterns.first() {
Some(Pattern::UpToLine(24, ExecutePattern::Times(1))) => (),
_ => panic!("expected UpToLine pattern"),
}
match patterns.get(1) {
Some(Pattern::UpToLine(42, ExecutePattern::Always)) => (),
_ => panic!("expected UpToLine pattern"),
}
match patterns.get(2) {
Some(Pattern::UpToLine(50, ExecutePattern::Times(5))) => (),
_ => panic!("expected UpToLine pattern"),
}
}
#[test]
#[allow(clippy::cognitive_complexity)]
fn up_to_match_pattern() {
let input = [
"/test1.*end$/",
"/test2.*end$/",
"{*}",
"/test3.*end$/",
"{4}",
"/test4.*end$/3",
"/test5.*end$/+3",
"/test6.*end$/-3",
];
let patterns = get_patterns(&input).unwrap();
assert_eq!(patterns.len(), 6);
match patterns.first() {
Some(Pattern::UpToMatch(reg, 0, ExecutePattern::Times(1))) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test1.*end$");
}
_ => panic!("expected UpToMatch pattern"),
}
match patterns.get(1) {
Some(Pattern::UpToMatch(reg, 0, ExecutePattern::Always)) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test2.*end$");
}
_ => panic!("expected UpToMatch pattern"),
}
match patterns.get(2) {
Some(Pattern::UpToMatch(reg, 0, ExecutePattern::Times(5))) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test3.*end$");
}
_ => panic!("expected UpToMatch pattern"),
}
match patterns.get(3) {
Some(Pattern::UpToMatch(reg, 3, ExecutePattern::Times(1))) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test4.*end$");
}
_ => panic!("expected UpToMatch pattern"),
}
match patterns.get(4) {
Some(Pattern::UpToMatch(reg, 3, ExecutePattern::Times(1))) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test5.*end$");
}
_ => panic!("expected UpToMatch pattern"),
}
match patterns.get(5) {
Some(Pattern::UpToMatch(reg, -3, ExecutePattern::Times(1))) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test6.*end$");
}
_ => panic!("expected UpToMatch pattern"),
}
}
#[test]
#[allow(clippy::cognitive_complexity)]
fn skip_to_match_pattern() {
let input = [
"%test1.*end$%",
"%test2.*end$%",
"{*}",
"%test3.*end$%",
"{4}",
"%test4.*end$%3",
"%test5.*end$%+3",
"%test6.*end$%-3",
];
let patterns = get_patterns(&input).unwrap();
assert_eq!(patterns.len(), 6);
match patterns.first() {
Some(Pattern::SkipToMatch(reg, 0, ExecutePattern::Times(1))) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test1.*end$");
}
_ => panic!("expected SkipToMatch pattern"),
}
match patterns.get(1) {
Some(Pattern::SkipToMatch(reg, 0, ExecutePattern::Always)) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test2.*end$");
}
_ => panic!("expected SkipToMatch pattern"),
}
match patterns.get(2) {
Some(Pattern::SkipToMatch(reg, 0, ExecutePattern::Times(5))) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test3.*end$");
}
_ => panic!("expected SkipToMatch pattern"),
}
match patterns.get(3) {
Some(Pattern::SkipToMatch(reg, 3, ExecutePattern::Times(1))) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test4.*end$");
}
_ => panic!("expected SkipToMatch pattern"),
}
match patterns.get(4) {
Some(Pattern::SkipToMatch(reg, 3, ExecutePattern::Times(1))) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test5.*end$");
}
_ => panic!("expected SkipToMatch pattern"),
}
match patterns.get(5) {
Some(Pattern::SkipToMatch(reg, -3, ExecutePattern::Times(1))) => {
let parsed_reg = format!("{reg}");
assert_eq!(parsed_reg, "test6.*end$");
}
_ => panic!("expected SkipToMatch pattern"),
}
}
#[test]
fn line_number_zero() {
let patterns = vec![Pattern::UpToLine(0, ExecutePattern::Times(1))];
match validate_line_numbers(&patterns) {
Err(CsplitError::LineNumberIsZero) => (),
_ => panic!("expected LineNumberIsZero error"),
}
}
#[test]
fn line_number_smaller_than_previous() {
let input = ["10", "5"];
match get_patterns(&input) {
Err(CsplitError::LineNumberSmallerThanPrevious(5, 10)) => (),
_ => panic!("expected LineNumberSmallerThanPrevious error"),
}
}
#[test]
fn line_number_smaller_than_previous_separate() {
let input = ["10", "/20/", "5"];
match get_patterns(&input) {
Err(CsplitError::LineNumberSmallerThanPrevious(5, 10)) => (),
_ => panic!("expected LineNumberSmallerThanPrevious error"),
}
}
#[test]
fn line_number_zero_separate() {
let input = ["10", "/20/", "0"];
match get_patterns(&input) {
Err(CsplitError::LineNumberIsZero) => (),
_ => panic!("expected LineNumberIsZero error"),
}
}
}