split: correct filename creation algorithm

Fix two issues with the filename creation algorithm. First, this
corrects the behavior of the `-a` option. This commit ensures a
failure occurs when the number of chunks exceeds the number of
filenames representable with the specified fixed width:

    $ printf "%0.sa" {1..11} | split -d -b 1 -a 1
    split: output file suffixes exhausted

Second, this corrects the behavior of the default behavior when `-a`
is not specified on the command line. Previously, it was always
settings the filenames to have length 2 suffixes. This commit corrects
the behavior to follow the algorithm implied by GNU split, where the
filename lengths grow dynamically by two characters once the number of
chunks grows sufficiently large:

    $ printf "%0.sa" {1..91} | ./target/debug/coreutils split -d -b 1 \
    >   && ls x* | tail
    x81
    x82
    x83
    x84
    x85
    x86
    x87
    x88
    x89
    x9000
This commit is contained in:
Jeffrey Finkelstein
2022-01-01 13:47:11 -05:00
parent e5d6b7a1cf
commit cfe5a0d82c
6 changed files with 618 additions and 48 deletions
File diff suppressed because it is too large Load Diff
+16 -46
View File
@@ -7,15 +7,17 @@
// spell-checker:ignore (ToDO) PREFIXaa
mod filenames;
mod platform;
use crate::filenames::FilenameFactory;
use clap::{crate_version, App, Arg, ArgMatches};
use std::convert::TryFrom;
use std::env;
use std::fs::remove_file;
use std::fs::File;
use std::io::{stdin, BufRead, BufReader, BufWriter, Read, Write};
use std::path::Path;
use std::{char, fs::remove_file};
use uucore::display::Quotable;
use uucore::error::{FromIo, UResult, USimpleError, UUsageError};
use uucore::parse_size::parse_size;
@@ -27,7 +29,7 @@ static OPT_ADDITIONAL_SUFFIX: &str = "additional-suffix";
static OPT_FILTER: &str = "filter";
static OPT_NUMERIC_SUFFIXES: &str = "numeric-suffixes";
static OPT_SUFFIX_LENGTH: &str = "suffix-length";
static OPT_DEFAULT_SUFFIX_LENGTH: &str = "2";
static OPT_DEFAULT_SUFFIX_LENGTH: &str = "0";
static OPT_VERBOSE: &str = "verbose";
static ARG_INPUT: &str = "input";
@@ -98,7 +100,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
}
split(&settings)
split(settings)
}
pub fn uu_app() -> App<'static, 'static> {
@@ -340,39 +342,7 @@ impl Splitter for ByteSplitter {
}
}
// (1, 3) -> "aab"
#[allow(clippy::many_single_char_names)]
fn str_prefix(i: usize, width: usize) -> String {
let mut c = "".to_owned();
let mut n = i;
let mut w = width;
while w > 0 {
w -= 1;
let div = 26usize.pow(w as u32);
let r = n / div;
n -= r * div;
c.push(char::from_u32((r as u32) + 97).unwrap());
}
c
}
// (1, 3) -> "001"
#[allow(clippy::many_single_char_names)]
fn num_prefix(i: usize, width: usize) -> String {
let mut c = "".to_owned();
let mut n = i;
let mut w = width;
while w > 0 {
w -= 1;
let div = 10usize.pow(w as u32);
let r = n / div;
n -= r * div;
c.push(char::from_digit(r as u32, 10).unwrap());
}
c
}
fn split(settings: &Settings) -> UResult<()> {
fn split(settings: Settings) -> UResult<()> {
let mut reader = BufReader::new(if settings.input == "-" {
Box::new(stdin()) as Box<dyn Read>
} else {
@@ -392,19 +362,19 @@ fn split(settings: &Settings) -> UResult<()> {
}
};
// This object is responsible for creating the filename for each chunk.
let filename_factory = FilenameFactory::new(
settings.prefix,
settings.additional_suffix,
settings.suffix_length,
settings.numeric_suffix,
);
let mut fileno = 0;
loop {
// Get a new part file set up, and construct `writer` for it.
let mut filename = settings.prefix.clone();
filename.push_str(
if settings.numeric_suffix {
num_prefix(fileno, settings.suffix_length)
} else {
str_prefix(fileno, settings.suffix_length)
}
.as_ref(),
);
filename.push_str(settings.additional_suffix.as_ref());
let filename = filename_factory
.make(fileno)
.ok_or_else(|| USimpleError::new(1, "output file suffixes exhausted"))?;
let mut writer = platform::instantiate_current_writer(&settings.filter, filename.as_str());
let bytes_consumed = splitter
+70 -2
View File
@@ -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 xzaaa sixhundredfiftyonebytes ninetyonebytes asciilowercase
extern crate rand;
extern crate regex;
@@ -16,7 +16,7 @@ use std::io::Write;
use std::path::Path;
use std::{
fs::{read_dir, File},
io::BufWriter,
io::{BufWriter, Read},
};
fn random_chars(n: usize) -> String {
@@ -340,3 +340,71 @@ fn test_split_invalid_bytes_size() {
}
}
}
fn file_read(at: &AtPath, filename: &str) -> String {
let mut s = String::new();
at.open(filename).read_to_string(&mut s).unwrap();
s
}
// TODO Use char::from_digit() in Rust v1.51.0 or later.
fn char_from_digit(n: usize) -> char {
(b'a' + n as u8) as char
}
/// Test for the default suffix length behavior: dynamically increasing size.
#[test]
fn test_alphabetic_dynamic_suffix_length() {
let (at, mut ucmd) = at_and_ucmd!();
// Split into chunks of one byte each.
//
// The input file has (26^2) - 26 + 1 = 651 bytes. This is just
// enough to force `split` to dynamically increase the length of
// the filename for the very last chunk.
//
// We expect the output files to be named
//
// xaa, xab, xac, ..., xyx, xyy, xyz, xzaaa
//
ucmd.args(&["-b", "1", "sixhundredfiftyonebytes.txt"])
.succeeds();
for i in 0..25 {
for j in 0..26 {
let filename = format!("x{}{}", char_from_digit(i), char_from_digit(j),);
let contents = file_read(&at, &filename);
assert_eq!(contents, "a");
}
}
assert_eq!(file_read(&at, "xzaaa"), "a");
}
/// Test for the default suffix length behavior: dynamically increasing size.
#[test]
fn test_numeric_dynamic_suffix_length() {
let (at, mut ucmd) = at_and_ucmd!();
// Split into chunks of one byte each, use numbers instead of
// letters as file suffixes.
//
// The input file has (10^2) - 10 + 1 = 91 bytes. This is just
// enough to force `split` to dynamically increase the length of
// the filename for the very last chunk.
//
// x00, x01, x02, ..., x87, x88, x89, x9000
//
ucmd.args(&["-d", "-b", "1", "ninetyonebytes.txt"])
.succeeds();
for i in 0..90 {
let filename = format!("x{:02}", i);
let contents = file_read(&at, &filename);
assert_eq!(contents, "a");
}
assert_eq!(file_read(&at, "x9000"), "a");
}
#[test]
fn test_suffixes_exhausted() {
new_ucmd!()
.args(&["-b", "1", "-a", "1", "asciilowercase.txt"])
.fails()
.stderr_only("split: output file suffixes exhausted");
}
+1
View File
@@ -0,0 +1 @@
abcdefghijklmnopqrstuvwxyz
+1
View File
@@ -0,0 +1 @@
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+1
View File
@@ -0,0 +1 @@
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa