diff --git a/src/uu/tail/src/parse.rs b/src/uu/tail/src/parse.rs new file mode 100644 index 000000000..929681811 --- /dev/null +++ b/src/uu/tail/src/parse.rs @@ -0,0 +1,161 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + +use std::ffi::OsString; + +#[derive(PartialEq, Debug)] +pub enum ParseError { + Syntax, + Overflow, +} +/// 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_numeric() { + 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)), + } + } else { + None + } + } else { + None + } +} + +#[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"]) + ); + assert_eq!(obsolete("-1vzc"), obsolete_result(&["-v", "-z", "-c", "1"])); + assert_eq!( + obsolete("-105kzm"), + obsolete_result(&["-z", "-c", "110100480"]) + ); + } + #[test] + fn test_parse_errors_obsolete() { + assert_eq!(obsolete("-5n"), Some(Err(ParseError::Syntax))); + assert_eq!(obsolete("-5c5"), Some(Err(ParseError::Syntax))); + } + #[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))); + } +} diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index eaf7bf8bf..d83f02724 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -16,17 +16,21 @@ extern crate clap; extern crate uucore; mod chunks; +mod parse; mod platform; use chunks::ReverseChunks; use clap::{App, Arg}; use std::collections::VecDeque; +use std::ffi::OsString; use std::fmt; use std::fs::{File, Metadata}; use std::io::{stdin, stdout, BufRead, BufReader, Read, Seek, SeekFrom, Write}; use std::path::Path; use std::thread::sleep; use std::time::Duration; +use uucore::display::Quotable; +use uucore::error::{UResult, USimpleError}; use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::ringbuffer::RingBuffer; @@ -58,105 +62,122 @@ pub mod options { pub static ARG_FILES: &str = "files"; } +#[derive(Debug)] enum FilterMode { Bytes(usize), Lines(usize, u8), // (number of lines, delimiter) } +impl Default for FilterMode { + fn default() -> Self { + FilterMode::Lines(10, b'\n') + } +} + +#[derive(Debug, Default)] struct Settings { + quiet: bool, + verbose: bool, mode: FilterMode, sleep_msec: u32, beginning: bool, follow: bool, pid: platform::Pid, + files: Vec, } -impl Default for Settings { - fn default() -> Settings { - Settings { - mode: FilterMode::Lines(10, b'\n'), +impl Settings { + pub fn get_from(args: impl uucore::Args) -> Result { + let matches = uu_app().get_matches_from(arg_iterate(args)?); + + let mut settings: Settings = Settings { sleep_msec: 1000, - beginning: false, - follow: false, - pid: 0, + follow: matches.is_present(options::FOLLOW), + ..Default::default() + }; + + if settings.follow { + if let Some(n) = matches.value_of(options::SLEEP_INT) { + let parsed: Option = n.parse().ok(); + if let Some(m) = parsed { + settings.sleep_msec = m * 1000 + } + } } + + if let Some(pid_str) = matches.value_of(options::PID) { + if let Ok(pid) = pid_str.parse() { + settings.pid = pid; + if pid != 0 { + if !settings.follow { + show_warning!("PID ignored; --pid=PID is useful only when following"); + } + + if !platform::supports_pid_checks(pid) { + show_warning!("--pid=PID is not supported on this system"); + settings.pid = 0; + } + } + } + } + + let mode_and_beginning = if let Some(arg) = matches.value_of(options::BYTES) { + match parse_num(arg) { + Ok((n, beginning)) => (FilterMode::Bytes(n), beginning), + Err(e) => return Err(format!("invalid number of bytes: {}", e)), + } + } else if let Some(arg) = matches.value_of(options::LINES) { + match parse_num(arg) { + Ok((n, beginning)) => (FilterMode::Lines(n, b'\n'), beginning), + Err(e) => return Err(format!("invalid number of lines: {}", e)), + } + } else { + (FilterMode::Lines(10, b'\n'), false) + }; + settings.mode = mode_and_beginning.0; + settings.beginning = mode_and_beginning.1; + + if matches.is_present(options::ZERO_TERM) { + if let FilterMode::Lines(count, _) = settings.mode { + settings.mode = FilterMode::Lines(count, 0); + } + } + + settings.verbose = matches.is_present(options::verbosity::VERBOSE); + settings.quiet = matches.is_present(options::verbosity::QUIET); + + settings.files = match matches.values_of(options::ARG_FILES) { + Some(v) => v.map(|s| s.to_owned()).collect(), + None => vec!["-".to_owned()], + }; + + Ok(settings) } } #[allow(clippy::cognitive_complexity)] -pub fn uumain(args: impl uucore::Args) -> i32 { - let mut settings: Settings = Default::default(); - - let app = uu_app(); - - let matches = app.get_matches_from(args); - - settings.follow = matches.is_present(options::FOLLOW); - if settings.follow { - if let Some(n) = matches.value_of(options::SLEEP_INT) { - let parsed: Option = n.parse().ok(); - if let Some(m) = parsed { - settings.sleep_msec = m * 1000 - } +#[uucore_procs::gen_uumain] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = match Settings::get_from(args) { + Ok(o) => o, + Err(s) => { + return Err(USimpleError::new(1, s)); } - } - - if let Some(pid_str) = matches.value_of(options::PID) { - if let Ok(pid) = pid_str.parse() { - settings.pid = pid; - if pid != 0 { - if !settings.follow { - show_warning!("PID ignored; --pid=PID is useful only when following"); - } - - if !platform::supports_pid_checks(pid) { - show_warning!("--pid=PID is not supported on this system"); - settings.pid = 0; - } - } - } - } - - let mode_and_beginning = if let Some(arg) = matches.value_of(options::BYTES) { - match parse_num(arg) { - Ok((n, beginning)) => (FilterMode::Bytes(n), beginning), - Err(e) => crash!(1, "invalid number of bytes: {}", e.to_string()), - } - } else if let Some(arg) = matches.value_of(options::LINES) { - match parse_num(arg) { - Ok((n, beginning)) => (FilterMode::Lines(n, b'\n'), beginning), - Err(e) => crash!(1, "invalid number of lines: {}", e.to_string()), - } - } else { - (FilterMode::Lines(10, b'\n'), false) }; - settings.mode = mode_and_beginning.0; - settings.beginning = mode_and_beginning.1; + uu_tail(&args) +} - if matches.is_present(options::ZERO_TERM) { - if let FilterMode::Lines(count, _) = settings.mode { - settings.mode = FilterMode::Lines(count, 0); - } - } - - let verbose = matches.is_present(options::verbosity::VERBOSE); - let quiet = matches.is_present(options::verbosity::QUIET); - - let files: Vec = matches - .values_of(options::ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_else(|| vec![String::from("-")]); - - let multiple = files.len() > 1; +fn uu_tail(settings: &Settings) -> UResult<()> { + let multiple = settings.files.len() > 1; let mut first_header = true; let mut readers: Vec<(Box, &String)> = Vec::new(); #[cfg(unix)] let stdin_string = String::from("standard input"); - for filename in &files { + for filename in &settings.files { let use_stdin = filename.as_str() == "-"; - if (multiple || verbose) && !quiet { + if (multiple || settings.verbose) && !settings.quiet { if !first_header { println!(); } @@ -170,7 +191,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { if use_stdin { let mut reader = BufReader::new(stdin()); - unbounded_tail(&mut reader, &settings); + unbounded_tail(&mut reader, settings); // Don't follow stdin since there are no checks for pipes/FIFOs // @@ -202,14 +223,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let mut file = File::open(&path).unwrap(); let md = file.metadata().unwrap(); if is_seekable(&mut file) && get_block_size(&md) > 0 { - bounded_tail(&mut file, &settings); + bounded_tail(&mut file, settings); if settings.follow { let reader = BufReader::new(file); readers.push((Box::new(reader), filename)); } } else { let mut reader = BufReader::new(file); - unbounded_tail(&mut reader, &settings); + unbounded_tail(&mut reader, settings); if settings.follow { readers.push((Box::new(reader), filename)); } @@ -218,10 +239,36 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } if settings.follow { - follow(&mut readers[..], &settings); + follow(&mut readers[..], settings); } - 0 + Ok(()) +} + +fn arg_iterate<'a>( + mut args: impl uucore::Args + 'a, +) -> Result + 'a>, String> { + // 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)) => match e { + parse::ParseError::Syntax => Err(format!("bad argument format: {}", s.quote())), + parse::ParseError::Overflow => Err(format!( + "invalid argument: {} Value too large for defined datatype", + s.quote() + )), + }, + None => Ok(Box::new(vec![first, second].into_iter().chain(args))), + } + } else { + Err("bad argument encoding".to_owned()) + } + } else { + Ok(Box::new(vec![first].into_iter())) + } } pub fn uu_app() -> App<'static, 'static> { diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 26d8106f0..a020f6235 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -358,6 +358,36 @@ fn test_positive_lines() { .stdout_is("c\nd\ne\n"); } +/// Test for reading all but the first NUM lines: `tail -3`. +#[test] +fn test_obsolete_syntax_positive_lines() { + new_ucmd!() + .args(&["-3"]) + .pipe_in("a\nb\nc\nd\ne\n") + .succeeds() + .stdout_is("c\nd\ne\n"); +} + +/// Test for reading all but the first NUM lines: `tail -n -10`. +#[test] +fn test_small_file() { + new_ucmd!() + .args(&["-n -10"]) + .pipe_in("a\nb\nc\nd\ne\n") + .succeeds() + .stdout_is("a\nb\nc\nd\ne\n"); +} + +/// Test for reading all but the first NUM lines: `tail -10`. +#[test] +fn test_obsolete_syntax_small_file() { + new_ucmd!() + .args(&["-10"]) + .pipe_in("a\nb\nc\nd\ne\n") + .succeeds() + .stdout_is("a\nb\nc\nd\ne\n"); +} + /// Test for reading all lines, specified by `tail -n +0`. #[test] fn test_positive_zero_lines() {