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

head/tail: add fixes and tests for bytes/lines NUM arg (undocumented sign)

* change tail bytes/lines clap parsing to fix posix override behavior
* change tail bytes/lines NUM parsing logic to be consistent with head
This commit is contained in:
Jan Scheer 2021-06-01 12:17:11 +02:00
parent a3e047ff16
commit 3c7175f00d
4 changed files with 115 additions and 36 deletions

View file

@ -97,9 +97,12 @@ pub fn parse_num(src: &str) -> Result<(usize, bool), ParseSizeError> {
let mut all_but_last = false; let mut all_but_last = false;
if let Some(c) = size_string.chars().next() { if let Some(c) = size_string.chars().next() {
if c == '-' { if c == '+' || c == '-' {
// head: '+' is not documented (8.32 man pages)
size_string = &size_string[1..]; size_string = &size_string[1..];
all_but_last = true; if c == '-' {
all_but_last = true;
}
} }
} else { } else {
return Err(ParseSizeError::ParseFailure(src.to_string())); return Err(ParseSizeError::ParseFailure(src.to_string()));

View file

@ -27,7 +27,7 @@ use std::io::{stdin, stdout, BufRead, BufReader, Read, Seek, SeekFrom, Write};
use std::path::Path; use std::path::Path;
use std::thread::sleep; use std::thread::sleep;
use std::time::Duration; use std::time::Duration;
use uucore::parse_size::parse_size; use uucore::parse_size::{parse_size, ParseSizeError};
use uucore::ringbuffer::RingBuffer; use uucore::ringbuffer::RingBuffer;
pub mod options { pub mod options {
@ -42,10 +42,9 @@ pub mod options {
pub static PID: &str = "pid"; pub static PID: &str = "pid";
pub static SLEEP_INT: &str = "sleep-interval"; pub static SLEEP_INT: &str = "sleep-interval";
pub static ZERO_TERM: &str = "zero-terminated"; pub static ZERO_TERM: &str = "zero-terminated";
pub static ARG_FILES: &str = "files";
} }
static ARG_FILES: &str = "files";
enum FilterMode { enum FilterMode {
Bytes(usize), Bytes(usize),
Lines(usize, u8), // (number of lines, delimiter) Lines(usize, u8), // (number of lines, delimiter)
@ -84,6 +83,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.long(options::BYTES) .long(options::BYTES)
.takes_value(true) .takes_value(true)
.allow_hyphen_values(true) .allow_hyphen_values(true)
.overrides_with_all(&[options::BYTES, options::LINES])
.help("Number of bytes to print"), .help("Number of bytes to print"),
) )
.arg( .arg(
@ -98,6 +98,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.long(options::LINES) .long(options::LINES)
.takes_value(true) .takes_value(true)
.allow_hyphen_values(true) .allow_hyphen_values(true)
.overrides_with_all(&[options::BYTES, options::LINES])
.help("Number of lines to print"), .help("Number of lines to print"),
) )
.arg( .arg(
@ -137,7 +138,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.help("Line delimiter is NUL, not newline"), .help("Line delimiter is NUL, not newline"),
) )
.arg( .arg(
Arg::with_name(ARG_FILES) Arg::with_name(options::ARG_FILES)
.multiple(true) .multiple(true)
.takes_value(true) .takes_value(true)
.min_values(1), .min_values(1),
@ -171,38 +172,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
} }
} }
match matches.value_of(options::LINES) { let mode_and_beginning = if let Some(arg) = matches.value_of(options::BYTES) {
Some(n) => { match parse_num(arg) {
let mut slice: &str = n; Ok((n, beginning)) => (FilterMode::Bytes(n), beginning),
let c = slice.chars().next().unwrap_or('_'); Err(e) => crash!(1, "invalid number of bytes: {}", e.to_string()),
if c == '+' || c == '-' {
slice = &slice[1..];
if c == '+' {
settings.beginning = true;
}
}
match parse_size(slice) {
Ok(m) => settings.mode = FilterMode::Lines(m, b'\n'),
Err(e) => crash!(1, "invalid number of bytes: {}", e.to_string()),
}
} }
None => { } else if let Some(arg) = matches.value_of(options::LINES) {
if let Some(n) = matches.value_of(options::BYTES) { match parse_num(arg) {
let mut slice: &str = n; Ok((n, beginning)) => (FilterMode::Lines(n, b'\n'), beginning),
let c = slice.chars().next().unwrap_or('_'); Err(e) => crash!(1, "invalid number of lines: {}", e.to_string()),
if c == '+' || c == '-' {
slice = &slice[1..];
if c == '+' {
settings.beginning = true;
}
}
match parse_size(slice) {
Ok(m) => settings.mode = FilterMode::Bytes(m),
Err(e) => crash!(1, "invalid number of bytes: {}", e.to_string()),
}
}
} }
} 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 matches.is_present(options::ZERO_TERM) {
if let FilterMode::Lines(count, _) = settings.mode { if let FilterMode::Lines(count, _) = settings.mode {
@ -215,7 +199,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|| matches.is_present(options::verbosity::SILENT); || matches.is_present(options::verbosity::SILENT);
let files: Vec<String> = matches let files: Vec<String> = matches
.values_of(ARG_FILES) .values_of(options::ARG_FILES)
.map(|v| v.map(ToString::to_string).collect()) .map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default(); .unwrap_or_default();
@ -422,3 +406,25 @@ fn print_byte<T: Write>(stdout: &mut T, ch: u8) {
crash!(1, "{}", err); crash!(1, "{}", err);
} }
} }
fn parse_num(src: &str) -> Result<(usize, bool), ParseSizeError> {
let mut size_string = src.trim();
let mut starting_with = false;
if let Some(c) = size_string.chars().next() {
if c == '+' || c == '-' {
// tail: '-' is not documented (8.32 man pages)
size_string = &size_string[1..];
if c == '+' {
starting_with = true;
}
}
} else {
return Err(ParseSizeError::ParseFailure(src.to_string()));
}
match parse_size(&size_string) {
Ok(n) => Ok((n, starting_with)),
Err(e) => Err(e),
}
}

View file

@ -267,3 +267,25 @@ fn test_head_invalid_num() {
"head: invalid number of lines: 1Y: Value too large to be stored in data type", "head: invalid number of lines: 1Y: Value too large to be stored in data type",
); );
} }
#[test]
fn test_head_num_with_undocumented_sign_bytes() {
// tail: '-' is not documented (8.32 man pages)
// head: '+' is not documented (8.32 man pages)
const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz";
new_ucmd!()
.args(&["-c", "5"])
.pipe_in(ALPHABET)
.succeeds()
.stdout_is("abcde");
new_ucmd!()
.args(&["-c", "-5"])
.pipe_in(ALPHABET)
.succeeds()
.stdout_is("abcdefghijklmnopqrstu");
new_ucmd!()
.args(&["-c", "+5"])
.pipe_in(ALPHABET)
.succeeds()
.stdout_is("abcde");
}

