Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/uu/tail/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ impl FilterMode {
}
Err(e) => {
let message =
translate!("tail-error-invalid-number-of-lines", "arg" => arg.quote());
translate!("tail-error-invalid-number-of-lines", "arg" => e.to_string());
return Err(raise(message, arg, 'n', "lines", &e));
}
}
Expand Down
52 changes: 45 additions & 7 deletions src/uucore/src/lib/features/parser/parse_signed_num.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ pub fn parse_signed_num_max(src: &str) -> Result<SignedNum, ParseSizeError> {
return Err(ParseSizeError::ParseFailure(src.to_string()));
}

// GNU keeps a `+` in the value it reports back, but drops a `-`: `-5Ki`
// is reported as `5Ki`, while `+5Ki` is reported as `+5Ki`. Only `+`
// needs the unstripped string; a `-` (or no sign at all) keeps using
// `size_string` as before.
let reported = if sign == Some(SignPrefix::Plus) {
src
} else {
size_string
};

// Remove leading zeros so size is interpreted as decimal, not octal
let trimmed = size_string.trim_start_matches('0');
let had_leading_zeros = trimmed.len() != size_string.len();
Expand All @@ -120,10 +130,10 @@ pub fn parse_signed_num_max(src: &str) -> Result<SignedNum, ParseSizeError> {
// Otherwise "0K" would parse as 1KiB (bare suffix means 1).
// A genuinely bare suffix with no digits at all (e.g. "kiB")
// still parses as 1 of that unit.
parse_count(trimmed).map_err(|e| as_typed(e, size_string))?;
parse_count(trimmed).map_err(|e| as_typed(e, reported))?;
0
} else {
parse_count(trimmed).map_err(|e| as_typed(e, size_string))?
parse_count(trimmed).map_err(|e| as_typed(e, reported))?
};

Ok(SignedNum { value, sign })
Expand All @@ -144,10 +154,18 @@ pub fn parse_signed_num(src: &str) -> Result<SignedNum, ParseSizeError> {
return Err(ParseSizeError::ParseFailure(src.to_string()));
}

// GNU keeps a `+` in the value it reports back, but drops a `-`; see
// `parse_signed_num_max` for why.
let reported = if sign == Some(SignPrefix::Plus) {
src
} else {
size_string
};

// Use parse_size_u64 but on failure, create our own error with the raw string
// (without quotes) so callers can format it as needed
let value = parse_size_u64(size_string)
.map_err(|_| ParseSizeError::ParseFailure(size_string.to_string()))?;
.map_err(|_| ParseSizeError::ParseFailure(reported.to_string()))?;

Ok(SignedNum { value, sign })
}
Expand All @@ -171,10 +189,13 @@ pub fn number_offset(src: &str) -> usize {
///
/// Zeros are only removed so the number is read as decimal rather than octal,
/// which is an implementation detail the message should not leak: GNU reports
/// `tail: invalid number of bytes: '007z'`, not `'7z'`. The sign is left off,
/// also matching GNU, which reports `-c-0fb` as `'0fb'`.
fn as_typed(error: ParseSizeError, size_string: &str) -> ParseSizeError {
let quoted = format!("{}", size_string.quote());
/// `tail: invalid number of bytes: '007z'`, not `'7z'`. `reported` is the
/// caller's choice of what else to put back: GNU reports `-c-0fb` as `'0fb'`
/// (the sign left off) but `-c+0fb` as `'+0fb'` (the sign kept), so the
/// caller passes `src` or the sign-stripped string depending on which sign,
/// if any, was found.
fn as_typed(error: ParseSizeError, reported: &str) -> ParseSizeError {
let quoted = format!("{}", reported.quote());
match error {
// These two carry the quoted operand and nothing else, so it can be
// swapped for the one that was actually typed.
Expand Down Expand Up @@ -292,6 +313,23 @@ mod tests {
assert!(result.has_minus());
}

/// GNU keeps a `+` in the value an error reports, but drops a `-`: the
/// same value, `5Ki`, is invalid either way, but the message reads back
/// what the user typed only when they typed a `+`.
#[test]
fn test_error_keeps_plus_but_not_minus() {
let plus = parse_signed_num_max("+5Ki").unwrap_err().to_string();
assert!(plus.contains("+5Ki"), "{plus}");

let minus = parse_signed_num_max("-5Ki").unwrap_err().to_string();
assert!(!minus.contains('-'), "{minus}");
assert!(minus.contains("5Ki"), "{minus}");

let unsigned = parse_signed_num_max("5Ki").unwrap_err().to_string();
assert_eq!(plus.replace('+', ""), unsigned);
assert_eq!(minus, unsigned);
}

#[test]
fn test_zero() {
let result = parse_signed_num_max("0").unwrap();
Expand Down
22 changes: 22 additions & 0 deletions tests/by-util/test_head.rs
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,28 @@ fn test_head_invalid_num() {
.stderr_is("head: invalid number of bytes: '³'\n");
}

/// A `-` sign is dropped from the value an error reports, but a `+` is kept,
/// as GNU does.
#[test]
fn test_invalid_num_keeps_a_plus_sign_but_not_a_minus() {
new_ucmd!()
.args(&["-c", "+5Ki", "emptyfile.txt"])
.fails()
.stderr_is("head: invalid number of bytes: '+5Ki'\n");
new_ucmd!()
.args(&["-n", "+5Ki", "emptyfile.txt"])
.fails()
.stderr_is("head: invalid number of lines: '+5Ki'\n");
new_ucmd!()
.args(&["-c", "-5Ki", "emptyfile.txt"])
.fails()
.stderr_is("head: invalid number of bytes: '5Ki'\n");
new_ucmd!()
.args(&["-n", "-5Ki", "emptyfile.txt"])
.fails()
.stderr_is("head: invalid number of lines: '5Ki'\n");
}

#[test]
fn test_head_num_with_undocumented_sign_bytes() {
// tail: '-' is not documented (8.32 man pages)
Expand Down
22 changes: 22 additions & 0 deletions tests/by-util/test_tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5328,3 +5328,25 @@ fn test_invalid_count_keeps_its_leading_zeros() {
.fails_with_code(1)
.stderr_is("tail: invalid number of bytes: '0fb'\n");
}

/// A `-` sign is dropped from the value an error reports, but a `+` is kept,
/// as GNU does; `-n` and `-c` must agree, since they share the same parser.
#[test]
fn test_invalid_count_keeps_a_plus_sign_but_not_a_minus() {
new_ucmd!()
.args(&["-c", "+5Ki", "/dev/null"])
.fails_with_code(1)
.stderr_is("tail: invalid number of bytes: '+5Ki'\n");
new_ucmd!()
.args(&["-n", "+5Ki", "/dev/null"])
.fails_with_code(1)
.stderr_is("tail: invalid number of lines: '+5Ki'\n");
new_ucmd!()
.args(&["-c", "-5Ki", "/dev/null"])
.fails_with_code(1)
.stderr_is("tail: invalid number of bytes: '5Ki'\n");
new_ucmd!()
.args(&["-n", "-5Ki", "/dev/null"])
.fails_with_code(1)
.stderr_is("tail: invalid number of lines: '5Ki'\n");
}
Loading