Skip to content

Commit

Permalink
tail: improve GNU compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
bbara committed Feb 16, 2023
1 parent aaaf57c commit bce55fb
Show file tree
Hide file tree
Showing 3 changed files with 324 additions and 104 deletions.
23 changes: 19 additions & 4 deletions src/uu/tail/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,25 @@ impl FilterMode {
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 @@ -313,14 +323,19 @@ pub fn arg_iterate<'a>(
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(
Some(Err(e)) => Err(USimpleError::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()
),
parse::ParseError::Context => {
format!(
"option used in invalid context -- {}",
s.chars().nth(1).unwrap_or_default()
)
}
},
)),
None => Ok(Box::new(vec![first, second].into_iter().chain(args))),
Expand Down
169 changes: 69 additions & 100 deletions src/uu/tail/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,92 +7,67 @@ use std::ffi::OsString;

#[derive(PartialEq, Eq, Debug)]
pub enum ParseError {
Syntax,
Overflow,
Context,
}
/// Parses obsolete syntax
/// tail -NUM\[kmzv\] // spell-checker:disable-line
/// tail -\[NUM\]\[bl\]\[f\] and tail +\[NUM\]\[bcl\]\[f\] // spell-checker:disable-line
pub fn parse_obsolete(src: &str) -> Option<Result<impl Iterator<Item = OsString>, 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;
}
let mut chars = src.chars();
let sign = chars.next()?;
if sign != '+' && sign != '-' {
return None;
}

let numbers: String = chars.clone().take_while(|&c| c.is_ascii_digit()).collect();
let has_num = !numbers.is_empty();
let num: usize = if has_num {
if let Ok(num) = numbers.parse() {
num
} else {
return Some(Err(ParseError::Overflow));
}
if has_num {
match src[1..=num_end].parse::<usize>() {
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)),
}
} else {
10
};

let mut follow = false;
let mut mode = None;
let mut first_char = true;
for char in chars.skip_while(|&c| c.is_ascii_digit()) {
if sign == '-' && char == 'c' && !has_num {
// special case: -c should be handled by clap (is ambiguous)
return None;
} else if char == 'f' {
follow = true;
} else if first_char && (char == 'b' || char == 'c' || char == 'l') {
mode = Some(char);
} else if has_num && sign == '-' {
return Some(Err(ParseError::Context));
} else {
None
return None;
}
first_char = false;
}

let mut options = Vec::new();
if follow {
options.push(OsString::from("-f"));
}
let mode = mode.unwrap_or('l');
if mode == 'b' || mode == 'c' {
options.push(OsString::from("-c"));
let n = if mode == 'b' { 512 } else { 1 };
let num = match num.checked_mul(n) {
Some(n) => n,
None => return Some(Err(ParseError::Overflow)),
};
options.push(OsString::from(format!("{sign}{num}")));
} else {
None
options.push(OsString::from("-n"));
options.push(OsString::from(format!("{sign}{num}")));
}
Some(Ok(options.into_iter()))
}

#[cfg(test)]
Expand All @@ -113,40 +88,35 @@ mod tests {
}
#[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"])
);
assert_eq!(obsolete("-1vzc"), obsolete_result(&["-v", "-z", "-c", "1"]));
assert_eq!(
obsolete("-105kzm"),
obsolete_result(&["-z", "-c", "110100480"])
);
assert_eq!(obsolete("+2c"), obsolete_result(&["-c", "+2"]));
assert_eq!(obsolete("-5"), obsolete_result(&["-n", "-5"]));
assert_eq!(obsolete("-100"), obsolete_result(&["-n", "-100"]));
assert_eq!(obsolete("-2b"), obsolete_result(&["-c", "-1024"]));
}
#[test]
fn test_parse_errors_obsolete() {
assert_eq!(obsolete("-5n"), Some(Err(ParseError::Syntax)));
assert_eq!(obsolete("-5c5"), Some(Err(ParseError::Syntax)));
assert_eq!(obsolete("-5n"), Some(Err(ParseError::Context)));
assert_eq!(obsolete("-5c5"), Some(Err(ParseError::Context)));
assert_eq!(obsolete("-1vzc"), Some(Err(ParseError::Context)));
assert_eq!(obsolete("-5m"), Some(Err(ParseError::Context)));
assert_eq!(obsolete("-1k"), Some(Err(ParseError::Context)));
assert_eq!(obsolete("-1mmk"), Some(Err(ParseError::Context)));
assert_eq!(obsolete("-105kzm"), Some(Err(ParseError::Context)));
assert_eq!(obsolete("-1vz"), Some(Err(ParseError::Context)));
assert_eq!(
obsolete("-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);
assert_eq!(obsolete("-cc"), 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))
Expand All @@ -156,6 +126,5 @@ mod tests {
#[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)));
}
}
Loading

0 comments on commit bce55fb

Please sign in to comment.