1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

Implement tail -<number> (#2747)

And add obsolete_syntax test
This commit is contained in:
Smicry 2021-11-20 04:37:47 +08:00 committed by GitHub
parent 00769af807
commit fc851e036b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 313 additions and 75 deletions

161
src/uu/tail/src/parse.rs Normal file
View file

@ -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<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_numeric() {
has_num = true;
num_end = n;
} else {
last_char = c;
break;
}
}
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 {
None
}
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn obsolete(src: &str) -> Option<Result<Vec<String>, 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<Result<Vec<String>, 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)));
}
}

View file

@ -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<String>,
}
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<Self, String> {
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<u32> = 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<u32> = 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<String> = 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<dyn BufRead>, &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<Box<dyn Iterator<Item = OsString> + '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> {

View file

@ -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() {