From fe7650a223837a83f02b1fdf42ff1f5d01e39eba Mon Sep 17 00:00:00 2001 From: emirmrkaljevic <93012621+emirmrkaljevic@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:14:55 +0200 Subject: [PATCH 1/2] tail: accept obsolete -cf and allow only one trailing f Fixes #15517 --- src/uu/tail/src/args.rs | 11 +++++++++-- src/uu/tail/src/parse.rs | 12 +++++++++--- tests/by-util/test_tail.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index b5b0f3ed5d8..4fea95f787d 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -428,13 +428,15 @@ fn parse_num(src: &str) -> Result { pub fn parse_args(args: impl uucore::Args) -> UResult { let args_vec: Vec = args.collect(); let clap_args = uu_app().try_get_matches_from(args_vec.clone()); + let mut invalid_value = false; let clap_result = match clap_args { // Kept for the caret in size diagnostics, which needs the value as // typed. - Ok(matches) => Ok(Settings::from_with_diagnostics( + Ok(matches) => Settings::from_with_diagnostics( &matches, uucore::diagnostics::capture(&args_vec).as_deref(), - )?), + ) + .inspect_err(|_| invalid_value = true), Err(err) => Err(err.into()), }; @@ -465,6 +467,11 @@ pub fn parse_args(args: impl uucore::Args) -> UResult { if clap_result.is_ok() && possible_obsolete_args.as_encoded_bytes().first() != Some(&b'+') { return clap_result; } + // `tail -cf` is obsolete syntax even though clap reads it as `-c f`, + // but in `tail -c f` the separate argument is always the count. + if invalid_value && possible_obsolete_args.len() <= 2 { + return clap_result; + } match parse_obsolete(possible_obsolete_args, args_vec.get(2))? { Some(settings) => Ok(settings), None => clap_result, diff --git a/src/uu/tail/src/parse.rs b/src/uu/tail/src/parse.rs index a95999a5ec2..0b0f97529b9 100644 --- a/src/uu/tail/src/parse.rs +++ b/src/uu/tail/src/parse.rs @@ -68,9 +68,9 @@ pub fn parse_obsolete(src: &OsString) -> Option 'l' }; - let follow = rest.contains('f'); - if !rest.chars().all(|f| f == 'f') { - // GNU allows an arbitrary amount of following fs, but nothing else + let follow = rest == "f"; + if !follow && !rest.is_empty() { + // At most one trailing 'f' is allowed if sign == '-' && has_num { return Some(Err(ParseError::Context)); } @@ -168,11 +168,17 @@ mod tests { parse_obsolete(&OsString::from("-1vzqvq")), // spell-checker:disable-line Some(Err(ParseError::Context)) ); + assert_eq!( + parse_obsolete(&OsString::from("-5ff")), + Some(Err(ParseError::Context)) + ); } #[test] fn test_parse_obsolete_no_match() { assert_eq!(parse_obsolete(&OsString::from("-k")), None); assert_eq!(parse_obsolete(&OsString::from("asd")), None); assert_eq!(parse_obsolete(&OsString::from("-cc")), None); + assert_eq!(parse_obsolete(&OsString::from("-cff")), None); + assert_eq!(parse_obsolete(&OsString::from("+ff")), None); } } diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 9758f5cf7ca..35351913a29 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -5141,6 +5141,36 @@ fn test_tail_obsolete_f_flag() { p.kill().make_assertion().with_all_output().no_output(); } +#[test] +#[cfg_attr(wasi_runner, ignore = "WASI: tail follow mode disabled")] +fn test_tail_obsolete_bytes_follow() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.write("file", "abcdefghijklmnop\n"); + let mut p = scene.ucmd().args(&["-cf", "file"]).run_no_wait(); + p.make_assertion_with_delay(500).is_alive(); + p.kill() + .make_assertion() + .with_all_output() + .stdout_only("hijklmnop\n"); + + scene + .ucmd() + .arg("-c") + .arg("f") + .fails_with_code(1) + .no_stdout() + .stderr_is("tail: invalid number of bytes: 'f'\n"); + + scene + .ucmd() + .args(&["-5ff", "file"]) + .fails_with_code(1) + .no_stdout() + .stderr_is("tail: option used in invalid context -- 5\n"); +} + #[test] #[cfg(unix)] #[cfg_attr(wasi_runner, ignore = "WASI: argv/filenames must be valid UTF-8")] From 41cb88bfeb7f3ca93971eca589b71b624cb41fb8 Mon Sep 17 00:00:00 2001 From: emirmrkaljevic <93012621+emirmrkaljevic@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:39:15 +0200 Subject: [PATCH 2/2] tail: clarify obsolete -c fallback and test -cf on stdin --- src/uu/tail/src/args.rs | 9 ++++++--- tests/by-util/test_tail.rs | 11 +++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index 4fea95f787d..9c8c2670fff 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -428,7 +428,7 @@ fn parse_num(src: &str) -> Result { pub fn parse_args(args: impl uucore::Args) -> UResult { let args_vec: Vec = args.collect(); let clap_args = uu_app().try_get_matches_from(args_vec.clone()); - let mut invalid_value = false; + let mut settings_failed = false; let clap_result = match clap_args { // Kept for the caret in size diagnostics, which needs the value as // typed. @@ -436,7 +436,7 @@ pub fn parse_args(args: impl uucore::Args) -> UResult { &matches, uucore::diagnostics::capture(&args_vec).as_deref(), ) - .inspect_err(|_| invalid_value = true), + .inspect_err(|_| settings_failed = true), Err(err) => Err(err.into()), }; @@ -469,7 +469,10 @@ pub fn parse_args(args: impl uucore::Args) -> UResult { } // `tail -cf` is obsolete syntax even though clap reads it as `-c f`, // but in `tail -c f` the separate argument is always the count. - if invalid_value && possible_obsolete_args.len() <= 2 { + // A bare short option such as `-c` is two bytes long: no value is + // attached to it, so the value came from the next argument. + let no_value_attached = possible_obsolete_args.len() <= "-c".len(); + if settings_failed && no_value_attached { return clap_result; } match parse_obsolete(possible_obsolete_args, args_vec.get(2))? { diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 35351913a29..fde26115ccd 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -5141,6 +5141,17 @@ fn test_tail_obsolete_f_flag() { p.kill().make_assertion().with_all_output().no_output(); } +#[test] +#[cfg_attr(wasi_runner, ignore = "WASI: tail follow mode disabled")] +fn test_tail_obsolete_bytes_follow_stdin() { + // `-f` is ignored when stdin is a pipe, so tail exits after printing. + new_ucmd!() + .arg("-cf") + .pipe_in("abcdefghijklmnop\n") + .succeeds() + .stdout_only("hijklmnop\n"); +} + #[test] #[cfg_attr(wasi_runner, ignore = "WASI: tail follow mode disabled")] fn test_tail_obsolete_bytes_follow() {