Skip to content

Commit 42bf580

Browse files
sbentmarcakebakersylvestre
authored
numfmt: add --invalid option (#4249)
* numfmt: add invalid option * numfmt: return code 0 if ignore or warn * numfmt: implement all --invalid modes * numfmt: validate stdout and stderr * numfmt: remove unnecessary code * numfmt: apply formatting * numfmt: fix clippy issues * numfmt: fix failing test cases * numfmt: fix formatting * numfmt: fix bug when handling broken pipe * numfmt: fix bug where extra newline was added * numfmt: add test cases for edge cases * numfmt: simplify error handling * numfmt: remove redundant if * numfmt: add newline between functions * numfmt: fix failing test cases * numfmt: add support for arg numbers using --invalid * numfmt: simplify error handling in value handlers * numfmt: fix merge conflict and align prints * numfmt: fix clippy suggestion * numfmt: replace "valid" with "invalid" in tests * numfmt: move INVALID to respect alph. order * numfmt: move printlns outside of match to avoid duplication Co-authored-by: Sylvestre Ledru <sledru@mozilla.com> * numfmt: remove empty line --------- Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com> Co-authored-by: Sylvestre Ledru <sledru@mozilla.com>
1 parent a145798 commit 42bf580

3 files changed

Lines changed: 241 additions & 21 deletions

File tree

src/uu/numfmt/src/numfmt.rs

Lines changed: 127 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ use crate::options::*;
1111
use crate::units::{Result, Unit};
1212
use clap::{crate_version, parser::ValueSource, Arg, ArgAction, ArgMatches, Command};
1313
use std::io::{BufRead, Write};
14+
use std::str::FromStr;
15+
1416
use units::{IEC_BASES, SI_BASES};
1517
use uucore::display::Quotable;
16-
use uucore::error::UResult;
18+
use uucore::error::{UError, UResult};
1719
use uucore::ranges::Range;
18-
use uucore::{format_usage, help_about, help_section, help_usage};
20+
use uucore::{format_usage, help_about, help_section, help_usage, show, show_error};
1921

2022
pub mod errors;
2123
pub mod format;
@@ -28,36 +30,50 @@ const USAGE: &str = help_usage!("numfmt.md");
2830

2931
fn handle_args<'a>(args: impl Iterator<Item = &'a str>, options: &NumfmtOptions) -> UResult<()> {
3032
for l in args {
31-
match format_and_print(l, options) {
32-
Ok(_) => Ok(()),
33-
Err(e) => Err(NumfmtError::FormattingError(e.to_string())),
34-
}?;
33+
format_and_handle_validation(l, options)?;
3534
}
36-
3735
Ok(())
3836
}
3937

