Skip to content

Commit ea20122

Browse files
committed
numfmt: add --zero-terminated option
1 parent f1f3a5d commit ea20122

File tree

4 files changed

+89
-4
lines changed

4 files changed

+89
-4
lines changed

src/uu/numfmt/src/format.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,12 +392,20 @@ fn format_and_print_whitespace(s: &str, options: &NumfmtOptions) -> Result<()> {
392392

393393
print!("{}", format_string(field, options, implicit_padding)?);
394394
} else {
395+
// the -z option converts an initial \n into a space
396+
let prefix = if options.zero_terminated && prefix.starts_with('\n') {
397+
print!(" ");
398+
&prefix[1..]
399+
} else {
400+
prefix
401+
};
395402
// print unselected field without conversion
396403
print!("{prefix}{field}");
397404
}
398405
}
399406

400-
println!();
407+
let eol = if options.zero_terminated { '\0' } else { '\n' };
408+
print!("{}", eol);
401409

402410
Ok(())
403411
}

src/uu/numfmt/src/numfmt.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use crate::format::format_and_print;
88
use crate::options::*;
99
use crate::units::{Result, Unit};
1010
use clap::{Arg, ArgAction, ArgMatches, Command, parser::ValueSource};
11-
use std::io::{BufRead, Write};
11+
use std::io::{BufRead, Error, Write};
12+
use std::result::Result as StdResult;
1213
use std::str::FromStr;
1314

1415
use units::{IEC_BASES, SI_BASES};
@@ -38,10 +39,29 @@ fn handle_buffer<R>(input: R, options: &NumfmtOptions) -> UResult<()>
3839
where
3940
R: BufRead,
4041
{
41-
for (idx, line_result) in input.lines().by_ref().enumerate() {
42+
if options.zero_terminated {
43+
handle_buffer_iterator(
44+
input
45+
.split(0)
46+
// FIXME: This panics on UTF8 decoding, but this util in general doesn't handle
47+
// invalid UTF8
48+
.map(|bytes| Ok(String::from_utf8(bytes?).unwrap())),
49+
options,
50+
)
51+
} else {
52+
handle_buffer_iterator(input.lines(), options)
53+
}
54+
}
55+
56+
fn handle_buffer_iterator(
57+
iter: impl Iterator<Item = StdResult<String, Error>>,
58+
options: &NumfmtOptions,
59+
) -> UResult<()> {
60+
let eol = if options.zero_terminated { '\0' } else { '\n' };
61+
for (idx, line_result) in iter.enumerate() {
4262
match line_result {
4363
Ok(line) if idx < options.header => {
44-
println!("{line}");
64+
print!("{line}{eol}");
4565
Ok(())
4666
}
4767
Ok(line) => format_and_handle_validation(line.as_ref(), options),
@@ -217,6 +237,8 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
217237
let invalid =
218238
InvalidModes::from_str(args.get_one::<String>(options::INVALID).unwrap()).unwrap();
219239

240+
let zero_terminated = args.get_flag(options::ZERO_TERMINATED);
241+
220242
Ok(NumfmtOptions {
221243
transform,
222244
padding,
@@ -227,6 +249,7 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
227249
suffix,
228250
format,
229251
invalid,
252+
zero_terminated,
230253
})
231254
}
232255

@@ -366,6 +389,13 @@ pub fn uu_app() -> Command {
366389
.value_parser(["abort", "fail", "warn", "ignore"])
367390
.value_name("INVALID"),
368391
)
392+
.arg(
393+
Arg::new(options::ZERO_TERMINATED)
394+
.long(options::ZERO_TERMINATED)
395+
.short('z')
396+
.help("line delimiter is NUL, not newline")
397+
.action(ArgAction::SetTrue),
398+
)
369399
.arg(
370400
Arg::new(options::NUMBER)
371401
.hide(true)

src/uu/numfmt/src/options.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub const TO: &str = "to";
2626
pub const TO_DEFAULT: &str = "none";
2727
pub const TO_UNIT: &str = "to-unit";
2828
pub const TO_UNIT_DEFAULT: &str = "1";
29+
pub const ZERO_TERMINATED: &str = "zero-terminated";
2930

3031
pub struct TransformOptions {
3132
pub from: Unit,
@@ -52,6 +53,7 @@ pub struct NumfmtOptions {
5253
pub suffix: Option<String>,
5354
pub format: FormatOptions,
5455
pub invalid: InvalidModes,
56+
pub zero_terminated: bool,
5557
}
5658

5759
#[derive(Clone, Copy)]

tests/by-util/test_numfmt.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,3 +1073,48 @@ fn test_format_grouping_conflicts_with_to_option() {
10731073
.fails_with_code(1)
10741074
.stderr_contains("grouping cannot be combined with --to");
10751075
}
1076+
1077+
#[test]
1078+
fn test_zero_terminated_command_line_args() {
1079+
new_ucmd!()
1080+
.args(&["--zero-terminated", "--to=si", "1000"])
1081+
.succeeds()
1082+
.stdout_is("1.0k\x00");
1083+
1084+
new_ucmd!()
1085+
.args(&["-z", "--to=si", "1000"])
1086+
.succeeds()
1087+
.stdout_is("1.0k\x00");
1088+
1089+
new_ucmd!()
1090+
.args(&["-z", "--to=si", "1000", "2000"])
1091+
.succeeds()
1092+
.stdout_is("1.0k\x002.0k\x00");
1093+
}
1094+
1095+
#[test]
1096+
fn test_zero_terminated_input() {
1097+
let values = vec![
1098+
("1000", "1.0k\x00"),
1099+
("1000\x00", "1.0k\x00"),
1100+
("1000\x002000\x00", "1.0k\x002.0k\x00"),
1101+
];
1102+
1103+
for (input, expected) in values {
1104+
new_ucmd!()
1105+
.args(&["-z", "--to=si"])
1106+
.pipe_in(input)
1107+
.succeeds()
1108+
.stdout_is(expected);
1109+
}
1110+
}
1111+
1112+
#[test]
1113+
fn test_zero_terminated_embedded_newline() {
1114+
new_ucmd!()
1115+
.args(&["-z", "--from=si", "--field=-"])
1116+
.pipe_in("1K\n2K\x003K\n4K\x00")
1117+
.succeeds()
1118+
// Newlines get replaced by a single space
1119+
.stdout_is("1000 2000\x003000 4000\x00");
1120+
}

0 commit comments

Comments
 (0)