diff --git a/src/uu/tail/src/args.rs b/src/uu/tail/src/args.rs index 6cb77757c..8e039b5f4 100644 --- a/src/uu/tail/src/args.rs +++ b/src/uu/tail/src/args.rs @@ -59,12 +59,30 @@ pub enum FilterMode { } impl FilterMode { + fn from_obsolete_args(args: &parse::ObsoleteArgs) -> Self { + let signum = if args.plus { + Signum::Positive(args.num) + } else { + Signum::Negative(args.num) + }; + if args.lines { + Self::Lines(signum, b'\n') + } else { + Self::Bytes(signum) + } + } + fn from(matches: &ArgMatches) -> UResult { let zero_term = matches.get_flag(options::ZERO_TERM); let mode = if let Some(arg) = matches.get_one::(options::BYTES) { match parse_num(arg) { Ok(signum) => Self::Bytes(signum), - Err(e) => return Err(UUsageError::new(1, format!("invalid number of bytes: {e}"))), + Err(e) => { + return Err(USimpleError::new( + 1, + format!("invalid number of bytes: {e}"), + )) + } } } else if let Some(arg) = matches.get_one::(options::LINES) { match parse_num(arg) { @@ -72,7 +90,12 @@ impl FilterMode { let delimiter = if zero_term { 0 } else { b'\n' }; Self::Lines(signum, delimiter) } - Err(e) => return Err(UUsageError::new(1, format!("invalid number of lines: {e}"))), + Err(e) => { + return Err(USimpleError::new( + 1, + format!("invalid number of lines: {e}"), + )) + } } } else if zero_term { Self::default_zero() @@ -107,7 +130,7 @@ pub enum VerificationResult { NoOutput, } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct Settings { pub follow: Option, pub max_unchanged_stats: u32, @@ -121,28 +144,64 @@ pub struct Settings { pub inputs: VecDeque, } +impl Default for Settings { + fn default() -> Self { + Self { + max_unchanged_stats: 5, + sleep_sec: Duration::from_secs_f32(1.0), + follow: Default::default(), + mode: Default::default(), + pid: Default::default(), + retry: Default::default(), + use_polling: Default::default(), + verbose: Default::default(), + presume_input_pipe: Default::default(), + inputs: Default::default(), + } + } +} + impl Settings { + pub fn from_obsolete_args(args: &parse::ObsoleteArgs, name: Option<&OsString>) -> Self { + let mut settings: Self = Default::default(); + if args.follow { + settings.follow = if name.is_some() { + Some(FollowMode::Name) + } else { + Some(FollowMode::Descriptor) + }; + } + settings.mode = FilterMode::from_obsolete_args(args); + let input = if let Some(name) = name { + Input::from(&name) + } else { + Input::default() + }; + settings.inputs.push_back(input); + settings + } + pub fn from(matches: &clap::ArgMatches) -> UResult { let mut settings: Self = Self { - sleep_sec: Duration::from_secs_f32(1.0), - max_unchanged_stats: 5, + follow: if matches.get_flag(options::FOLLOW_RETRY) { + Some(FollowMode::Name) + } else if matches.value_source(options::FOLLOW) != Some(ValueSource::CommandLine) { + None + } else if matches.get_one::(options::FOLLOW) + == Some(String::from("name")).as_ref() + { + Some(FollowMode::Name) + } else { + Some(FollowMode::Descriptor) + }, + retry: matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY), + use_polling: matches.get_flag(options::USE_POLLING), + mode: FilterMode::from(matches)?, + verbose: matches.get_flag(options::verbosity::VERBOSE), + presume_input_pipe: matches.get_flag(options::PRESUME_INPUT_PIPE), ..Default::default() }; - settings.follow = if matches.get_flag(options::FOLLOW_RETRY) { - Some(FollowMode::Name) - } else if matches.value_source(options::FOLLOW) != Some(ValueSource::CommandLine) { - None - } else if matches.get_one::(options::FOLLOW) == Some(String::from("name")).as_ref() - { - Some(FollowMode::Name) - } else { - Some(FollowMode::Descriptor) - }; - - settings.retry = - matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY); - if let Some(source) = matches.get_one::(options::SLEEP_INT) { // Advantage of `fundu` over `Duration::(try_)from_secs_f64(source.parse().unwrap())`: // * doesn't panic on errors like `Duration::from_secs_f64` would. @@ -159,8 +218,6 @@ impl Settings { })?; } - settings.use_polling = matches.get_flag(options::USE_POLLING); - if let Some(s) = matches.get_one::(options::MAX_UNCHANGED_STATS) { settings.max_unchanged_stats = match s.parse::() { Ok(s) => s, @@ -200,11 +257,9 @@ impl Settings { } } - settings.mode = FilterMode::from(matches)?; - let mut inputs: VecDeque = matches .get_many::(options::ARG_FILES) - .map(|v| v.map(|string| Input::from(string.clone())).collect()) + .map(|v| v.map(|string| Input::from(&string)).collect()) .unwrap_or_default(); // apply default and add '-' to inputs if none is present @@ -212,13 +267,10 @@ impl Settings { inputs.push_front(Input::default()); } - settings.verbose = (matches.get_flag(options::verbosity::VERBOSE) || inputs.len() > 1) - && !matches.get_flag(options::verbosity::QUIET); + settings.verbose = inputs.len() > 1 && !matches.get_flag(options::verbosity::QUIET); settings.inputs = inputs; - settings.presume_input_pipe = matches.get_flag(options::PRESUME_INPUT_PIPE); - Ok(settings) } @@ -298,32 +350,31 @@ impl Settings { } } -pub fn arg_iterate<'a>( - mut args: impl uucore::Args + 'a, -) -> UResult + 'a>> { - // argv[0] is always present - let first = args.next().unwrap(); - if let Some(second) = args.next() { - if let Some(s) = second.to_str() { - match parse::parse_obsolete(s) { - Some(Ok(iter)) => Ok(Box::new(vec![first].into_iter().chain(iter).chain(args))), - Some(Err(e)) => Err(UUsageError::new( - 1, - match e { - parse::ParseError::Syntax => format!("bad argument format: {}", s.quote()), - parse::ParseError::Overflow => format!( - "invalid argument: {} Value too large for defined datatype", - s.quote() - ), - }, - )), - None => Ok(Box::new(vec![first, second].into_iter().chain(args))), - } - } else { - Err(UUsageError::new(1, "bad argument encoding".to_owned())) +pub fn parse_obsolete(arg: &OsString, input: Option<&OsString>) -> UResult> { + match parse::parse_obsolete(arg) { + Some(Ok(args)) => Ok(Some(Settings::from_obsolete_args(&args, input))), + None => Ok(None), + Some(Err(e)) => { + let arg_str = arg.to_string_lossy(); + Err(USimpleError::new( + 1, + match e { + parse::ParseError::OutOfRange => format!( + "invalid number: {}: Numerical result out of range", + arg_str.quote() + ), + parse::ParseError::Overflow => format!("invalid number: {}", arg_str.quote()), + // this ensures compatibility to GNU's error message (as tested in misc/tail) + parse::ParseError::Context => format!( + "option used in invalid context -- {}", + arg_str.chars().nth(1).unwrap_or_default() + ), + parse::ParseError::InvalidEncoding => { + format!("bad argument encoding: '{arg_str}'") + } + }, + )) } - } else { - Ok(Box::new(vec![first].into_iter())) } } @@ -352,8 +403,44 @@ fn parse_num(src: &str) -> Result { } pub fn parse_args(args: impl uucore::Args) -> UResult { - let matches = uu_app().try_get_matches_from(arg_iterate(args)?)?; - Settings::from(&matches) + let args_vec: Vec = args.collect(); + let clap_args = uu_app().try_get_matches_from(args_vec.clone()); + let clap_result = match clap_args { + Ok(matches) => Ok(Settings::from(&matches)?), + Err(err) => Err(err.into()), + }; + + // clap isn't able to handle obsolete syntax. + // therefore, we want to check further for obsolete arguments. + // argv[0] is always present, argv[1] might be obsolete arguments + // argv[2] might contain an input file, argv[3] isn't allowed in obsolete mode + if args_vec.len() != 2 && args_vec.len() != 3 { + return clap_result; + } + + // At this point, there are a few possible cases: + // + // 1. clap has succeeded and the arguments would be invalid for the obsolete syntax. + // 2. The case of `tail -c 5` is ambiguous. clap parses this as `tail -c5`, + // but it could also be interpreted as valid obsolete syntax (tail -c on file '5'). + // GNU chooses to interpret this as `tail -c5`, like clap. + // 3. `tail -f foo` is also ambiguous, but has the same effect in both cases. We can safely + // use the clap result here. + // 4. clap succeeded for obsolete arguments starting with '+', but misinterprets them as + // input files (e.g. 'tail +f'). + // 5. clap failed because of unknown flags, but possibly valid obsolete arguments + // (e.g. tail -l; tail -10c). + // + // In cases 4 & 5, we want to try parsing the obsolete arguments, which corresponds to + // checking whether clap succeeded or the first argument starts with '+'. + let possible_obsolete_args = &args_vec[1]; + if clap_result.is_ok() && !possible_obsolete_args.to_string_lossy().starts_with('+') { + return clap_result; + } + match parse_obsolete(possible_obsolete_args, args_vec.get(2))? { + Some(settings) => Ok(settings), + None => clap_result, + } } pub fn uu_app() -> Command { @@ -386,6 +473,7 @@ pub fn uu_app() -> Command { .num_args(0..=1) .require_equals(true) .value_parser(["descriptor", "name"]) + .overrides_with(options::FOLLOW) .help("Print the file as it grows"), ) .arg( @@ -482,6 +570,8 @@ pub fn uu_app() -> Command { #[cfg(test)] mod tests { + use crate::parse::ObsoleteArgs; + use super::*; #[test] @@ -513,4 +603,17 @@ mod tests { assert!(result.is_ok()); assert_eq!(result.unwrap(), Signum::Negative(1)); } + + #[test] + fn test_parse_obsolete_settings_f() { + let args = ObsoleteArgs { + follow: true, + ..Default::default() + }; + let result = Settings::from_obsolete_args(&args, None); + assert_eq!(result.follow, Some(FollowMode::Descriptor)); + + let result = Settings::from_obsolete_args(&args, Some(&"file".into())); + assert_eq!(result.follow, Some(FollowMode::Name)); + } } diff --git a/src/uu/tail/src/parse.rs b/src/uu/tail/src/parse.rs index 2129d8e29..96cf1e918 100644 --- a/src/uu/tail/src/parse.rs +++ b/src/uu/tail/src/parse.rs @@ -5,157 +5,185 @@ use std::ffi::OsString; +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +pub struct ObsoleteArgs { + pub num: u64, + pub plus: bool, + pub lines: bool, + pub follow: bool, +} + +impl Default for ObsoleteArgs { + fn default() -> Self { + Self { + num: 10, + plus: false, + lines: true, + follow: false, + } + } +} + #[derive(PartialEq, Eq, Debug)] pub enum ParseError { - Syntax, + OutOfRange, Overflow, + Context, + InvalidEncoding, } /// Parses obsolete syntax -/// tail -NUM\[kmzv\] // spell-checker:disable-line -pub fn parse_obsolete(src: &str) -> Option, ParseError>> { - let mut chars = src.char_indices(); - if let Some((_, '-')) = chars.next() { - let mut num_end = 0usize; - let mut has_num = false; - let mut last_char = 0 as char; - for (n, c) in &mut chars { - if c.is_ascii_digit() { - has_num = true; - num_end = n; - } else { - last_char = c; - break; - } - } - if has_num { - match src[1..=num_end].parse::() { - Ok(num) => { - let mut quiet = false; - let mut verbose = false; - let mut zero_terminated = false; - let mut multiplier = None; - let mut c = last_char; - loop { - // not that here, we only match lower case 'k', 'c', and 'm' - match c { - // we want to preserve order - // this also saves us 1 heap allocation - 'q' => { - quiet = true; - verbose = false; - } - 'v' => { - verbose = true; - quiet = false; - } - 'z' => zero_terminated = true, - 'c' => multiplier = Some(1), - 'b' => multiplier = Some(512), - 'k' => multiplier = Some(1024), - 'm' => multiplier = Some(1024 * 1024), - '\0' => {} - _ => return Some(Err(ParseError::Syntax)), - } - if let Some((_, next)) = chars.next() { - c = next; - } else { - break; - } - } - let mut options = Vec::new(); - if quiet { - options.push(OsString::from("-q")); - } - if verbose { - options.push(OsString::from("-v")); - } - if zero_terminated { - options.push(OsString::from("-z")); - } - if let Some(n) = multiplier { - options.push(OsString::from("-c")); - let num = match num.checked_mul(n) { - Some(n) => n, - None => return Some(Err(ParseError::Overflow)), - }; - options.push(OsString::from(format!("{num}"))); - } else { - options.push(OsString::from("-n")); - options.push(OsString::from(format!("{num}"))); - } - Some(Ok(options.into_iter())) - } - Err(_) => Some(Err(ParseError::Overflow)), - } +/// tail -\[NUM\]\[bcl\]\[f\] and tail +\[NUM\]\[bcl\]\[f\] // spell-checker:disable-line +pub fn parse_obsolete(src: &OsString) -> Option> { + let mut rest = match src.to_str() { + Some(src) => src, + None => return Some(Err(ParseError::InvalidEncoding)), + }; + let sign = if let Some(r) = rest.strip_prefix('-') { + rest = r; + '-' + } else if let Some(r) = rest.strip_prefix('+') { + rest = r; + '+' + } else { + return None; + }; + + let end_num = rest + .find(|c: char| !c.is_ascii_digit()) + .unwrap_or(rest.len()); + let has_num = !rest[..end_num].is_empty(); + let num: u64 = if has_num { + if let Ok(num) = rest[..end_num].parse() { + num } else { - None + return Some(Err(ParseError::OutOfRange)); } } else { - None + 10 + }; + rest = &rest[end_num..]; + + let mode = if let Some(r) = rest.strip_prefix('l') { + rest = r; + 'l' + } else if let Some(r) = rest.strip_prefix('c') { + rest = r; + 'c' + } else if let Some(r) = rest.strip_prefix('b') { + rest = r; + 'b' + } else { + 'l' + }; + + let follow = rest.contains('f'); + if !rest.chars().all(|f| f == 'f') { + // GNU allows an arbitrary amount of following fs, but nothing else + if sign == '-' && has_num { + return Some(Err(ParseError::Context)); + } + return None; } + + let multiplier = if mode == 'b' { 512 } else { 1 }; + let num = match num.checked_mul(multiplier) { + Some(n) => n, + None => return Some(Err(ParseError::Overflow)), + }; + + Some(Ok(ObsoleteArgs { + num, + plus: sign == '+', + lines: mode == 'l', + follow, + })) } #[cfg(test)] mod tests { use super::*; - fn obsolete(src: &str) -> Option, ParseError>> { - let r = parse_obsolete(src); - match r { - Some(s) => match s { - Ok(v) => Some(Ok(v.map(|s| s.to_str().unwrap().to_owned()).collect())), - Err(e) => Some(Err(e)), - }, - None => None, - } - } - fn obsolete_result(src: &[&str]) -> Option, ParseError>> { - Some(Ok(src.iter().map(|s| s.to_string()).collect())) - } #[test] fn test_parse_numbers_obsolete() { - assert_eq!(obsolete("-5"), obsolete_result(&["-n", "5"])); - assert_eq!(obsolete("-100"), obsolete_result(&["-n", "100"])); - assert_eq!(obsolete("-5m"), obsolete_result(&["-c", "5242880"])); - assert_eq!(obsolete("-1k"), obsolete_result(&["-c", "1024"])); - assert_eq!(obsolete("-2b"), obsolete_result(&["-c", "1024"])); - assert_eq!(obsolete("-1mmk"), obsolete_result(&["-c", "1024"])); - assert_eq!(obsolete("-1vz"), obsolete_result(&["-v", "-z", "-n", "1"])); assert_eq!( - obsolete("-1vzqvq"), // spell-checker:disable-line - obsolete_result(&["-q", "-z", "-n", "1"]) + parse_obsolete(&OsString::from("+2c")), + Some(Ok(ObsoleteArgs { + num: 2, + plus: true, + lines: false, + follow: false, + })) ); - assert_eq!(obsolete("-1vzc"), obsolete_result(&["-v", "-z", "-c", "1"])); assert_eq!( - obsolete("-105kzm"), - obsolete_result(&["-z", "-c", "110100480"]) + parse_obsolete(&OsString::from("-5")), + Some(Ok(ObsoleteArgs { + num: 5, + plus: false, + lines: true, + follow: false, + })) + ); + assert_eq!( + parse_obsolete(&OsString::from("+100f")), + Some(Ok(ObsoleteArgs { + num: 100, + plus: true, + lines: true, + follow: true, + })) + ); + assert_eq!( + parse_obsolete(&OsString::from("-2b")), + Some(Ok(ObsoleteArgs { + num: 1024, + plus: false, + lines: false, + follow: false, + })) ); } #[test] fn test_parse_errors_obsolete() { - assert_eq!(obsolete("-5n"), Some(Err(ParseError::Syntax))); - assert_eq!(obsolete("-5c5"), Some(Err(ParseError::Syntax))); + assert_eq!( + parse_obsolete(&OsString::from("-5n")), + Some(Err(ParseError::Context)) + ); + assert_eq!( + parse_obsolete(&OsString::from("-5c5")), + Some(Err(ParseError::Context)) + ); + assert_eq!( + parse_obsolete(&OsString::from("-1vzc")), + Some(Err(ParseError::Context)) + ); + assert_eq!( + parse_obsolete(&OsString::from("-5m")), + Some(Err(ParseError::Context)) + ); + assert_eq!( + parse_obsolete(&OsString::from("-1k")), + Some(Err(ParseError::Context)) + ); + assert_eq!( + parse_obsolete(&OsString::from("-1mmk")), + Some(Err(ParseError::Context)) + ); + assert_eq!( + parse_obsolete(&OsString::from("-105kzm")), + Some(Err(ParseError::Context)) + ); + assert_eq!( + parse_obsolete(&OsString::from("-1vz")), + Some(Err(ParseError::Context)) + ); + assert_eq!( + parse_obsolete(&OsString::from("-1vzqvq")), // spell-checker:disable-line + Some(Err(ParseError::Context)) + ); } #[test] fn test_parse_obsolete_no_match() { - assert_eq!(obsolete("-k"), None); - assert_eq!(obsolete("asd"), None); - } - #[test] - #[cfg(target_pointer_width = "64")] - fn test_parse_obsolete_overflow_x64() { - assert_eq!( - obsolete("-1000000000000000m"), - Some(Err(ParseError::Overflow)) - ); - assert_eq!( - obsolete("-10000000000000000000000"), - Some(Err(ParseError::Overflow)) - ); - } - #[test] - #[cfg(target_pointer_width = "32")] - fn test_parse_obsolete_overflow_x32() { - assert_eq!(obsolete("-42949672960"), Some(Err(ParseError::Overflow))); - assert_eq!(obsolete("-42949672k"), Some(Err(ParseError::Overflow))); + assert_eq!(parse_obsolete(&OsString::from("-k")), None); + assert_eq!(parse_obsolete(&OsString::from("asd")), None); + assert_eq!(parse_obsolete(&OsString::from("-cc")), None); } } diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index d8e6ece9a..4badd6866 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -6,6 +6,7 @@ // spell-checker:ignore tailable seekable stdlib (stdlib) use crate::text; +use std::ffi::OsStr; use std::fs::{File, Metadata}; use std::io::{Seek, SeekFrom}; #[cfg(unix)] @@ -26,21 +27,20 @@ pub struct Input { } impl Input { - // TODO: from &str may be the better choice - pub fn from(string: String) -> Self { - let kind = if string == text::DASH { + pub fn from>(string: &T) -> Self { + let kind = if string.as_ref() == Path::new(text::DASH) { InputKind::Stdin } else { - InputKind::File(PathBuf::from(&string)) + InputKind::File(PathBuf::from(string.as_ref())) }; let display_name = match kind { - InputKind::File(_) => string, + InputKind::File(_) => string.as_ref().to_string_lossy().to_string(), InputKind::Stdin => { if cfg!(unix) { text::STDIN_HEADER.to_string() } else { - string + string.as_ref().to_string_lossy().to_string() } } }; diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index b2ec0e7bd..eb18e858f 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -47,6 +47,13 @@ static FOLLOW_NAME_EXP: &str = "follow_name.expected"; #[cfg(not(windows))] const DEFAULT_SLEEP_INTERVAL_MILLIS: u64 = 1000; +// The binary integer "10000000" is *not* a valid UTF-8 encoding +// of a character: https://en.wikipedia.org/wiki/UTF-8#Encoding +#[cfg(unix)] +const INVALID_UTF8: u8 = 0x80; +#[cfg(windows)] +const INVALID_UTF16: u16 = 0xD800; + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); @@ -469,16 +476,13 @@ fn test_follow_non_utf8_bytes() { // Now append some bytes that are not valid UTF-8. // - // The binary integer "10000000" is *not* a valid UTF-8 encoding - // of a character: https://en.wikipedia.org/wiki/UTF-8#Encoding - // // We also write the newline character because our implementation // of `tail` is attempting to read a line of input, so the // presence of a newline character will force the `follow()` // function to conclude reading input bytes and start writing them // to output. The newline character is not fundamental to this // test, it is just a requirement of the current implementation. - let expected = [0b10000000, b'\n']; + let expected = [INVALID_UTF8, b'\n']; at.append_bytes(FOOBAR_TXT, &expected); child @@ -4475,3 +4479,319 @@ fn test_args_sleep_interval_when_illegal_argument_then_usage_error(#[case] sleep .usage_error(format!("invalid number of seconds: '{sleep_interval}'")) .code_is(1); } + +#[test] +fn test_gnu_args_plus_c() { + let scene = TestScenario::new(util_name!()); + + // obs-plus-c1 + scene + .ucmd() + .arg("+2c") + .pipe_in("abcd") + .succeeds() + .stdout_only("bcd"); + // obs-plus-c2 + scene + .ucmd() + .arg("+8c") + .pipe_in("abcd") + .succeeds() + .stdout_only(""); + // obs-plus-x1: same as +10c + scene + .ucmd() + .arg("+c") + .pipe_in(format!("x{}z", "y".repeat(10))) + .succeeds() + .stdout_only("yyz"); +} + +#[test] +fn test_gnu_args_c() { + let scene = TestScenario::new(util_name!()); + + // obs-c3 + scene + .ucmd() + .arg("-1c") + .pipe_in("abcd") + .succeeds() + .stdout_only("d"); + // obs-c4 + scene + .ucmd() + .arg("-9c") + .pipe_in("abcd") + .succeeds() + .stdout_only("abcd"); + // obs-c5 + scene + .ucmd() + .arg("-12c") + .pipe_in(format!("x{}z", "y".repeat(12))) + .succeeds() + .stdout_only(&format!("{}z", "y".repeat(11))); +} + +#[test] +fn test_gnu_args_l() { + let scene = TestScenario::new(util_name!()); + + // obs-l1 + scene + .ucmd() + .arg("-1l") + .pipe_in("x") + .succeeds() + .stdout_only("x"); + // obs-l2 + scene + .ucmd() + .arg("-1l") + .pipe_in("x\ny\n") + .succeeds() + .stdout_only("y\n"); + // obs-l3 + scene + .ucmd() + .arg("-1l") + .pipe_in("x\ny") + .succeeds() + .stdout_only("y"); + // obs-l: same as -10l + scene + .ucmd() + .arg("-l") + .pipe_in(format!("x{}z", "y\n".repeat(10))) + .succeeds() + .stdout_only(&format!("{}z", "y\n".repeat(9))); +} + +#[test] +fn test_gnu_args_plus_l() { + let scene = TestScenario::new(util_name!()); + + // obs-plus-l4 + scene + .ucmd() + .arg("+1l") + .pipe_in("x\ny\n") + .succeeds() + .stdout_only("x\ny\n"); + // ops-plus-l5 + scene + .ucmd() + .arg("+2l") + .pipe_in("x\ny\n") + .succeeds() + .stdout_only("y\n"); + // obs-plus-x2: same as +10l + scene + .ucmd() + .arg("+l") + .pipe_in(format!("x\n{}z", "y\n".repeat(10))) + .succeeds() + .stdout_only("y\ny\nz"); +} + +#[test] +fn test_gnu_args_number() { + let scene = TestScenario::new(util_name!()); + + // obs-1 + scene + .ucmd() + .arg("-1") + .pipe_in("x") + .succeeds() + .stdout_only("x"); + // obs-2 + scene + .ucmd() + .arg("-1") + .pipe_in("x\ny\n") + .succeeds() + .stdout_only("y\n"); + // obs-3 + scene + .ucmd() + .arg("-1") + .pipe_in("x\ny") + .succeeds() + .stdout_only("y"); +} + +#[test] +fn test_gnu_args_plus_number() { + let scene = TestScenario::new(util_name!()); + + // obs-plus-4 + scene + .ucmd() + .arg("+1") + .pipe_in("x\ny\n") + .succeeds() + .stdout_only("x\ny\n"); + // ops-plus-5 + scene + .ucmd() + .arg("+2") + .pipe_in("x\ny\n") + .succeeds() + .stdout_only("y\n"); +} + +#[test] +fn test_gnu_args_b() { + let scene = TestScenario::new(util_name!()); + + // obs-b + scene + .ucmd() + .arg("-b") + .pipe_in("x\n".repeat(512 * 10 / 2 + 1)) + .succeeds() + .stdout_only(&"x\n".repeat(512 * 10 / 2)); +} + +#[test] +fn test_gnu_args_err() { + let scene = TestScenario::new(util_name!()); + + // err-1 + scene + .ucmd() + .arg("+cl") + .fails() + .no_stdout() + .stderr_is("tail: cannot open '+cl' for reading: No such file or directory\n") + .code_is(1); + // err-2 + scene + .ucmd() + .arg("-cl") + .fails() + .no_stdout() + .stderr_is("tail: invalid number of bytes: 'l'\n") + .code_is(1); + // err-3 + scene + .ucmd() + .arg("+2cz") + .fails() + .no_stdout() + .stderr_is("tail: cannot open '+2cz' for reading: No such file or directory\n") + .code_is(1); + // err-4 + scene + .ucmd() + .arg("-2cX") + .fails() + .no_stdout() + .stderr_is("tail: option used in invalid context -- 2\n") + .code_is(1); + // err-5 + scene + .ucmd() + .arg("-c99999999999999999999") + .fails() + .no_stdout() + .stderr_is("tail: invalid number of bytes: '99999999999999999999'\n") + .code_is(1); + // err-6 + scene + .ucmd() + .arg("-c --") + .fails() + .no_stdout() + .stderr_is("tail: invalid number of bytes: '-'\n") + .code_is(1); + scene + .ucmd() + .arg("-5cz") + .fails() + .no_stdout() + .stderr_is("tail: option used in invalid context -- 5\n") + .code_is(1); + scene + .ucmd() + .arg("-9999999999999999999b") + .fails() + .no_stdout() + .stderr_is("tail: invalid number: '-9999999999999999999b'\n") + .code_is(1); + scene + .ucmd() + .arg("-999999999999999999999b") + .fails() + .no_stdout() + .stderr_is( + "tail: invalid number: '-999999999999999999999b': Numerical result out of range\n", + ) + .code_is(1); +} + +#[test] +fn test_gnu_args_f() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let source = "file"; + at.touch(source); + let mut p = scene.ucmd().args(&["+f", source]).run_no_wait(); + p.make_assertion_with_delay(500).is_alive(); + p.kill() + .make_assertion() + .with_all_output() + .no_stderr() + .no_stdout(); + + let mut p = scene + .ucmd() + .set_stdin(Stdio::piped()) + .arg("+f") + .run_no_wait(); + p.make_assertion_with_delay(500).is_alive(); + p.kill() + .make_assertion() + .with_all_output() + .no_stderr() + .no_stdout(); +} + +#[test] +#[cfg(unix)] +fn test_obsolete_encoding_unix() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let invalid_utf8_arg = OsStr::from_bytes(&[b'-', INVALID_UTF8, b'b']); + + scene + .ucmd() + .arg(invalid_utf8_arg) + .fails() + .no_stdout() + .stderr_is("tail: bad argument encoding: '-�b'\n") + .code_is(1); +} + +#[test] +#[cfg(windows)] +fn test_obsolete_encoding_windows() { + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + + let scene = TestScenario::new(util_name!()); + let invalid_utf16_arg = OsString::from_wide(&['-' as u16, INVALID_UTF16, 'b' as u16]); + + scene + .ucmd() + .arg(&invalid_utf16_arg) + .fails() + .no_stdout() + .stderr_is("tail: bad argument encoding: '-�b'\n") + .code_is(1); +}