View file

@ -352,3 +352,51 @@ fn test_positive_zero_lines() {
.succeeds() .succeeds()
.stdout_is("a\nb\nc\nd\ne\n"); .stdout_is("a\nb\nc\nd\ne\n");
} }
#[test]
fn test_tail_invalid_num() {
new_ucmd!()
.args(&["-c", "1024R", "emptyfile.txt"])
.fails()
.stderr_is("tail: invalid number of bytes: 1024R");
new_ucmd!()
.args(&["-n", "1024R", "emptyfile.txt"])
.fails()
.stderr_is("tail: invalid number of lines: 1024R");
#[cfg(not(target_pointer_width = "128"))]
new_ucmd!()
.args(&["-c", "1Y", "emptyfile.txt"])
.fails()
.stderr_is(
"tail: invalid number of bytes: 1Y: Value too large to be stored in data type",
);
#[cfg(not(target_pointer_width = "128"))]
new_ucmd!()
.args(&["-n", "1Y", "emptyfile.txt"])
.fails()
.stderr_is(
"tail: invalid number of lines: 1Y: Value too large to be stored in data type",
);
}
#[test]
fn test_tail_num_with_undocumented_sign_bytes() {
// tail: '-' is not documented (8.32 man pages)
// head: '+' is not documented (8.32 man pages)
const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz";
new_ucmd!()
.args(&["-c", "5"])
.pipe_in(ALPHABET)
.succeeds()
.stdout_is("vwxyz");
new_ucmd!()
.args(&["-c", "-5"])
.pipe_in(ALPHABET)
.succeeds()
.stdout_is("vwxyz");
new_ucmd!()
.args(&["-c", "+5"])
.pipe_in(ALPHABET)
.succeeds()
.stdout_is("efghijklmnopqrstuvwxyz");
}