Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tail: fix GNU test 'misc/tail' #4347

Merged
merged 5 commits into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 158 additions & 55 deletions src/uu/tail/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,43 @@ 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<Self> {
let zero_term = matches.get_flag(options::ZERO_TERM);
let mode = if let Some(arg) = matches.get_one::<String>(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::<String>(options::LINES) {
match parse_num(arg) {
Ok(signum) => {
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()
Expand Down Expand Up @@ -107,7 +130,7 @@ pub enum VerificationResult {
NoOutput,
}

#[derive(Debug, Default)]
#[derive(Debug)]
pub struct Settings {
pub follow: Option<FollowMode>,
pub max_unchanged_stats: u32,
Expand All @@ -121,27 +144,63 @@ pub struct Settings {
pub inputs: VecDeque<Input>,
}

impl Settings {
pub fn from(matches: &clap::ArgMatches) -> UResult<Self> {
let mut settings: Self = Self {
sleep_sec: Duration::from_secs_f32(1.0),
impl Default for Settings {
fn default() -> Self {
Self {
max_unchanged_stats: 5,
..Default::default()
};
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(),
}
}
}

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::<String>(options::FOLLOW) == Some(String::from("name")).as_ref()
{
Some(FollowMode::Name)
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 {
Some(FollowMode::Descriptor)
Input::default()
};
settings.inputs.push_back(input);
settings
}

settings.retry =
matches.get_flag(options::RETRY) || matches.get_flag(options::FOLLOW_RETRY);
pub fn from(matches: &clap::ArgMatches) -> UResult<Self> {
let mut settings: Self = Self {
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::<String>(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()
};

if let Some(source) = matches.get_one::<String>(options::SLEEP_INT) {
// Advantage of `fundu` over `Duration::(try_)from_secs_f64(source.parse().unwrap())`:
Expand All @@ -159,8 +218,6 @@ impl Settings {
})?;
}

settings.use_polling = matches.get_flag(options::USE_POLLING);

if let Some(s) = matches.get_one::<String>(options::MAX_UNCHANGED_STATS) {
settings.max_unchanged_stats = match s.parse::<u32>() {
Ok(s) => s,
Expand Down Expand Up @@ -200,25 +257,20 @@ impl Settings {
}
}

settings.mode = FilterMode::from(matches)?;

let mut inputs: VecDeque<Input> = matches
.get_many::<String>(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
if inputs.is_empty() {
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)
}

Expand Down Expand Up @@ -298,32 +350,31 @@ impl Settings {
}
}

pub fn arg_iterate<'a>(
mut args: impl uucore::Args + 'a,
) -> UResult<Box<dyn Iterator<Item = OsString> + '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<Option<Settings>> {
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()
),
tertsdiepraam marked this conversation as resolved.
Show resolved Hide resolved
parse::ParseError::InvalidEncoding => {
format!("bad argument encoding: '{arg_str}'")
}
},
))
}
} else {
Ok(Box::new(vec![first].into_iter()))
}
}

Expand Down Expand Up @@ -352,8 +403,44 @@ fn parse_num(src: &str) -> Result<Signum, ParseSizeError> {
}

pub fn parse_args(args: impl uucore::Args) -> UResult<Settings> {
let matches = uu_app().try_get_matches_from(arg_iterate(args)?)?;
Settings::from(&matches)
let args_vec: Vec<OsString> = 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 {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -482,6 +570,8 @@ pub fn uu_app() -> Command {

#[cfg(test)]
mod tests {
use crate::parse::ObsoleteArgs;

use super::*;

#[test]
Expand Down Expand Up @@ -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));
}
}
Loading