4038
fn handle_buffer<R>(input: R, options: &NumfmtOptions) -> UResult<()>
4139
where
4240
R: BufRead,
4341
{
44-
let mut lines = input.lines();
45-
for (idx, line) in lines.by_ref().enumerate() {
46-
match line {
47-
Ok(l) if idx < options.header => {
48-
println!("{l}");
42+
for (idx, line_result) in input.lines().by_ref().enumerate() {
43+
match line_result {
44+
Ok(line) if idx < options.header => {
45+
println!("{line}");
4946
Ok(())
5047
}
51-
Ok(l) => match format_and_print(&l, options) {
52-
Ok(_) => Ok(()),
53-
Err(e) => Err(NumfmtError::FormattingError(e.to_string())),
54-
},
55-
Err(e) => Err(NumfmtError::IoError(e.to_string())),
48+
Ok(line) => format_and_handle_validation(line.as_ref(), options),
49+
Err(err) => return Err(Box::new(NumfmtError::IoError(err.to_string()))),
5650
}?;
5751
}
5852
Ok(())
5953
}
6054

55+
fn format_and_handle_validation(input_line: &str, options: &NumfmtOptions) -> UResult<()> {
56+
let handled_line = format_and_print(input_line, options);
57+
58+
if let Err(error_message) = handled_line {
59+
match options.invalid {
60+
InvalidModes::Abort => {
61+
return Err(Box::new(NumfmtError::FormattingError(error_message)));
62+
}
63+
InvalidModes::Fail => {
64+
show!(NumfmtError::FormattingError(error_message));
65+
}
66+
InvalidModes::Warn => {
67+
show_error!("{}", error_message);
68+
}
69+
InvalidModes::Ignore => {}
70+
};
71+
println!("{}", input_line);
72+
}
73+
74+
Ok(())
75+
}
76+
6177
fn parse_unit(s: &str) -> Result<Unit> {
6278
match s {
6379
"auto" => Ok(Unit::Auto),
@@ -201,6 +217,9 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
201217
.get_one::<String>(options::SUFFIX)
202218
.map(|s| s.to_owned());
203219

220+
let invalid =
221+
InvalidModes::from_str(args.get_one::<String>(options::INVALID).unwrap()).unwrap();
222+
204223
Ok(NumfmtOptions {
205224
transform,
206225
padding,
@@ -210,6 +229,7 @@ fn parse_options(args: &ArgMatches) -> Result<NumfmtOptions> {
210229
round,
211230
suffix,
212231
format,
232+
invalid,
213233
})
214234
}
215235

@@ -357,6 +377,17 @@ pub fn uu_app() -> Command {
357377
)
358378
.value_name("SUFFIX"),
359379
)
380+
.arg(
381+
Arg::new(options::INVALID)
382+
.long(options::INVALID)
383+
.help(
384+
"set the failure mode for invalid input; \
385+
valid options are abort, fail, warn or ignore",
386+
)
387+
.default_value("abort")
388+
.value_parser(["abort", "fail", "warn", "ignore"])
389+
.value_name("INVALID"),
390+
)
360391
.arg(
361392
Arg::new(options::NUMBER)
362393
.hide(true)
@@ -366,9 +397,11 @@ pub fn uu_app() -> Command {
366397

367398
#[cfg(test)]
368399
mod tests {
400+
use uucore::error::get_exit_code;
401+
369402
use super::{
370-
handle_buffer, parse_unit_size, parse_unit_size_suffix, FormatOptions, NumfmtOptions,
371-
Range, RoundMethod, TransformOptions, Unit,
403+
handle_args, handle_buffer, parse_unit_size, parse_unit_size_suffix, FormatOptions,
404+
InvalidModes, NumfmtOptions, Range, RoundMethod, TransformOptions, Unit,
372405
};
373406
use std::io::{BufReader, Error, ErrorKind, Read};
374407
struct MockBuffer {}
@@ -394,6 +427,7 @@ mod tests {
394427
round: RoundMethod::Nearest,
395428
suffix: None,
396429
format: FormatOptions::default(),
430+
invalid: InvalidModes::Abort,
397431
}
398432
}
399433

@@ -409,6 +443,20 @@ mod tests {
409443
assert_eq!(result.code(), 1);
410444
}
411445

446+
#[test]
447+
fn broken_buffer_returns_io_error_after_header() {
448+
let mock_buffer = MockBuffer {};
449+
let mut options = get_valid_options();
450+
options.header = 0;
451+
let result = handle_buffer(BufReader::new(mock_buffer), &options)
452+
.expect_err("returned Ok after receiving IO error");
453+
let result_debug = format!("{:?}", result);
454+
let result_display = format!("{}", result);
455+
assert_eq!(result_debug, "IoError(\"broken pipe\")");
456+
assert_eq!(result_display, "broken pipe");
457+
assert_eq!(result.code(), 1);
458+
}
459+
412460
#[test]
413461
fn non_numeric_returns_formatting_error() {
414462
let input_value = b"135\nhello";
@@ -431,6 +479,66 @@ mod tests {
431479
assert!(result.is_ok(), "did not return Ok for valid input");
432480
}
433481

482+
#[test]
483+
fn warn_returns_ok_for_invalid_input() {
484+
let input_value = b"5\n4Q\n";
485+
let mut options = get_valid_options();
486+
options.invalid = InvalidModes::Warn;
487+
let result = handle_buffer(BufReader::new(&input_value[..]), &options);
488+
assert!(result.is_ok(), "did not return Ok for invalid input");
489+
}
490+
491+
#[test]
492+
fn ignore_returns_ok_for_invalid_input() {
493+
let input_value = b"5\n4Q\n";
494+
let mut options = get_valid_options();
495+
options.invalid = InvalidModes::Ignore;
496+
let result = handle_buffer(BufReader::new(&input_value[..]), &options);
497+
assert!(result.is_ok(), "did not return Ok for invalid input");
498+
}
499+
500+
#[test]
501+
fn buffer_fail_returns_status_2_for_invalid_input() {
502+
let input_value = b"5\n4Q\n";
503+
let mut options = get_valid_options();
504+
options.invalid = InvalidModes::Fail;
505+
handle_buffer(BufReader::new(&input_value[..]), &options).unwrap();
506+
assert!(
507+
get_exit_code() == 2,
508+
"should set exit code 2 for formatting errors"
509+
);
510+
}
511+
512+
#[test]
513+
fn abort_returns_status_2_for_invalid_input() {
514+
let input_value = b"5\n4Q\n";
515+
let mut options = get_valid_options();
516+
options.invalid = InvalidModes::Abort;
517+
let result = handle_buffer(BufReader::new(&input_value[..]), &options);
518+
assert!(result.is_err(), "did not return err for invalid input");
519+
}
520+
521+
#[test]
522+
fn args_fail_returns_status_2_for_invalid_input() {
523+
let input_value = ["5", "4Q"].into_iter();
524+
let mut options = get_valid_options();
525+
options.invalid = InvalidModes::Fail;
526+
handle_args(input_value, &options).unwrap();
527+
assert!(
528+
get_exit_code() == 2,
529+
"should set exit code 2 for formatting errors"
530+
);
531+
}
532+
533+
#[test]
534+
fn args_warn_returns_status_0_for_invalid_input() {
535+
let input_value = ["5", "4Q"].into_iter();
536+
let mut options = get_valid_options();
537+
options.invalid = InvalidModes::Warn;
538+
let result = handle_args(input_value, &options);
539+
assert!(result.is_ok(), "did not return ok for invalid input");
540+
}
541+
434542
#[test]
435543
fn test_parse_unit_size() {
436544
assert_eq!(1, parse_unit_size("1").unwrap());

src/uu/numfmt/src/options.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub const FROM_UNIT: &str = "from-unit";
1313
pub const FROM_UNIT_DEFAULT: &str = "1";
1414
pub const HEADER: &str = "header";
1515
pub const HEADER_DEFAULT: &str = "1";
16+
pub const INVALID: &str = "invalid";
1617
pub const NUMBER: &str = "NUMBER";
1718
pub const PADDING: &str = "padding";
1819
pub const ROUND: &str = "round";
@@ -29,6 +30,14 @@ pub struct TransformOptions {
2930
pub to_unit: usize,
3031
}
3132

33+
#[derive(Debug, PartialEq, Eq)]
34+
pub enum InvalidModes {
35+
Abort,
36+
Fail,
37+
Warn,
38+
Ignore,
39+
}
40+
3241
pub struct NumfmtOptions {
3342
pub transform: TransformOptions,
3443
pub padding: isize,
@@ -38,6 +47,7 @@ pub struct NumfmtOptions {
3847
pub round: RoundMethod,
3948
pub suffix: Option<String>,
4049
pub format: FormatOptions,
50+
pub invalid: InvalidModes,
4151
}
4252

4353
#[derive(Clone, Copy)]
@@ -227,6 +237,20 @@ impl FromStr for FormatOptions {
227237
}
228238
}
229239

240+
impl FromStr for InvalidModes {
241+
type Err = String;
242+
243+
fn from_str(s: &str) -> Result<Self, Self::Err> {
244+
match s.to_lowercase().as_str() {
245+
"abort" => Ok(Self::Abort),
246+
"fail" => Ok(Self::Fail),
247+
"warn" => Ok(Self::Warn),
248+
"ignore" => Ok(Self::Ignore),
249+
unknown => Err(format!("Unknown invalid mode: {unknown}")),
250+
}
251+
}
252+
}
253+
230254
#[cfg(test)]
231255
mod tests {
232256
use super::*;
@@ -336,4 +360,21 @@ mod tests {
336360
assert_eq!(expected_options, "%0'0'0'f".parse().unwrap());
337361
assert_eq!(expected_options, "%'0'0'0f".parse().unwrap());
338362
}
363+
364+
#[test]
365+
fn test_set_invalid_mode() {
366+
assert_eq!(Ok(InvalidModes::Abort), InvalidModes::from_str("abort"));
367+
assert_eq!(Ok(InvalidModes::Abort), InvalidModes::from_str("ABORT"));
368+
369+
assert_eq!(Ok(InvalidModes::Fail), InvalidModes::from_str("fail"));
370+
assert_eq!(Ok(InvalidModes::Fail), InvalidModes::from_str("FAIL"));
371+
372+
assert_eq!(Ok(InvalidModes::Ignore), InvalidModes::from_str("ignore"));
373+
assert_eq!(Ok(InvalidModes::Ignore), InvalidModes::from_str("IGNORE"));
374+
375+
assert_eq!(Ok(InvalidModes::Warn), InvalidModes::from_str("warn"));
376+
assert_eq!(Ok(InvalidModes::Warn), InvalidModes::from_str("WARN"));
377+
378+
assert!(InvalidModes::from_str("something unknown").is_err());
379+
}
339380
}

tests/by-util/test_numfmt.rs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -666,8 +666,79 @@ fn test_invalid_stdin_number_in_middle_of_input() {
666666
}
667667

668668
#[test]
669-
fn test_invalid_argument_number_returns_status_2() {
670-
new_ucmd!().args(&["hello"]).fails().code_is(2);
669+
fn test_invalid_stdin_number_with_warn_returns_status_0() {
670+
new_ucmd!()
671+
.args(&["--invalid=warn"])
672+
.pipe_in("4Q")
673+
.succeeds()
674+
.stdout_is("4Q\n")
675+
.stderr_is("numfmt: invalid suffix in input: '4Q'\n");
676+
}
677+
678+
#[test]
679+
fn test_invalid_stdin_number_with_ignore_returns_status_0() {
680+
new_ucmd!()
681+
.args(&["--invalid=ignore"])
682+
.pipe_in("4Q")
683+
.succeeds()
684+
.stdout_only("4Q\n");
685+
}
686+
687+
#[test]
688+
fn test_invalid_stdin_number_with_abort_returns_status_2() {
689+
new_ucmd!()
690+
.args(&["--invalid=abort"])
691+
.pipe_in("4Q")
692+
.fails()
693+
.code_is(2)
694+
.stderr_only("numfmt: invalid suffix in input: '4Q'\n");
695+
}
696+
697+
#[test]
698+
fn test_invalid_stdin_number_with_fail_returns_status_2() {
699+
new_ucmd!()
700+
.args(&["--invalid=fail"])
701+
.pipe_in("4Q")
702+
.fails()
703+
.code_is(2)
704+
.stdout_is("4Q\n")
705+
.stderr_is("numfmt: invalid suffix in input: '4Q'\n");
706+
}
707+
708+
#[test]
709+
fn test_invalid_arg_number_with_warn_returns_status_0() {
710+
new_ucmd!()
711+
.args(&["--invalid=warn", "4Q"])
712+
.succeeds()
713+
.stdout_is("4Q\n")
714+
.stderr_is("numfmt: invalid suffix in input: '4Q'\n");
715+
}
716+
717+
#[test]
718+
fn test_invalid_arg_number_with_ignore_returns_status_0() {
719+
new_ucmd!()
720+
.args(&["--invalid=ignore", "4Q"])
721+
.succeeds()
722+
.stdout_only("4Q\n");
723+
}
724+
725+
#[test]
726+
fn test_invalid_arg_number_with_abort_returns_status_2() {
727+
new_ucmd!()
728+
.args(&["--invalid=abort", "4Q"])
729+
.fails()
730+
.code_is(2)
731+
.stderr_only("numfmt: invalid suffix in input: '4Q'\n");
732+
}
733+
734+
#[test]
735+
fn test_invalid_arg_number_with_fail_returns_status_2() {
736+
new_ucmd!()
737+
.args(&["--invalid=fail", "4Q"])
738+
.fails()
739+
.code_is(2)
740+
.stdout_is("4Q\n")
741+
.stderr_is("numfmt: invalid suffix in input: '4Q'\n");
671742
}
672743

673744
#[test]

0 commit comments

Comments
 (0)