mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
split: refactor filename suffix
This commit is contained in:
committed by
Sylvestre Ledru
parent
fbb454a080
commit
8372a3d2cc
+313
-61
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
// spell-checker:ignore zaaa zaab
|
||||
// spell-checker:ignore zaaa zaab feff
|
||||
//! A number in arbitrary radix expressed in a positional notation.
|
||||
//!
|
||||
//! Use the [`Number`] enum to represent an arbitrary number in an
|
||||
|
||||
+22
-592
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,379 @@
|
||||
// This file is part of the uutils coreutils package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
//! Determine the strategy for breaking up the input (file or stdin) into chunks
|
||||
//! based on the command line options
|
||||
|
||||
use crate::{OPT_BYTES, OPT_LINES, OPT_LINE_BYTES, OPT_NUMBER};
|
||||
use clap::{parser::ValueSource, ArgMatches};
|
||||
use std::fmt;
|
||||
use uucore::parse_size::{parse_size_u64, parse_size_u64_max, ParseSizeError};
|
||||
|
||||
/// Sub-strategy of the [`Strategy::Number`]
|
||||
/// Splitting a file into a specific number of chunks.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum NumberType {
|
||||
/// Split into a specific number of chunks by byte.
|
||||
Bytes(u64),
|
||||
|
||||
/// Split into a specific number of chunks by byte
|
||||
/// but output only the *k*th chunk.
|
||||
KthBytes(u64, u64),
|
||||
|
||||
/// Split into a specific number of chunks by line (approximately).
|
||||
Lines(u64),
|
||||
|
||||
/// Split into a specific number of chunks by line
|
||||
/// (approximately), but output only the *k*th chunk.
|
||||
KthLines(u64, u64),
|
||||
|
||||
/// Assign lines via round-robin to the specified number of output chunks.
|
||||
RoundRobin(u64),
|
||||
|
||||
/// Assign lines via round-robin to the specified number of output
|
||||
/// chunks, but output only the *k*th chunk.
|
||||
KthRoundRobin(u64, u64),
|
||||
}
|
||||
|
||||
impl NumberType {
|
||||
/// The number of chunks for this number type.
|
||||
pub fn num_chunks(&self) -> u64 {
|
||||
match self {
|
||||
Self::Bytes(n) => *n,
|
||||
Self::KthBytes(_, n) => *n,
|
||||
Self::Lines(n) => *n,
|
||||
Self::KthLines(_, n) => *n,
|
||||
Self::RoundRobin(n) => *n,
|
||||
Self::KthRoundRobin(_, n) => *n,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error due to an invalid parameter to the `-n` command-line option.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum NumberTypeError {
|
||||
/// The number of chunks was invalid.
|
||||
///
|
||||
/// This can happen if the value of `N` in any of the following
|
||||
/// command-line options is not a positive integer:
|
||||
///
|
||||
/// ```ignore
|
||||
/// -n N
|
||||
/// -n K/N
|
||||
/// -n l/N
|
||||
/// -n l/K/N
|
||||
/// -n r/N
|
||||
/// -n r/K/N
|
||||
/// ```
|
||||
NumberOfChunks(String),
|
||||
|
||||
/// The chunk number was invalid.
|
||||
///
|
||||
/// This can happen if the value of `K` in any of the following
|
||||
/// command-line options is not a positive integer
|
||||
/// or if `K` is 0
|
||||
/// or if `K` is greater than `N`:
|
||||
///
|
||||
/// ```ignore
|
||||
/// -n K/N
|
||||
/// -n l/K/N
|
||||
/// -n r/K/N
|
||||
/// ```
|
||||
ChunkNumber(String),
|
||||
}
|
||||
|
||||
impl NumberType {
|
||||
/// Parse a `NumberType` from a string.
|
||||
///
|
||||
/// The following strings are valid arguments:
|
||||
///
|
||||
/// ```ignore
|
||||
/// "N"
|
||||
/// "K/N"
|
||||
/// "l/N"
|
||||
/// "l/K/N"
|
||||
/// "r/N"
|
||||
/// "r/K/N"
|
||||
/// ```
|
||||
///
|
||||
/// The `N` represents the number of chunks and the `K` represents
|
||||
/// a chunk number.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If the string is not one of the valid number types,
|
||||
/// if `K` is not a nonnegative integer,
|
||||
/// or if `K` is 0,
|
||||
/// or if `N` is not a positive integer,
|
||||
/// or if `K` is greater than `N`
|
||||
/// then this function returns [`NumberTypeError`].
|
||||
fn from(s: &str) -> Result<Self, NumberTypeError> {
|
||||
fn is_invalid_chunk(chunk_number: u64, num_chunks: u64) -> bool {
|
||||
chunk_number > num_chunks || chunk_number == 0
|
||||
}
|
||||
let parts: Vec<&str> = s.split('/').collect();
|
||||
match &parts[..] {
|
||||
[n_str] => {
|
||||
let num_chunks = parse_size_u64(n_str)
|
||||
.map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?;
|
||||
if num_chunks > 0 {
|
||||
Ok(Self::Bytes(num_chunks))
|
||||
} else {
|
||||
Err(NumberTypeError::NumberOfChunks(s.to_string()))
|
||||
}
|
||||
}
|
||||
[k_str, n_str] if !k_str.starts_with('l') && !k_str.starts_with('r') => {
|
||||
let num_chunks = parse_size_u64(n_str)
|
||||
.map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?;
|
||||
let chunk_number = parse_size_u64(k_str)
|
||||
.map_err(|_| NumberTypeError::ChunkNumber(k_str.to_string()))?;
|
||||
if is_invalid_chunk(chunk_number, num_chunks) {
|
||||
return Err(NumberTypeError::ChunkNumber(k_str.to_string()));
|
||||
}
|
||||
Ok(Self::KthBytes(chunk_number, num_chunks))
|
||||
}
|
||||
["l", n_str] => {
|
||||
let num_chunks = parse_size_u64(n_str)
|
||||
.map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?;
|
||||
Ok(Self::Lines(num_chunks))
|
||||
}
|
||||
["l", k_str, n_str] => {
|
||||
let num_chunks = parse_size_u64(n_str)
|
||||
.map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?;
|
||||
let chunk_number = parse_size_u64(k_str)
|
||||
.map_err(|_| NumberTypeError::ChunkNumber(k_str.to_string()))?;
|
||||
if is_invalid_chunk(chunk_number, num_chunks) {
|
||||
return Err(NumberTypeError::ChunkNumber(k_str.to_string()));
|
||||
}
|
||||
Ok(Self::KthLines(chunk_number, num_chunks))
|
||||
}
|
||||
["r", n_str] => {
|
||||
let num_chunks = parse_size_u64(n_str)
|
||||
.map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?;
|
||||
Ok(Self::RoundRobin(num_chunks))
|
||||
}
|
||||
["r", k_str, n_str] => {
|
||||
let num_chunks = parse_size_u64(n_str)
|
||||
.map_err(|_| NumberTypeError::NumberOfChunks(n_str.to_string()))?;
|
||||
let chunk_number = parse_size_u64(k_str)
|
||||
.map_err(|_| NumberTypeError::ChunkNumber(k_str.to_string()))?;
|
||||
if is_invalid_chunk(chunk_number, num_chunks) {
|
||||
return Err(NumberTypeError::ChunkNumber(k_str.to_string()));
|
||||
}
|
||||
Ok(Self::KthRoundRobin(chunk_number, num_chunks))
|
||||
}
|
||||
_ => Err(NumberTypeError::NumberOfChunks(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The strategy for breaking up the input file into chunks.
|
||||
pub enum Strategy {
|
||||
/// Each chunk has the specified number of lines.
|
||||
Lines(u64),
|
||||
|
||||
/// Each chunk has the specified number of bytes.
|
||||
Bytes(u64),
|
||||
|
||||
/// Each chunk has as many lines as possible without exceeding the
|
||||
/// specified number of bytes.
|
||||
LineBytes(u64),
|
||||
|
||||
/// Split the file into this many chunks.
|
||||
///
|
||||
/// There are several sub-strategies available, as defined by
|
||||
/// [`NumberType`].
|
||||
Number(NumberType),
|
||||
}
|
||||
|
||||
/// An error when parsing a chunking strategy from command-line arguments.
|
||||
pub enum StrategyError {
|
||||
/// Invalid number of lines.
|
||||
Lines(ParseSizeError),
|
||||
|
||||
/// Invalid number of bytes.
|
||||
Bytes(ParseSizeError),
|
||||
|
||||
/// Invalid number type.
|
||||
NumberType(NumberTypeError),
|
||||
|
||||
/// Multiple chunking strategies were specified (but only one should be).
|
||||
MultipleWays,
|
||||
}
|
||||
|
||||
impl fmt::Display for StrategyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::Lines(e) => write!(f, "invalid number of lines: {e}"),
|
||||
Self::Bytes(e) => write!(f, "invalid number of bytes: {e}"),
|
||||
Self::NumberType(NumberTypeError::NumberOfChunks(s)) => {
|
||||
write!(f, "invalid number of chunks: {s}")
|
||||
}
|
||||
Self::NumberType(NumberTypeError::ChunkNumber(s)) => {
|
||||
write!(f, "invalid chunk number: {s}")
|
||||
}
|
||||
Self::MultipleWays => write!(f, "cannot split in more than one way"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Strategy {
|
||||
/// Parse a strategy from the command-line arguments.
|
||||
pub fn from(matches: &ArgMatches, obs_lines: &Option<String>) -> Result<Self, StrategyError> {
|
||||
fn get_and_parse(
|
||||
matches: &ArgMatches,
|
||||
option: &str,
|
||||
strategy: fn(u64) -> Strategy,
|
||||
error: fn(ParseSizeError) -> StrategyError,
|
||||
) -> Result<Strategy, StrategyError> {
|
||||
let s = matches.get_one::<String>(option).unwrap();
|
||||
let n = parse_size_u64_max(s).map_err(error)?;
|
||||
if n > 0 {
|
||||
Ok(strategy(n))
|
||||
} else {
|
||||
Err(error(ParseSizeError::ParseFailure(s.to_string())))
|
||||
}
|
||||
}
|
||||
// Check that the user is not specifying more than one strategy.
|
||||
//
|
||||
// Note: right now, this exact behavior cannot be handled by
|
||||
// overrides_with_all() due to obsolete lines value option
|
||||
match (
|
||||
obs_lines,
|
||||
matches.value_source(OPT_LINES) == Some(ValueSource::CommandLine),
|
||||
matches.value_source(OPT_BYTES) == Some(ValueSource::CommandLine),
|
||||
matches.value_source(OPT_LINE_BYTES) == Some(ValueSource::CommandLine),
|
||||
matches.value_source(OPT_NUMBER) == Some(ValueSource::CommandLine),
|
||||
) {
|
||||
(Some(v), false, false, false, false) => {
|
||||
let v = parse_size_u64_max(v).map_err(|_| {
|
||||
StrategyError::Lines(ParseSizeError::ParseFailure(v.to_string()))
|
||||
})?;
|
||||
if v > 0 {
|
||||
Ok(Self::Lines(v))
|
||||
} else {
|
||||
Err(StrategyError::Lines(ParseSizeError::ParseFailure(
|
||||
v.to_string(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
(None, false, false, false, false) => Ok(Self::Lines(1000)),
|
||||
(None, true, false, false, false) => {
|
||||
get_and_parse(matches, OPT_LINES, Self::Lines, StrategyError::Lines)
|
||||
}
|
||||
(None, false, true, false, false) => {
|
||||
get_and_parse(matches, OPT_BYTES, Self::Bytes, StrategyError::Bytes)
|
||||
}
|
||||
(None, false, false, true, false) => get_and_parse(
|
||||
matches,
|
||||
OPT_LINE_BYTES,
|
||||
Self::LineBytes,
|
||||
StrategyError::Bytes,
|
||||
),
|
||||
(None, false, false, false, true) => {
|
||||
let s = matches.get_one::<String>(OPT_NUMBER).unwrap();
|
||||
let number_type = NumberType::from(s).map_err(StrategyError::NumberType)?;
|
||||
Ok(Self::Number(number_type))
|
||||
}
|
||||
_ => Err(StrategyError::MultipleWays),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::{strategy::NumberType, strategy::NumberTypeError};
|
||||
|
||||
#[test]
|
||||
fn test_number_type_from() {
|
||||
assert_eq!(NumberType::from("123").unwrap(), NumberType::Bytes(123));
|
||||
assert_eq!(NumberType::from("l/123").unwrap(), NumberType::Lines(123));
|
||||
assert_eq!(
|
||||
NumberType::from("l/123/456").unwrap(),
|
||||
NumberType::KthLines(123, 456)
|
||||
);
|
||||
assert_eq!(
|
||||
NumberType::from("r/123").unwrap(),
|
||||
NumberType::RoundRobin(123)
|
||||
);
|
||||
assert_eq!(
|
||||
NumberType::from("r/123/456").unwrap(),
|
||||
NumberType::KthRoundRobin(123, 456)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn test_number_type_from_error() {
|
||||
assert_eq!(
|
||||
NumberType::from("xyz").unwrap_err(),
|
||||
NumberTypeError::NumberOfChunks("xyz".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
NumberType::from("l/xyz").unwrap_err(),
|
||||
NumberTypeError::NumberOfChunks("xyz".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
NumberType::from("l/123/xyz").unwrap_err(),
|
||||
NumberTypeError::NumberOfChunks("xyz".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
NumberType::from("l/abc/456").unwrap_err(),
|
||||
NumberTypeError::ChunkNumber("abc".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
NumberType::from("l/456/123").unwrap_err(),
|
||||
NumberTypeError::ChunkNumber("456".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
NumberType::from("r/456/123").unwrap_err(),
|
||||
NumberTypeError::ChunkNumber("456".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
NumberType::from("456/123").unwrap_err(),
|
||||
NumberTypeError::ChunkNumber("456".to_string())
|
||||
);
|
||||
// In GNU split, the number of chunks get precedence:
|
||||
//
|
||||
// $ split -n l/abc/xyz
|
||||
// split: invalid number of chunks: ‘xyz’
|
||||
//
|
||||
assert_eq!(
|
||||
NumberType::from("l/abc/xyz").unwrap_err(),
|
||||
NumberTypeError::NumberOfChunks("xyz".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
NumberType::from("r/xyz").unwrap_err(),
|
||||
NumberTypeError::NumberOfChunks("xyz".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
NumberType::from("r/123/xyz").unwrap_err(),
|
||||
NumberTypeError::NumberOfChunks("xyz".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
NumberType::from("r/abc/456").unwrap_err(),
|
||||
NumberTypeError::ChunkNumber("abc".to_string())
|
||||
);
|
||||
// In GNU split, the number of chunks get precedence:
|
||||
//
|
||||
// $ split -n r/abc/xyz
|
||||
// split: invalid number of chunks: ‘xyz’
|
||||
//
|
||||
assert_eq!(
|
||||
NumberType::from("r/abc/xyz").unwrap_err(),
|
||||
NumberTypeError::NumberOfChunks("xyz".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_type_num_chunks() {
|
||||
assert_eq!(NumberType::from("123").unwrap().num_chunks(), 123);
|
||||
assert_eq!(NumberType::from("123/456").unwrap().num_chunks(), 456);
|
||||
assert_eq!(NumberType::from("l/123").unwrap().num_chunks(), 123);
|
||||
assert_eq!(NumberType::from("l/123/456").unwrap().num_chunks(), 456);
|
||||
assert_eq!(NumberType::from("r/123").unwrap().num_chunks(), 123);
|
||||
assert_eq!(NumberType::from("r/123/456").unwrap().num_chunks(), 456);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user