mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
parent
00769af807
commit
fc851e036b
3 changed files with 313 additions and 75 deletions
161
src/uu/tail/src/parse.rs
Normal file
161
src/uu/tail/src/parse.rs
Normal 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)));
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue