Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/uu/numfmt/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,12 +392,20 @@ fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> {

print!("{}", format_string(field, options, implicit_padding)?);
} 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
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably just some implementation detail, but one of the GNU compatibility tests depends on this behavior. As per the guidelines, I haven't examined the GNU implementation for numfmt, but here are some manual tests I did for compatibility:

$ printf "A\nB 1001 C\x00D E\n2002 F\x00" | ./src/numfmt -z --field=3 --to=si | cat --show-all # official test
A B 1.1k C^@D E 2.1k F^@⏎
$ printf "A \nB 1001 C\x00D E\n2002 F\x00" | ./src/numfmt -z --field=3 --to=si | cat --show-all
A $
B 1.1k C^@D E 2.1k F^@⏎
$ printf "A\n B 1001 C\x00D E\n2002 F\x00" | ./src/numfmt -z --field=3 --to=si | cat --show-all
A  B 1.1k C^@D E 2.1k F^@⏎

The above output is from the GNU version of numfmt, but it's identical to the uutils version output (with this patch of course).

// print unselected field without conversion
print!("{prefix}{field}");
}
}

println!();
let eol = if options.zero_terminated { '\0' } else { '\n' };
print!("{}", eol);

Ok(())
}
Expand Down
37 changes: 34 additions & 3 deletions src/uu/numfmt/src/numfmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use crate::format::format_and_print;
use crate::options::*;
use crate::units::{Result, Unit};
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 units::{IEC_BASES, SI_BASES};
Expand Down Expand Up @@ -38,10 +39,29 @@ fn handle_buffer<R>(input: R, options: &NumfmtOptions) -> UResult<()>
where
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 {
Ok(line) if idx < options.header => {
println!("{line}");
print!("{line}{eol}");
Ok(())
}
Ok(line) => format_and_handle_validation(line.as_ref(), options),
Expand Down Expand Up @@ -217,6 +237,8 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
let invalid =
InvalidModes::from_str(args.get_one::<String>(options::INVALID).unwrap()).unwrap();

let zero_terminated = args.get_flag(options::ZERO_TERMINATED);

Ok(NumfmtOptions {
transform,
padding,
Expand All @@ -227,6 +249,7 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
suffix,
format,
invalid,
zero_terminated,
})
}

Expand Down Expand Up @@ -366,6 +389,13 @@ pub fn uu_app() -> Command {
.value_parser(["abort", "fail", "warn", "ignore"])
.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::new(options::NUMBER)
.hide(true)
Expand Down Expand Up @@ -406,6 +436,7 @@ mod tests {
suffix: None,
format: FormatOptions::default(),
invalid: InvalidModes::Abort,
zero_terminated: false,
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/uu/numfmt/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub const TO: &str = "to";
pub const TO_DEFAULT: &str = "none";
pub const TO_UNIT: &str = "to-unit";
pub const TO_UNIT_DEFAULT: &str = "1";
pub const ZERO_TERMINATED: &str = "zero-terminated";

pub struct TransformOptions {
pub from: Unit,
Expand All @@ -52,6 +53,7 @@ pub struct NumfmtOptions {
pub suffix: Option<String>,
pub format: FormatOptions,
pub invalid: InvalidModes,
pub zero_terminated: bool,
}

#[derive(Clone, Copy)]
Expand Down
45 changes: 45 additions & 0 deletions tests/by-util/test_numfmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,3 +1073,48 @@ fn test_format_grouping_conflicts_with_to_option() {
.fails_with_code(1)
.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");
}
Loading