fix(numfmt): update line termination handling for consistency with GNU behavior

This commit is contained in:
FidelSch
2026-03-01 20:23:39 -03:00
committed by Daniel Hofstetter
parent 8a7a7788e7
commit 995c6dcdcd
3 changed files with 97 additions and 88 deletions
+8 -12
View File
@@ -473,6 +473,7 @@ pub fn write_formatted_with_delimiter<W: std::io::Write>(
writer: &mut W,
input: &[u8],
options: &NumfmtOptions,
eol: Option<u8>,
) -> Result<()> {
let delimiter = options.delimiter.as_deref().unwrap();
@@ -497,12 +498,9 @@ pub fn write_formatted_with_delimiter<W: std::io::Write>(
}
}
let eol = if options.zero_terminated {
b"\0"
} else {
b"\n"
};
writer.write_all(eol).unwrap();
if let Some(eol) = eol {
writer.write_all(&[eol]).unwrap();
}
Ok(())
}
@@ -511,6 +509,7 @@ pub fn write_formatted_with_whitespace<W: std::io::Write>(
writer: &mut W,
s: &str,
options: &NumfmtOptions,
eol: Option<u8>,
) -> Result<()> {
for (n, (prefix, field)) in (1..).zip(WhitespaceSplitter { s: Some(s) }) {
let field_selected = uucore::ranges::contain(&options.fields, n);
@@ -548,12 +547,9 @@ pub fn write_formatted_with_whitespace<W: std::io::Write>(
}
}
let eol = if options.zero_terminated {
b"\0"
} else {
b"\n"
};
writer.write_all(eol).unwrap();
if let Some(eol) = eol {
writer.write_all(&[eol]).unwrap();
}
Ok(())
}
+43 -32
View File
@@ -14,8 +14,7 @@ use crate::options::{
use crate::units::{Result, Unit};
use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, parser::ValueSource};
use std::ffi::OsString;
use std::io::{BufRead, Error, Write as _, stderr};
use std::result::Result as StdResult;
use std::io::{BufRead, Write as _, stderr};
use std::str::FromStr;
use units::{IEC_BASES, SI_BASES};
@@ -33,37 +32,51 @@ mod units;
fn handle_args<'a>(args: impl Iterator<Item = &'a [u8]>, options: &NumfmtOptions) -> UResult<()> {
let mut stdout = std::io::stdout().lock();
let terminator = if options.zero_terminated { 0u8 } else { b'\n' };
for l in args {
write_line(&mut stdout, l, options)?;
write_line(&mut stdout, l, options, Some(terminator))?;
}
Ok(())
}
fn handle_buffer<R>(input: R, options: &NumfmtOptions) -> UResult<()>
where
R: BufRead,
{
fn handle_buffer<R: BufRead>(mut input: R, options: &NumfmtOptions) -> UResult<()> {
let terminator = if options.zero_terminated { 0u8 } else { b'\n' };
handle_buffer_iterator(input.split(terminator), options, terminator)
}
fn handle_buffer_iterator(
iter: impl Iterator<Item = StdResult<Vec<u8>, Error>>,
options: &NumfmtOptions,
terminator: u8,
) -> UResult<()> {
let mut stdout = std::io::stdout().lock();
for (idx, line_result) in iter.enumerate() {
match line_result {
Ok(line) if idx < options.header => {
stdout.write_all(&line)?;
stdout.write_all(&[terminator])?;
Ok(())
let mut buf = Vec::new();
let mut idx = 0;
loop {
buf.clear();
let n = input
.read_until(terminator, &mut buf)
.map_err(|e| NumfmtError::IoError(e.to_string()))?;
if n == 0 {
break;
}
let has_terminator = buf.last() == Some(&terminator);
let line = if has_terminator {
&buf[..buf.len() - 1]
} else {
&buf[..]
};
// Emit the terminator only if the input line had one.
// i.e. if the last line of the input does not end with a newline, we should not add one.
let eol = has_terminator.then_some(terminator);
if idx < options.header {
stdout.write_all(line)?;
if let Some(t) = eol {
stdout.write_all(&[t])?;
}
Ok(line) => write_line(&mut stdout, &line, options),
Err(err) => return Err(Box::new(NumfmtError::IoError(err.to_string()))),
}?;
} else {
write_line(&mut stdout, line, options, eol)?;
}
idx += 1;
}
Ok(())
}
@@ -71,6 +84,7 @@ fn write_line<W: std::io::Write>(
writer: &mut W,
input_line: &[u8],
options: &NumfmtOptions,
eol: Option<u8>,
) -> UResult<()> {
// Read lines only up to null byte (as GNU does)
let line = input_line
@@ -80,11 +94,11 @@ fn write_line<W: std::io::Write>(
.collect::<Vec<u8>>();
let handled_line = if options.delimiter.is_some() {
write_formatted_with_delimiter(writer, &line, options)
write_formatted_with_delimiter(writer, &line, options, eol)
} else {
// Whitespace mode requires valid UTF-8
match std::str::from_utf8(&line) {
Ok(s) => write_formatted_with_whitespace(writer, s, options),
Ok(s) => write_formatted_with_whitespace(writer, s, options, eol),
Err(_) => Err(translate!("numfmt-error-invalid-input")),
}
};
@@ -104,12 +118,9 @@ fn write_line<W: std::io::Write>(
}
writer.write_all(input_line)?;
let eol = if options.zero_terminated {
b"\0"
} else {
b"\n"
};
writer.write_all(eol)?;
if let Some(eol) = eol {
writer.write_all(&[eol])?;
}
}
Ok(())
+46 -44
View File
@@ -33,7 +33,7 @@ fn test_from_si() {
.args(&["--from=si"])
.pipe_in("1000\n1.1M\n0.1G")
.succeeds()
.stdout_is("1000\n1100000\n100000000\n");
.stdout_is("1000\n1100000\n100000000");
}
#[test]
@@ -42,7 +42,7 @@ fn test_from_iec() {
.args(&["--from=iec"])
.pipe_in("1024\n1.1M\n0.1G")
.succeeds()
.stdout_is("1024\n1153434\n107374183\n");
.stdout_is("1024\n1153434\n107374183");
}
#[test]
@@ -51,7 +51,7 @@ fn test_from_iec_i() {
.args(&["--from=iec-i"])
.pipe_in("1.1Mi\n0.1Gi")
.succeeds()
.stdout_is("1153434\n107374183\n");
.stdout_is("1153434\n107374183");
}
#[test]
@@ -84,7 +84,7 @@ fn test_from_auto() {
.args(&["--from=auto"])
.pipe_in("1K\n1Ki")
.succeeds()
.stdout_is("1000\n1024\n");
.stdout_is("1000\n1024");
}
#[test]
@@ -93,7 +93,7 @@ fn test_to_si() {
.args(&["--to=si"])
.pipe_in("1000\n1100000\n100000000")
.succeeds()
.stdout_is("1.0k\n1.1M\n100M\n");
.stdout_is("1.0k\n1.1M\n100M");
}
#[test]
@@ -102,7 +102,7 @@ fn test_to_iec() {
.args(&["--to=iec"])
.pipe_in("1024\n1153434\n107374182")
.succeeds()
.stdout_is("1.0K\n1.2M\n103M\n");
.stdout_is("1.0K\n1.2M\n103M");
}
#[test]
@@ -111,7 +111,7 @@ fn test_to_iec_i() {
.args(&["--to=iec-i"])
.pipe_in("1024\n1153434\n107374182")
.succeeds()
.stdout_is("1.0Ki\n1.2Mi\n103Mi\n");
.stdout_is("1.0Ki\n1.2Mi\n103Mi");
}
#[test]
@@ -128,7 +128,7 @@ fn test_padding() {
.args(&["--from=si", "--padding=8"])
.pipe_in("1K\n1.1M\n0.1G")
.succeeds()
.stdout_is(" 1000\n 1100000\n100000000\n");
.stdout_is(" 1000\n 1100000\n100000000");
}
#[test]
@@ -137,7 +137,7 @@ fn test_negative_padding() {
.args(&["--from=si", "--padding=-8"])
.pipe_in("1K\n1.1M\n0.1G")
.succeeds()
.stdout_is("1000 \n1100000 \n100000000\n");
.stdout_is("1000 \n1100000 \n100000000");
}
#[test]
@@ -146,7 +146,7 @@ fn test_header() {
.args(&["--from=si", "--header=2"])
.pipe_in("header\nheader2\n1K\n1.1M\n0.1G")
.succeeds()
.stdout_is("header\nheader2\n1000\n1100000\n100000000\n");
.stdout_is("header\nheader2\n1000\n1100000\n100000000");
}
#[test]
@@ -155,7 +155,7 @@ fn test_header_default() {
.args(&["--from=si", "--header"])
.pipe_in("header\n1K\n1.1M\n0.1G")
.succeeds()
.stdout_is("header\n1000\n1100000\n100000000\n");
.stdout_is("header\n1000\n1100000\n100000000");
}
#[test]
@@ -188,12 +188,12 @@ fn test_negative() {
.args(&["--from=si"])
.pipe_in("-1000\n-1.1M\n-0.1G")
.succeeds()
.stdout_is("-1000\n-1100000\n-100000000\n");
.stdout_is("-1000\n-1100000\n-100000000");
new_ucmd!()
.args(&["--to=iec-i"])
.pipe_in("-1024\n-1153434\n-107374182")
.succeeds()
.stdout_is("-1.0Ki\n-1.2Mi\n-103Mi\n");
.stdout_is("-1.0Ki\n-1.2Mi\n-103Mi");
}
#[test]
@@ -201,7 +201,7 @@ fn test_negative_zero() {
new_ucmd!()
.pipe_in("-0\n-0.0")
.succeeds()
.stdout_is("0\n0.0\n");
.stdout_is("0\n0.0");
}
#[test]
@@ -209,7 +209,7 @@ fn test_no_op() {
new_ucmd!()
.pipe_in("1024\n1234567")
.succeeds()
.stdout_is("1024\n1234567\n");
.stdout_is("1024\n1234567");
}
#[test]
@@ -218,7 +218,7 @@ fn test_normalize() {
.args(&["--from=si", "--to=si"])
.pipe_in("10000000K\n0.001K")
.succeeds()
.stdout_is("10G\n1\n");
.stdout_is("10G\n1");
}
#[test]
@@ -322,14 +322,14 @@ fn test_should_skip_leading_space_from_stdin() {
.args(&["--from=auto"])
.pipe_in(" 2Ki")
.succeeds()
.stdout_is("2048\n");
.stdout_is("2048");
// multi-line
new_ucmd!()
.args(&["--from=auto"])
.pipe_in("\t1Ki\n 2K")
.succeeds()
.stdout_is("1024\n2000\n");
.stdout_is("1024\n2000");
}
#[test]
@@ -338,7 +338,7 @@ fn test_should_convert_only_first_number_in_line() {
.args(&["--from=auto"])
.pipe_in("1Ki 2M 3G")
.succeeds()
.stdout_is("1024 2M 3G\n");
.stdout_is("1024 2M 3G");
}
#[test]
@@ -347,13 +347,13 @@ fn test_leading_whitespace_should_imply_padding() {
.args(&["--from=auto"])
.pipe_in(" 1K")
.succeeds()
.stdout_is(" 1000\n");
.stdout_is(" 1000");
new_ucmd!()
.args(&["--from=auto"])
.pipe_in(" 202Ki")
.succeeds()
.stdout_is(" 206848\n");
.stdout_is(" 206848");
}
#[test]
@@ -362,7 +362,7 @@ fn test_should_calculate_implicit_padding_per_line() {
.args(&["--from=auto"])
.pipe_in(" 1Ki\n 2K")
.succeeds()
.stdout_is(" 1024\n 2000\n");
.stdout_is(" 1024\n 2000");
}
#[test]
@@ -520,7 +520,7 @@ fn test_delimiter_only() {
.args(&["-d", ","])
.pipe_in("1234,56")
.succeeds()
.stdout_only("1234,56\n");
.stdout_only("1234,56");
}
#[test]
@@ -529,7 +529,7 @@ fn test_line_is_field_with_no_delimiter() {
.args(&["-d,", "--to=iec"])
.pipe_in("123456")
.succeeds()
.stdout_only("121K\n");
.stdout_only("121K");
}
#[test]
@@ -538,7 +538,7 @@ fn test_delimiter_to_si() {
.args(&["-d=,", "--to=si"])
.pipe_in("1234,56")
.succeeds()
.stdout_only("1.3k,56\n");
.stdout_only("1.3k,56");
}
#[test]
@@ -547,7 +547,7 @@ fn test_delimiter_skips_leading_whitespace() {
.args(&["-d=,", "--to=si"])
.pipe_in(" \t 1234,56")
.succeeds()
.stdout_only("1.3k,56\n");
.stdout_only("1.3k,56");
}
#[test]
@@ -556,7 +556,7 @@ fn test_delimiter_preserves_leading_whitespace_in_unselected_fields() {
.args(&["-d=|", "--to=si"])
.pipe_in(" 1000| 2000")
.succeeds()
.stdout_only("1.0k| 2000\n");
.stdout_only("1.0k| 2000");
}
#[test]
@@ -565,7 +565,7 @@ fn test_delimiter_from_si() {
.args(&["-d=,", "--from=si"])
.pipe_in("1.2K,56")
.succeeds()
.stdout_only("1200,56\n");
.stdout_only("1200,56");
}
#[test]
@@ -583,7 +583,7 @@ fn test_delimiter_with_padding() {
.args(&["-d=|", "--to=si", "--padding=5"])
.pipe_in("1000|2000")
.succeeds()
.stdout_only(" 1.0k|2000\n");
.stdout_only(" 1.0k|2000");
}
#[test]
@@ -592,7 +592,7 @@ fn test_delimiter_with_padding_and_fields() {
.args(&["-d=|", "--to=si", "--padding=5", "--field=-"])
.pipe_in("1000|2000")
.succeeds()
.stdout_only(" 1.0k| 2.0k\n");
.stdout_only(" 1.0k| 2.0k");
}
#[test]
@@ -655,7 +655,7 @@ fn test_suffix_is_added_if_not_supplied() {
.args(&["--suffix=TEST"])
.pipe_in("1000")
.succeeds()
.stdout_only("1000TEST\n");
.stdout_only("1000TEST");
}
#[test]
@@ -664,7 +664,7 @@ fn test_suffix_is_preserved() {
.args(&["--suffix=TEST"])
.pipe_in("1000TEST")
.succeeds()
.stdout_only("1000TEST\n");
.stdout_only("1000TEST");
}
#[test]
@@ -673,7 +673,7 @@ fn test_suffix_is_only_applied_to_selected_field() {
.args(&["--suffix=TEST", "--field=2"])
.pipe_in("1000 2000 3000")
.succeeds()
.stdout_only("1000 2000TEST 3000\n");
.stdout_only("1000 2000TEST 3000");
}
#[test]
@@ -682,7 +682,7 @@ fn test_transform_with_suffix_on_input() {
.args(&["--suffix=b", "--to=si"])
.pipe_in("2000b")
.succeeds()
.stdout_only("2.0kb\n");
.stdout_only("2.0kb");
}
#[test]
@@ -691,7 +691,7 @@ fn test_transform_without_suffix_on_input() {
.args(&["--suffix=b", "--to=si"])
.pipe_in("2000")
.succeeds()
.stdout_only("2.0kb\n");
.stdout_only("2.0kb");
}
#[test]
@@ -700,7 +700,7 @@ fn test_transform_with_suffix_and_delimiter() {
.args(&["--suffix=b", "--to=si", "-d=|"])
.pipe_in("1000b|2000|3000")
.succeeds()
.stdout_only("1.0kb|2000|3000\n");
.stdout_only("1.0kb|2000|3000");
}
#[test]
@@ -709,7 +709,7 @@ fn test_suffix_with_padding() {
.args(&["--suffix=pad", "--padding=12"])
.pipe_in("1000 2000 3000")
.succeeds()
.stdout_only(" 1000pad 2000 3000\n");
.stdout_only(" 1000pad 2000 3000");
}
#[test]
@@ -732,7 +732,7 @@ fn test_invalid_stdin_number_with_warn_returns_status_0() {
.args(&["--invalid=warn"])
.pipe_in("4Q")
.succeeds()
.stdout_is("4Q\n")
.stdout_is("4Q")
.stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n");
}
@@ -742,7 +742,7 @@ fn test_invalid_stdin_number_with_ignore_returns_status_0() {
.args(&["--invalid=ignore"])
.pipe_in("4Q")
.succeeds()
.stdout_only("4Q\n");
.stdout_only("4Q");
}
#[test]
@@ -760,7 +760,7 @@ fn test_invalid_stdin_number_with_fail_returns_status_2() {
.args(&["--invalid=fail"])
.pipe_in("4Q")
.fails_with_code(2)
.stdout_is("4Q\n")
.stdout_is("4Q")
.stderr_is("numfmt: rejecting suffix in input: '4Q' (consider using --from)\n");
}
@@ -1126,7 +1126,7 @@ fn test_zero_terminated_command_line_args() {
#[test]
fn test_zero_terminated_input() {
let values = vec![
("1000", "1.0k\x00"),
("1000", "1.0k"),
("1000\x00", "1.0k\x00"),
("1000\x002000\x00", "1.0k\x002.0k\x00"),
];
@@ -1253,9 +1253,11 @@ fn test_empty_delimiter_whitespace_rejection() {
#[test]
fn test_null_byte_input() {
new_ucmd!()
.pipe_in("1000\x00")
.pipe_in("1000\x00\n")
.succeeds()
.stdout_is("1000\n");
new_ucmd!().pipe_in("1000\x00").succeeds().stdout_is("1000");
}
#[test]
@@ -1263,10 +1265,10 @@ fn test_null_byte_input_multiline() {
new_ucmd!()
.pipe_in("1000\x00\n2000\x00")
.succeeds()
.stdout_is("1000\n2000\n");
.stdout_is("1000\n2000");
new_ucmd!()
.pipe_in("1000\x002000\n3000")
.succeeds()
.stdout_is("1000\n3000\n");
.stdout_is("1000\n3000");
}