1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

numfmt: add --zero-terminated option

This commit is contained in:
Dan Hipschman 2025-04-07 11:45:42 -07:00
parent f1f3a5d9d2
commit 81911f9f6a
4 changed files with 90 additions and 4 deletions

View file

@ -392,12 +392,20 @@ fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> {
print!("{}", format_string(field, options, implicit_padding)?); print!("{}", format_string(field, options, implicit_padding)?);
} else { } else {
// the -z option converts an initial \n into a space
let prefix = if options.zero_terminated && prefix.starts_with('\n') {
print!(" ");
&prefix[1..]
} else {
prefix
};
// print unselected field without conversion // print unselected field without conversion
print!("{prefix}{field}"); print!("{prefix}{field}");
} }
} }
println!(); let eol = if options.zero_terminated { '\0' } else { '\n' };
print!("{}", eol);
Ok(()) Ok(())
} }

View file

@ -8,7 +8,8 @@ use crate::format::format_and_print;
use crate::options::*; use crate::options::*;
use crate::units::{Result, Unit}; use crate::units::{Result, Unit};
use clap::{Arg, ArgAction, ArgMatches, Command, parser::ValueSource}; use clap::{Arg, ArgAction, ArgMatches, Command, parser::ValueSource};
use std::io::{BufRead, Write}; use std::io::{BufRead, Error, Write};
use std::result::Result as StdResult;
use std::str::FromStr; use std::str::FromStr;
use units::{IEC_BASES, SI_BASES}; use units::{IEC_BASES, SI_BASES};
@ -38,10 +39,29 @@ fn handle_buffer<R>(input: R, options: &NumfmtOptions) -> UResult<()>
where where
R: BufRead, R: BufRead,
{ {
for (idx, line_result) in input.lines().by_ref().enumerate() { if options.zero_terminated {
handle_buffer_iterator(
input
.split(0)
// FIXME: This panics on UTF8 decoding, but this util in general doesn't handle
// invalid UTF8
.map(|bytes| Ok(String::from_utf8(bytes?).unwrap())),
options,
)
} else {
handle_buffer_iterator(input.lines(), options)
}
}
fn handle_buffer_iterator(
iter: impl Iterator<Item = StdResult<String, Error>>,
options: &NumfmtOptions,
) -> UResult<()> {
let eol = if options.zero_terminated { '\0' } else { '\n' };
for (idx, line_result) in iter.enumerate() {
match line_result { match line_result {
Ok(line) if idx < options.header => { Ok(line) if idx < options.header => {
println!("{line}"); print!("{line}{eol}");
Ok(()) Ok(())
} }
Ok(line) => format_and_handle_validation(line.as_ref(), options), Ok(line) => format_and_handle_validation(line.as_ref(), options),
@ -217,6 +237,8 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
let invalid = let invalid =
InvalidModes::from_str(args.get_one::<String>(options::INVALID).unwrap()).unwrap(); InvalidModes::from_str(args.get_one::<String>(options::INVALID).unwrap()).unwrap();
let zero_terminated = args.get_flag(options::ZERO_TERMINATED);
Ok(NumfmtOptions { Ok(NumfmtOptions {
transform, transform,
padding, padding,
@ -227,6 +249,7 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
suffix, suffix,
format, format,
invalid, invalid,
zero_terminated,
}) })
} }
@ -366,6 +389,13 @@ pub fn uu_app() -> Command {
.value_parser(["abort", "fail", "warn", "ignore"]) .value_parser(["abort", "fail", "warn", "ignore"])
.value_name("INVALID"), .value_name("INVALID"),
) )
.arg(
Arg::new(options::ZERO_TERMINATED)
.long(options::ZERO_TERMINATED)
.short('z')
.help("line delimiter is NUL, not newline")
.action(ArgAction::SetTrue),
)
.arg( .arg(
Arg::new(options::NUMBER) Arg::new(options::NUMBER)
.hide(true) .hide(true)
@ -406,6 +436,7 @@ mod tests {
suffix: None, suffix: None,
format: FormatOptions::default(), format: FormatOptions::default(),
invalid: InvalidModes::Abort, invalid: InvalidModes::Abort,
zero_terminated: false,
} }
} }

View file

@ -26,6 +26,7 @@ pub const TO: &str = "to";
pub const TO_DEFAULT: &str = "none"; pub const TO_DEFAULT: &str = "none";
pub const TO_UNIT: &str = "to-unit"; pub const TO_UNIT: &str = "to-unit";
pub const TO_UNIT_DEFAULT: &str = "1"; pub const TO_UNIT_DEFAULT: &str = "1";
pub const ZERO_TERMINATED: &str = "zero-terminated";
pub struct TransformOptions { pub struct TransformOptions {
pub from: Unit, pub from: Unit,
@ -52,6 +53,7 @@ pub struct NumfmtOptions {
pub suffix: Option<String>, pub suffix: Option<String>,
pub format: FormatOptions, pub format: FormatOptions,
pub invalid: InvalidModes, pub invalid: InvalidModes,
pub zero_terminated: bool,
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]

View file

@ -1073,3 +1073,48 @@ fn test_format_grouping_conflicts_with_to_option() {
.fails_with_code(1) .fails_with_code(1)
.stderr_contains("grouping cannot be combined with --to"); .stderr_contains("grouping cannot be combined with --to");
} }
#[test]
fn test_zero_terminated_command_line_args() {
new_ucmd!()
.args(&["--zero-terminated", "--to=si", "1000"])
.succeeds()
.stdout_is("1.0k\x00");
new_ucmd!()
.args(&["-z", "--to=si", "1000"])
.succeeds()
.stdout_is("1.0k\x00");
new_ucmd!()
.args(&["-z", "--to=si", "1000", "2000"])
.succeeds()
.stdout_is("1.0k\x002.0k\x00");
}
#[test]
fn test_zero_terminated_input() {
let values = vec![
("1000", "1.0k\x00"),
("1000\x00", "1.0k\x00"),
("1000\x002000\x00", "1.0k\x002.0k\x00"),
];
for (input, expected) in values {
new_ucmd!()
.args(&["-z", "--to=si"])
.pipe_in(input)
.succeeds()
.stdout_is(expected);
}
}
#[test]
fn test_zero_terminated_embedded_newline() {
new_ucmd!()
.args(&["-z", "--from=si", "--field=-"])
.pipe_in("1K\n2K\x003K\n4K\x00")
.succeeds()
// Newlines get replaced by a single space
.stdout_is("1000 2000\x003000 4000\x00");
}