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:
parent
a3e047ff16
commit
3c7175f00d
4 changed files with 115 additions and 36 deletions
|
@ -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()));
|
||||||
|
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue