Merge pull request #1672 from sylvestre/csplit2

Implement csplit
This commit is contained in:
Sylvestre Ledru
2020-12-29 10:06:43 +01:00
committed by GitHub
12 changed files with 3007 additions and 16 deletions
Generated
+33 -10
View File
@@ -1,3 +1,5 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "advapi32-sys"
version = "0.2.0"
@@ -227,6 +229,7 @@ dependencies = [
"cc 1.0.61 (registry+https://github.com/rust-lang/crates.io-index)",
"conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"filetime 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
"glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -252,6 +255,7 @@ dependencies = [
"uu_cksum 0.0.1",
"uu_comm 0.0.1",
"uu_cp 0.0.1",
"uu_csplit 0.0.1",
"uu_cut 0.0.1",
"uu_date 0.0.1",
"uu_df 0.0.1",
@@ -495,7 +499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bstr 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
"csv-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.118 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -622,6 +626,11 @@ dependencies = [
"wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "glob"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "glob"
version = "0.3.0"
@@ -689,7 +698,7 @@ dependencies = [
[[package]]
name = "itoa"
version = "0.4.6"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@@ -1150,7 +1159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -1158,7 +1167,7 @@ name = "serde_json"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.118 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -1216,7 +1225,7 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.55"
version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1490,6 +1499,19 @@ dependencies = [
"xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "uu_csplit"
version = "0.0.1"
dependencies = [
"failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"uucore 0.0.4",
"uucore_procs 0.0.4",
]
[[package]]
name = "uu_cut"
version = "0.0.1"
@@ -2355,7 +2377,7 @@ version = "0.0.4"
dependencies = [
"proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -2402,7 +2424,7 @@ dependencies = [
"log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)",
"wasm-bindgen-shared 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -2422,7 +2444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)",
"wasm-bindgen-backend 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
"wasm-bindgen-shared 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -2556,6 +2578,7 @@ dependencies = [
"checksum generic-array 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fceb69994e330afed50c93524be68c42fa898c2d9fd4ee8da03bd7363acd26f2"
"checksum getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
"checksum getrandom 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
"checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
"checksum half 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177"
"checksum hermit-abi 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8"
@@ -2565,7 +2588,7 @@ dependencies = [
"checksum isatty 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e31a8281fc93ec9693494da65fbf28c0c2aa60a2eaec25dc58e2f31952e95edc"
"checksum itertools 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
"checksum itertools 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
"checksum itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
"checksum itoa 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
"checksum js-sys 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
@@ -2634,7 +2657,7 @@ dependencies = [
"checksum smallvec 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6"
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
"checksum syn 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)" = "a571a711dddd09019ccc628e1b17fe87c59b09d513c06c026877aa708334f37a"
"checksum syn 1.0.56 (registry+https://github.com/rust-lang/crates.io-index)" = "a9802ddde94170d186eeee5005b798d9c159fa970403f1be19976d0cfb939b72"
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
"checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd"
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
+3
View File
@@ -37,6 +37,7 @@ feat_common_core = [
"cksum",
"comm",
"cp",
"csplit",
"cut",
"date",
"df",
@@ -241,6 +242,7 @@ chroot = { optional=true, version="0.0.1", package="uu_chroot", path="src/uu/c
cksum = { optional=true, version="0.0.1", package="uu_cksum", path="src/uu/cksum" }
comm = { optional=true, version="0.0.1", package="uu_comm", path="src/uu/comm" }
cp = { optional=true, version="0.0.1", package="uu_cp", path="src/uu/cp" }
csplit = { optional=true, version="0.0.1", package="uu_csplit", path="src/uu/csplit" }
cut = { optional=true, version="0.0.1", package="uu_cut", path="src/uu/cut" }
date = { optional=true, version="0.0.1", package="uu_date", path="src/uu/date" }
df = { optional=true, version="0.0.1", package="uu_df", path="src/uu/df" }
@@ -332,6 +334,7 @@ pin_winapi-util = { version="0.1.2, < 0.1.3", package="winapi-util" } ## winapi-
[dev-dependencies]
conv = "0.3"
filetime = "0.2"
glob = "0.3.0"
libc = "0.2"
rand = "0.7"
regex = "1.0"
+2
View File
@@ -53,6 +53,7 @@ PROGS := \
cksum \
comm \
cp \
csplit \
cut \
df \
dircolors \
@@ -160,6 +161,7 @@ TEST_PROGS := \
cksum \
comm \
cp \
csplit \
cut \
dircolors \
dirname \
+7 -6
View File
@@ -289,9 +289,9 @@ Utilities
| Done | Semi-Done | To Do |
|-----------|-----------|--------|
| arch | cp | chcon |
| base32 | expr | csplit |
| base64 | install | dd |
| basename | ls | numfmt |
| base32 | expr | dd |
| base64 | install | numfmt |
| basename | ls | stty |
| cat | more | pr |
| chgrp | od (`--strings` and 128-bit data types missing) | runcon |
| chmod | printf | stty |
@@ -299,9 +299,10 @@ Utilities
| chroot | split | |
| cksum | tail | |
| comm | test | |
| cut | date | |
| dircolors | join | |
| dirname | df | |
| csplit | date | |
| cut | join | |
| dircolors | df | |
| dirname | | |
| du | | |
| echo | | |
| env | | |
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "uu_csplit"
version = "0.0.1"
authors = ["uutils developers"]
license = "MIT"
description = "csplit ~ (uutils) Output pieces of FILE separated by PATTERN(s) to files 'xx00', 'xx01', ..., and output byte counts of each piece to standard output"
homepage = "https://github.com/uutils/coreutils"
repository = "https://github.com/uutils/coreutils/tree/master/src/uu/ls"
keywords = ["coreutils", "uutils", "cross-platform", "cli", "utility"]
categories = ["command-line-utilities"]
edition = "2018"
[lib]
path = "src/csplit.rs"
[dependencies]
getopts = "0.2.17"
failure = "0.1.1"
failure_derive = "0.1.1"
regex = "1.0.0"
glob = "0.2.11"
uucore = { version=">=0.0.4", package="uucore", path="../../uucore", features=["entries", "fs"] }
uucore_procs = { version=">=0.0.4", package="uucore_procs", path="../../uucore_procs" }
[[bin]]
name = "csplit"
path = "src/main.rs"
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
use std::io;
/// Errors thrown by the csplit command
#[derive(Debug, Fail)]
pub enum CsplitError {
#[fail(display = "IO error: {}", _0)]
IoError(io::Error),
#[fail(display = "'{}': line number out of range", _0)]
LineOutOfRange(String),
#[fail(display = "'{}': line number out of range on repetition {}", _0, _1)]
LineOutOfRangeOnRepetition(String, usize),
#[fail(display = "'{}': match not found", _0)]
MatchNotFound(String),
#[fail(display = "'{}': match not found on repetition {}", _0, _1)]
MatchNotFoundOnRepetition(String, usize),
#[fail(display = "line number must be greater than zero")]
LineNumberIsZero,
#[fail(
display = "line number '{}' is smaller than preceding line number, {}",
_0, _1
)]
LineNumberSmallerThanPrevious(usize, usize),
#[fail(display = "invalid pattern: {}", _0)]
InvalidPattern(String),
#[fail(display = "invalid number: '{}'", _0)]
InvalidNumber(String),
#[fail(display = "incorrect conversion specification in suffix")]
SuffixFormatIncorrect,
#[fail(display = "too many % conversion specifications in suffix")]
SuffixFormatTooManyPercents,
}
impl From<io::Error> for CsplitError {
fn from(error: io::Error) -> Self {
CsplitError::IoError(error)
}
}
+1
View File
@@ -0,0 +1 @@
uucore_procs::main!(uu_csplit); // spell-checker:ignore procs uucore
+355
View File
@@ -0,0 +1,355 @@
use crate::csplit_error::CsplitError;
use regex::Regex;
/// The definition of a pattern to match on a line.
#[derive(Debug)]
pub enum Pattern {
/// Copy the file's content to a split up to, not including, the given line number. The number
/// of times the pattern is executed is detailed in [`ExecutePattern`].
UpToLine(usize, ExecutePattern),
/// Copy the file's content to a split up to, not including, the line matching the regex. The
/// integer is an offset relative to the matched line of what to include (if positive) or
/// to exclude (if negative). The number of times the pattern is executed is detailed in
/// [`ExecutePattern`].
UpToMatch(Regex, i32, ExecutePattern),
/// Skip the file's content up to, not including, the line matching the regex. The integer
/// is an offset relative to the matched line of what to include (if positive) or to exclude
/// (if negative). The number of times the pattern is executed is detailed in [`ExecutePattern`].
SkipToMatch(Regex, i32, ExecutePattern),
}
impl ToString for Pattern {
fn to_string(&self) -> String {
match self {
Pattern::UpToLine(n, _) => n.to_string(),
Pattern::UpToMatch(regex, 0, _) => format!("/{}/", regex.as_str()),
Pattern::UpToMatch(regex, offset, _) => format!("/{}/{:+}", regex.as_str(), offset),
Pattern::SkipToMatch(regex, 0, _) => format!("%{}%", regex.as_str()),
Pattern::SkipToMatch(regex, offset, _) => format!("%{}%{:+}", regex.as_str(), offset),
}
}
}
/// The number of times a pattern can be used.
#[derive(Debug)]
pub enum ExecutePattern {
/// Execute the pattern as many times as possible
Always,
/// Execute the pattern a fixed number of times
Times(usize),
}
impl ExecutePattern {
pub fn iter(&self) -> ExecutePatternIter {
match self {
ExecutePattern::Times(n) => ExecutePatternIter::new(Some(*n)),
ExecutePattern::Always => ExecutePatternIter::new(None),
}
}
}
pub struct ExecutePatternIter {
max: Option<usize>,
cur: usize,
}
impl ExecutePatternIter {
fn new(max: Option<usize>) -> ExecutePatternIter {
ExecutePatternIter { max, cur: 0 }
}
}
impl Iterator for ExecutePatternIter {
type Item = (Option<usize>, usize);
fn next(&mut self) -> Option<(Option<usize>, usize)> {
match self.max {
// iterate until m is reached
Some(m) => {
if self.cur == m {
None
} else {
self.cur += 1;
Some((self.max, self.cur))
}
}
// no limit, just increment a counter
None => {
self.cur += 1;
Some((None, self.cur))
}
}
}
}
/// Parses the definitions of patterns given on the command line into a list of [`Pattern`]s.
///
/// # Errors
///
/// If a pattern is incorrect, a [`::CsplitError::InvalidPattern`] error is returned, which may be
/// due to, e.g.,:
/// - an invalid regular expression;
/// - an invalid number for, e.g., the offset.
pub fn get_patterns(args: &[String]) -> Result<Vec<Pattern>, CsplitError> {
let patterns = extract_patterns(args)?;
validate_line_numbers(&patterns)?;
Ok(patterns)
}
fn extract_patterns(args: &[String]) -> 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>[\+-]\d+)?$").unwrap();
let execute_ntimes_reg = Regex::new(r"^\{(?P<TIMES>\d+)|\*\}$").unwrap();
let mut iter = args.iter().peekable();
while let Some(arg) = iter.next() {
// get the number of times a pattern is repeated, which is at least once plus whatever is
// in the quantifier.
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) => {
// skip the next item
iter.next();
if let Some(times) = r.name("TIMES") {
ExecutePattern::Times(times.as_str().parse::<usize>().unwrap() + 1)
} else {
ExecutePattern::Always
}
}
}
}
};
// get the pattern definition
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 = match Regex::new(up_to_match.as_str()) {
Err(_) => {
return Err(CsplitError::InvalidPattern(arg.to_string()));
}
Ok(reg) => reg,
};
patterns.push(Pattern::UpToMatch(pattern, offset, execute_ntimes));
} else if let Some(skip_to_match) = captures.name("SKIPTO") {
let pattern = match Regex::new(skip_to_match.as_str()) {
Err(_) => {
return Err(CsplitError::InvalidPattern(arg.to_string()));
}
Ok(reg) => reg,
};
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_string()));
}
}
Ok(patterns)
}
/// Asserts the line numbers are in increasing order, starting at 1.
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, &current_ln| match (prev_ln, current_ln) {
// a line number cannot be zero
(_, 0) => Err(CsplitError::LineNumberIsZero),
// two consecutifs numbers should not be equal
(n, m) if n == m => {
show_warning!("line number '{}' is the same as preceding line number", n);
Ok(n)
}
// a number cannot be greater than the one that follows
(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 = vec!["bad".to_string()];
assert!(get_patterns(input.as_slice()).is_err());
}
#[test]
fn up_to_line_pattern() {
let input: Vec<String> = vec!["24", "42", "{*}", "50", "{4}"]
.into_iter()
.map(|v| v.to_string())
.collect();
let patterns = get_patterns(input.as_slice()).unwrap();
assert_eq!(patterns.len(), 3);
match patterns.get(0) {
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]
fn up_to_match_pattern() {
let input: Vec<String> = vec![
"/test1.*end$/",
"/test2.*end$/",
"{*}",
"/test3.*end$/",
"{4}",
"/test4.*end$/+3",
"/test5.*end$/-3",
]
.into_iter()
.map(|v| v.to_string())
.collect();
let patterns = get_patterns(input.as_slice()).unwrap();
assert_eq!(patterns.len(), 5);
match patterns.get(0) {
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"),
};
}
#[test]
fn skip_to_match_pattern() {
let input: Vec<String> = vec![
"%test1.*end$%",
"%test2.*end$%",
"{*}",
"%test3.*end$%",
"{4}",
"%test4.*end$%+3",
"%test5.*end$%-3",
]
.into_iter()
.map(|v| v.to_string())
.collect();
let patterns = get_patterns(input.as_slice()).unwrap();
assert_eq!(patterns.len(), 5);
match patterns.get(0) {
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"),
};
}
#[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: Vec<String> = vec!["10".to_string(), "5".to_string()];
match get_patterns(input.as_slice()) {
Err(CsplitError::LineNumberSmallerThanPrevious(5, 10)) => (),
_ => panic!("expected LineNumberSmallerThanPrevious error"),
}
}
#[test]
fn line_number_smaller_than_previous_separate() {
let input: Vec<String> = vec!["10".to_string(), "/20/".to_string(), "5".to_string()];
match get_patterns(input.as_slice()) {
Err(CsplitError::LineNumberSmallerThanPrevious(5, 10)) => (),
_ => panic!("expected LineNumberSmallerThanPrevious error"),
}
}
#[test]
fn line_number_zero_separate() {
let input: Vec<String> = vec!["10".to_string(), "/20/".to_string(), "0".to_string()];
match get_patterns(input.as_slice()) {
Err(CsplitError::LineNumberIsZero) => (),
_ => panic!("expected LineNumberIsZero error"),
}
}
}
+395
View File
@@ -0,0 +1,395 @@
use regex::Regex;
use crate::csplit_error::CsplitError;
/// Computes the filename of a split, taking into consideration a possible user-defined suffix
/// format.
pub struct SplitName {
fn_split_name: Box<dyn Fn(usize) -> String>,
}
impl SplitName {
/// Creates a new SplitName with the given user-defined options:
/// - `prefix_opt` specifies a prefix for all splits.
/// - `format_opt` specifies a custom format for the suffix part of the filename, using the
/// `sprintf` format notation.
/// - `n_digits_opt` defines the width of the split number.
///
/// # Caveats
///
/// If `prefix_opt` and `format_opt` are defined, and the `format_opt` has some string appearing
/// before the conversion pattern (e.g., "here-%05d"), then it is appended to the passed prefix
/// via `prefix_opt`.
///
/// If `n_digits_opt` and `format_opt` are defined, then width defined in `format_opt` is
/// taken.
pub fn new(
prefix_opt: Option<String>,
format_opt: Option<String>,
n_digits_opt: Option<String>,
) -> Result<SplitName, CsplitError> {
// get the prefix
let prefix = prefix_opt.unwrap_or_else(|| "xx".to_string());
// the width for the split offset
let n_digits = match n_digits_opt {
None => 2,
Some(opt) => match opt.parse::<usize>() {
Ok(digits) => digits,
Err(_) => return Err(CsplitError::InvalidNumber(opt)),
},
};
// translate the custom format into a function
let fn_split_name: Box<dyn Fn(usize) -> String> = match format_opt {
None => Box::new(move |n: usize| -> String {
format!("{}{:0width$}", prefix, n, width = n_digits)
}),
Some(custom) => {
let spec =
Regex::new(r"(?P<ALL>%(?P<FLAG>[0#-])(?P<WIDTH>\d+)?(?P<TYPE>[diuoxX]))")
.unwrap();
let mut captures_iter = spec.captures_iter(&custom);
let custom_fn: Box<dyn Fn(usize) -> String> = match captures_iter.next() {
Some(captures) => {
let all = captures.name("ALL").unwrap();
let before = custom[0..all.start()].to_owned();
let after = custom[all.end()..].to_owned();
let n_digits = match captures.name("WIDTH") {
None => 0,
Some(m) => m.as_str().parse::<usize>().unwrap(),
};
match (captures.name("FLAG"), captures.name("TYPE")) {
(Some(ref f), Some(ref t)) => {
match (f.as_str(), t.as_str()) {
/*
* zero padding
*/
// decimal
("0", "d") | ("0", "i") | ("0", "u") => {
Box::new(move |n: usize| -> String {
format!(
"{}{}{:0width$}{}",
prefix,
before,
n,
after,
width = n_digits
)
})
}
// octal
("0", "o") => Box::new(move |n: usize| -> String {
format!(
"{}{}{:0width$o}{}",
prefix,
before,
n,
after,
width = n_digits
)
}),
// lower hexadecimal
("0", "x") => Box::new(move |n: usize| -> String {
format!(
"{}{}{:0width$x}{}",
prefix,
before,
n,
after,
width = n_digits
)
}),
// upper hexadecimal
("0", "X") => Box::new(move |n: usize| -> String {
format!(
"{}{}{:0width$X}{}",
prefix,
before,
n,
after,
width = n_digits
)
}),
/*
* Alternate form
*/
// octal
("#", "o") => Box::new(move |n: usize| -> String {
format!(
"{}{}{:>#width$o}{}",
prefix,
before,
n,
after,
width = n_digits
)
}),
// lower hexadecimal
("#", "x") => Box::new(move |n: usize| -> String {
format!(
"{}{}{:>#width$x}{}",
prefix,
before,
n,
after,
width = n_digits
)
}),
// upper hexadecimal
("#", "X") => Box::new(move |n: usize| -> String {
format!(
"{}{}{:>#width$X}{}",
prefix,
before,
n,
after,
width = n_digits
)
}),
/*
* Left adjusted
*/
// decimal
("-", "d") | ("-", "i") | ("-", "u") => {
Box::new(move |n: usize| -> String {
format!(
"{}{}{:<#width$}{}",
prefix,
before,
n,
after,
width = n_digits
)
})
}
// octal
("-", "o") => Box::new(move |n: usize| -> String {
format!(
"{}{}{:<#width$o}{}",
prefix,
before,
n,
after,
width = n_digits
)
}),
// lower hexadecimal
("-", "x") => Box::new(move |n: usize| -> String {
format!(
"{}{}{:<#width$x}{}",
prefix,
before,
n,
after,
width = n_digits
)
}),
// upper hexadecimal
("-", "X") => Box::new(move |n: usize| -> String {
format!(
"{}{}{:<#width$X}{}",
prefix,
before,
n,
after,
width = n_digits
)
}),
_ => return Err(CsplitError::SuffixFormatIncorrect),
}
}
_ => return Err(CsplitError::SuffixFormatIncorrect),
}
}
None => return Err(CsplitError::SuffixFormatIncorrect),
};
// there cannot be more than one format pattern
if captures_iter.next().is_some() {
return Err(CsplitError::SuffixFormatTooManyPercents);
}
custom_fn
}
};
Ok(SplitName { fn_split_name })
}
/// Returns the filename of the i-th split.
pub fn get(&self, n: usize) -> String {
(self.fn_split_name)(n)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_number() {
let split_name = SplitName::new(None, None, Some(String::from("bad")));
match split_name {
Err(CsplitError::InvalidNumber(_)) => (),
_ => panic!("should fail with InvalidNumber"),
};
}
#[test]
fn invalid_suffix_format1() {
let split_name = SplitName::new(None, Some(String::from("no conversion string")), None);
match split_name {
Err(CsplitError::SuffixFormatIncorrect) => (),
_ => panic!("should fail with SuffixFormatIncorrect"),
};
}
#[test]
fn invalid_suffix_format2() {
let split_name = SplitName::new(None, Some(String::from("%042a")), None);
match split_name {
Err(CsplitError::SuffixFormatIncorrect) => (),
_ => panic!("should fail with SuffixFormatIncorrect"),
};
}
#[test]
fn default_formatter() {
let split_name = SplitName::new(None, None, None).unwrap();
assert_eq!(split_name.get(2), "xx02");
}
#[test]
fn default_formatter_with_prefix() {
let split_name = SplitName::new(Some(String::from("aaa")), None, None).unwrap();
assert_eq!(split_name.get(2), "aaa02");
}
#[test]
fn default_formatter_with_width() {
let split_name = SplitName::new(None, None, Some(String::from("5"))).unwrap();
assert_eq!(split_name.get(2), "xx00002");
}
#[test]
fn zero_padding_decimal1() {
let split_name = SplitName::new(None, Some(String::from("cst-%03d-")), None).unwrap();
assert_eq!(split_name.get(2), "xxcst-002-");
}
#[test]
fn zero_padding_decimal2() {
let split_name = SplitName::new(
Some(String::from("pre-")),
Some(String::from("cst-%03d-post")),
None,
)
.unwrap();
assert_eq!(split_name.get(2), "pre-cst-002-post");
}
#[test]
fn zero_padding_decimal3() {
let split_name = SplitName::new(
None,
Some(String::from("cst-%03d-")),
Some(String::from("42")),
)
.unwrap();
assert_eq!(split_name.get(2), "xxcst-002-");
}
#[test]
fn zero_padding_decimal4() {
let split_name = SplitName::new(None, Some(String::from("cst-%03i-")), None).unwrap();
assert_eq!(split_name.get(2), "xxcst-002-");
}
#[test]
fn zero_padding_decimal5() {
let split_name = SplitName::new(None, Some(String::from("cst-%03u-")), None).unwrap();
assert_eq!(split_name.get(2), "xxcst-002-");
}
#[test]
fn zero_padding_octal() {
let split_name = SplitName::new(None, Some(String::from("cst-%03o-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst-052-");
}
#[test]
fn zero_padding_lower_hexa() {
let split_name = SplitName::new(None, Some(String::from("cst-%03x-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst-02a-");
}
#[test]
fn zero_padding_upper_hexa() {
let split_name = SplitName::new(None, Some(String::from("cst-%03X-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst-02A-");
}
#[test]
fn alternate_form_octal() {
let split_name = SplitName::new(None, Some(String::from("cst-%#10o-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst- 0o52-");
}
#[test]
fn alternate_form_lower_hexa() {
let split_name = SplitName::new(None, Some(String::from("cst-%#10x-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst- 0x2a-");
}
#[test]
fn alternate_form_upper_hexa() {
let split_name = SplitName::new(None, Some(String::from("cst-%#10X-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst- 0x2A-");
}
#[test]
fn left_adjusted_decimal1() {
let split_name = SplitName::new(None, Some(String::from("cst-%-10d-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst-42 -");
}
#[test]
fn left_adjusted_decimal2() {
let split_name = SplitName::new(None, Some(String::from("cst-%-10i-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst-42 -");
}
#[test]
fn left_adjusted_decimal3() {
let split_name = SplitName::new(None, Some(String::from("cst-%-10u-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst-42 -");
}
#[test]
fn left_adjusted_octal() {
let split_name = SplitName::new(None, Some(String::from("cst-%-10o-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst-0o52 -");
}
#[test]
fn left_adjusted_lower_hexa() {
let split_name = SplitName::new(None, Some(String::from("cst-%-10x-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst-0x2a -");
}
#[test]
fn left_adjusted_upper_hexa() {
let split_name = SplitName::new(None, Some(String::from("cst-%-10X-")), None).unwrap();
assert_eq!(split_name.get(42), "xxcst-0x2A -");
}
#[test]
fn too_many_percent() {
let split_name = SplitName::new(None, Some(String::from("%02d-%-3x")), None);
match split_name {
Err(CsplitError::SuffixFormatTooManyPercents) => (),
_ => panic!("should fail with SuffixFormatTooManyPercents"),
};
}
}
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50