diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 41fe5bf895c..adcd541d616 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -945,7 +945,14 @@ impl Stater { /// Returns the tokens parsed before the bad one, so the caller can print /// them the way GNU does, and the error that stopped the parse. A format /// that ends in a bad directive keeps no trailing newline. - fn generate_tokens(format_str: &str, use_printf: bool) -> (Vec, Option) { + /// + /// `trailing_newline` adds the newline GNU prints after a `--format` + /// output, even one that already ends with a newline. + fn generate_tokens( + format_str: &str, + use_printf: bool, + trailing_newline: bool, + ) -> (Vec, Option) { let mut tokens = Vec::new(); let chars = format_str.chars().collect::>(); let bound = chars.len(); @@ -970,7 +977,7 @@ impl Stater { } i += 1; } - if !use_printf && !format_str.ends_with('\n') { + if trailing_newline { tokens.push(Token::Char('\n')); } (tokens, None) @@ -1036,19 +1043,25 @@ impl Stater { // error there is our bug, has nothing to point at, and is raised now. let mut format_error = None; let default_tokens = if format_str.is_empty() { - let (tokens, error) = - Self::generate_tokens(&Self::default_format(show_fs, terse, false), use_printf); + let (tokens, error) = Self::generate_tokens( + &Self::default_format(show_fs, terse, false), + use_printf, + false, + ); if let Some(error) = error { return Err(error.to_error(diag_args, None)); } tokens } else { - let (tokens, error) = Self::generate_tokens(format_str, use_printf); + let (tokens, error) = Self::generate_tokens(format_str, use_printf, !use_printf); format_error = error; tokens }; - let (default_dev_tokens, default_dev_error) = - Self::generate_tokens(&Self::default_format(show_fs, terse, true), use_printf); + let (default_dev_tokens, default_dev_error) = Self::generate_tokens( + &Self::default_format(show_fs, terse, true), + use_printf, + false, + ); if let Some(error) = default_dev_error { return Err(error.to_error(diag_args, None)); } @@ -1622,8 +1635,10 @@ mod tests { format: 'w', }, Token::Char('\n'), + // --format adds its own newline after the one the format ends with + Token::Char('\n'), ]; - let (tokens, error) = Stater::generate_tokens(s, false); + let (tokens, error) = Stater::generate_tokens(s, false, true); assert!(error.is_none()); assert_eq!(&expected, &tokens); } @@ -1668,7 +1683,7 @@ mod tests { Token::Byte(b'J'), Token::Byte(b'\n'), ]; - let (tokens, error) = Stater::generate_tokens(s, true); + let (tokens, error) = Stater::generate_tokens(s, true, false); assert!(error.is_none()); assert_eq!(&expected, &tokens); } diff --git a/tests/by-util/test_stat.rs b/tests/by-util/test_stat.rs index 2f7c3157723..3581efb2cb7 100644 --- a/tests/by-util/test_stat.rs +++ b/tests/by-util/test_stat.rs @@ -774,6 +774,40 @@ fn test_printf_invalid_directive() { .stderr_contains("'%9%': invalid directive"); } +#[test] +fn test_format_always_ends_with_a_newline() { + // GNU adds the newline after --format output even when the format ends in one. + new_ucmd!() + .args(&["-c", "%n\n", "."]) + .succeeds() + .stdout_only(".\n\n"); + new_ucmd!() + .args(&["--printf=%n\n", "."]) + .succeeds() + .stdout_only(".\n"); +} + +#[test] +fn test_default_formats_end_with_one_newline() { + let mut cases: Vec<&[&str]> = vec![&["."], &["-t", "."]]; + // `stat -f` needs the file system type, which is not implemented everywhere. + if cfg!(any( + target_vendor = "apple", + target_os = "linux", + target_os = "android", + target_os = "freebsd" + )) { + cases.extend([&["-f", "."][..], &["-f", "-t", "."]]); + } + for args in cases { + let out = new_ucmd!().args(args).succeeds().stdout_move_str(); + assert!( + out.ends_with('\n') && !out.ends_with("\n\n"), + "{args:?} should end with exactly one newline: {out:?}" + ); + } +} + #[test] fn test_invalid_directive_after_multibyte_char() { let ts = TestScenario::new(util_name!());