diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index b5b0f3ed5d8..9c8c2670fff 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 settings_failed = 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(|_| settings_failed = true), Err(err) => Err(err.into()), }; @@ -465,6 +467,14 @@ 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. + // 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))? { 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..fde26115ccd 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -5141,6 +5141,47 @@ 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() { + 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")]