Skip to content

Commit 7c7f1fc

Browse files
authored
Merge pull request #9727 from sylvestre/dup
Consolidate legacy argument parsing for head/tail
2 parents 7da2a2d + ac487de commit 7c7f1fc

File tree

4 files changed

+248
-46
lines changed

4 files changed

+248
-46
lines changed

src/uu/head/src/parse.rs

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
// file that was distributed with this source code.
55

66
use std::ffi::OsString;
7-
use uucore::parser::parse_size::{ParseSizeError, parse_size_u64_max};
7+
use uucore::parser::parse_signed_num::{SignPrefix, parse_signed_num_max};
8+
use uucore::parser::parse_size::ParseSizeError;
89

910
#[derive(PartialEq, Eq, Debug)]
1011
pub struct ParseError;
@@ -107,30 +108,12 @@ fn process_num_block(
107108
}
108109

109110
/// Parses an -c or -n argument,
110-
/// the bool specifies whether to read from the end
111+
/// the bool specifies whether to read from the end (all but last N)
111112
pub fn parse_num(src: &str) -> Result<(u64, bool), ParseSizeError> {
112-
let mut size_string = src.trim();
113-
let mut all_but_last = false;
114-
115-
if let Some(c) = size_string.chars().next() {
116-
if c == '+' || c == '-' {
117-
// head: '+' is not documented (8.32 man pages)
118-
size_string = &size_string[1..];
119-
if c == '-' {
120-
all_but_last = true;
121-
}
122-
}
123-
} else {
124-
return Err(ParseSizeError::ParseFailure(src.to_string()));
125-
}
126-
127-
// remove leading zeros so that size is interpreted as decimal, not octal
128-
let trimmed_string = size_string.trim_start_matches('0');
129-
if trimmed_string.is_empty() {
130-
Ok((0, all_but_last))
131-
} else {
132-
parse_size_u64_max(trimmed_string).map(|n| (n, all_but_last))
133-
}
113+
let result = parse_signed_num_max(src)?;
114+
// head: '-' means "all but last N"
115+
let all_but_last = result.sign == Some(SignPrefix::Minus);
116+
Ok((result.value, all_but_last))
134117
}
135118

136119
#[cfg(test)]

src/uu/tail/src/args.rs

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ use std::ffi::OsString;
1313
use std::io::IsTerminal;
1414
use std::time::Duration;
1515
use uucore::error::{UResult, USimpleError, UUsageError};
16-
use uucore::parser::parse_size::{ParseSizeError, parse_size_u64};
16+
use uucore::parser::parse_signed_num::{SignPrefix, parse_signed_num};
17+
use uucore::parser::parse_size::ParseSizeError;
1718
use uucore::parser::parse_time;
1819
use uucore::parser::shortcut_value_parser::ShortcutValueParser;
1920
use uucore::translate;
@@ -386,27 +387,15 @@ pub fn parse_obsolete(arg: &OsString, input: Option<&OsString>) -> UResult<Optio
386387
}
387388

388389
fn parse_num(src: &str) -> Result<Signum, ParseSizeError> {
389-
let mut size_string = src.trim();
390-
let mut starting_with = false;
391-
392-
if let Some(c) = size_string.chars().next() {
393-
if c == '+' || c == '-' {
394-
// tail: '-' is not documented (8.32 man pages)
395-
size_string = &size_string[1..];
396-
if c == '+' {
397-
starting_with = true;
398-
}
399-
}
400-
}
401-
402-
match parse_size_u64(size_string) {
403-
Ok(n) => match (n, starting_with) {
404-
(0, true) => Ok(Signum::PlusZero),
405-
(0, false) => Ok(Signum::MinusZero),
406-
(n, true) => Ok(Signum::Positive(n)),
407-
(n, false) => Ok(Signum::Negative(n)),
408-
},
409-
Err(_) => Err(ParseSizeError::ParseFailure(size_string.to_string())),
390+
let result = parse_signed_num(src)?;
391+
// tail: '+' means "starting from line/byte N", default/'-' means "last N"
392+
let is_plus = result.sign == Some(SignPrefix::Plus);
393+
394+
match (result.value, is_plus) {
395+
(0, true) => Ok(Signum::PlusZero),
396+
(0, false) => Ok(Signum::MinusZero),
397+
(n, true) => Ok(Signum::Positive(n)),
398+
(n, false) => Ok(Signum::Negative(n)),
410399
}
411400
}
412401

src/uucore/src/lib/features/parser/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub mod num_parser;
99
#[cfg(any(feature = "parser", feature = "parser-glob"))]
1010
pub mod parse_glob;
1111
#[cfg(any(feature = "parser", feature = "parser-size"))]
12+
pub mod parse_signed_num;
13+
#[cfg(any(feature = "parser", feature = "parser-size"))]
1214
pub mod parse_size;
1315
#[cfg(any(feature = "parser", feature = "parser-num"))]
1416
pub mod parse_time;
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
//! Parser for signed numeric arguments used by head, tail, and similar utilities.
7+
//!
8+
//! These utilities accept arguments like `-5`, `+10`, `-100K` where the leading
9+
//! sign indicates different behavior (e.g., "first N" vs "last N" vs "starting from N").
10+
11+
use super::parse_size::{ParseSizeError, parse_size_u64, parse_size_u64_max};
12+
13+
/// The sign prefix found on a numeric argument.
14+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15+
pub enum SignPrefix {
16+
/// Plus sign prefix (e.g., "+10")
17+
Plus,
18+
/// Minus sign prefix (e.g., "-10")
19+
Minus,
20+
}
21+
22+
/// A parsed signed numeric argument.
23+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24+
pub struct SignedNum {
25+
/// The numeric value
26+
pub value: u64,
27+
/// The sign prefix that was present, if any
28+
pub sign: Option<SignPrefix>,
29+
}
30+
31+
impl SignedNum {
32+
/// Returns true if the value is zero.
33+
pub fn is_zero(&self) -> bool {
34+
self.value == 0
35+
}
36+
37+
/// Returns true if a plus sign was present.
38+
pub fn has_plus(&self) -> bool {
39+
self.sign == Some(SignPrefix::Plus)
40+
}
41+
42+
/// Returns true if a minus sign was present.
43+
pub fn has_minus(&self) -> bool {
44+
self.sign == Some(SignPrefix::Minus)
45+
}
46+
}
47+
48+
/// Parse a signed numeric argument, clamping to u64::MAX on overflow.
49+
///
50+
/// This function parses strings like "10", "+5K", "-100M" where:
51+
/// - The optional leading `+` or `-` indicates direction/behavior
52+
/// - The number can have size suffixes (K, M, G, etc.)
53+
///
54+
/// # Arguments
55+
/// * `src` - The string to parse
56+
///
57+
/// # Returns
58+
/// * `Ok(SignedNum)` - The parsed value and sign
59+
/// * `Err(ParseSizeError)` - If the string cannot be parsed
60+
///
61+
/// # Examples
62+
/// ```ignore
63+
/// use uucore::parser::parse_signed_num::parse_signed_num_max;
64+
///
65+
/// let result = parse_signed_num_max("10").unwrap();
66+
/// assert_eq!(result.value, 10);
67+
/// assert_eq!(result.sign, None);
68+
///
69+
/// let result = parse_signed_num_max("+5K").unwrap();
70+
/// assert_eq!(result.value, 5 * 1024);
71+
/// assert_eq!(result.sign, Some(SignPrefix::Plus));
72+
///
73+
/// let result = parse_signed_num_max("-100").unwrap();
74+
/// assert_eq!(result.value, 100);
75+
/// assert_eq!(result.sign, Some(SignPrefix::Minus));
76+
/// ```
77+
pub fn parse_signed_num_max(src: &str) -> Result<SignedNum, ParseSizeError> {
78+
let (sign, size_string) = strip_sign_prefix(src);
79+
80+
// Empty string after stripping sign is an error
81+
if size_string.is_empty() {
82+
return Err(ParseSizeError::ParseFailure(src.to_string()));
83+
}
84+
85+
// Remove leading zeros so size is interpreted as decimal, not octal
86+
let trimmed = size_string.trim_start_matches('0');
87+
let value = if trimmed.is_empty() {
88+
// All zeros (e.g., "000" or "0")
89+
0
90+
} else {
91+
parse_size_u64_max(trimmed)?
92+
};
93+
94+
Ok(SignedNum { value, sign })
95+
}
96+
97+
/// Parse a signed numeric argument, returning error on overflow.
98+
///
99+
/// Same as [`parse_signed_num_max`] but returns an error instead of clamping
100+
/// when the value overflows u64.
101+
///
102+
/// Note: On parse failure, this returns an error with the raw string (without quotes)
103+
/// to allow callers to format the error message as needed.
104+
pub fn parse_signed_num(src: &str) -> Result<SignedNum, ParseSizeError> {
105+
let (sign, size_string) = strip_sign_prefix(src);
106+
107+
// Empty string after stripping sign is an error
108+
if size_string.is_empty() {
109+
return Err(ParseSizeError::ParseFailure(src.to_string()));
110+
}
111+
112+
// Use parse_size_u64 but on failure, create our own error with the raw string
113+
// (without quotes) so callers can format it as needed
114+
let value = parse_size_u64(size_string)
115+
.map_err(|_| ParseSizeError::ParseFailure(size_string.to_string()))?;
116+
117+
Ok(SignedNum { value, sign })
118+
}
119+
120+
/// Strip the sign prefix from a string and return both the sign and remaining string.
121+
fn strip_sign_prefix(src: &str) -> (Option<SignPrefix>, &str) {
122+
let trimmed = src.trim();
123+
124+
if let Some(rest) = trimmed.strip_prefix('+') {
125+
(Some(SignPrefix::Plus), rest)
126+
} else if let Some(rest) = trimmed.strip_prefix('-') {
127+
(Some(SignPrefix::Minus), rest)
128+
} else {
129+
(None, trimmed)
130+
}
131+
}
132+
133+
#[cfg(test)]
134+
mod tests {
135+
use super::*;
136+
137+
#[test]
138+
fn test_no_sign() {
139+
let result = parse_signed_num_max("10").unwrap();
140+
assert_eq!(result.value, 10);
141+
assert_eq!(result.sign, None);
142+
assert!(!result.has_plus());
143+
assert!(!result.has_minus());
144+
}
145+
146+
#[test]
147+
fn test_plus_sign() {
148+
let result = parse_signed_num_max("+10").unwrap();
149+
assert_eq!(result.value, 10);
150+
assert_eq!(result.sign, Some(SignPrefix::Plus));
151+
assert!(result.has_plus());
152+
assert!(!result.has_minus());
153+
}
154+
155+
#[test]
156+
fn test_minus_sign() {
157+
let result = parse_signed_num_max("-10").unwrap();
158+
assert_eq!(result.value, 10);
159+
assert_eq!(result.sign, Some(SignPrefix::Minus));
160+
assert!(!result.has_plus());
161+
assert!(result.has_minus());
162+
}
163+
164+
#[test]
165+
fn test_with_suffix() {
166+
let result = parse_signed_num_max("+5K").unwrap();
167+
assert_eq!(result.value, 5 * 1024);
168+
assert!(result.has_plus());
169+
170+
let result = parse_signed_num_max("-2M").unwrap();
171+
assert_eq!(result.value, 2 * 1024 * 1024);
172+
assert!(result.has_minus());
173+
}
174+
175+
#[test]
176+
fn test_zero() {
177+
let result = parse_signed_num_max("0").unwrap();
178+
assert_eq!(result.value, 0);
179+
assert!(result.is_zero());
180+
181+
let result = parse_signed_num_max("+0").unwrap();
182+
assert_eq!(result.value, 0);
183+
assert!(result.is_zero());
184+
assert!(result.has_plus());
185+
186+
let result = parse_signed_num_max("-0").unwrap();
187+
assert_eq!(result.value, 0);
188+
assert!(result.is_zero());
189+
assert!(result.has_minus());
190+
}
191+
192+
#[test]
193+
fn test_leading_zeros() {
194+
let result = parse_signed_num_max("007").unwrap();
195+
assert_eq!(result.value, 7);
196+
197+
let result = parse_signed_num_max("+007").unwrap();
198+
assert_eq!(result.value, 7);
199+
assert!(result.has_plus());
200+
201+
let result = parse_signed_num_max("000").unwrap();
202+
assert_eq!(result.value, 0);
203+
}
204+
205+
#[test]
206+
fn test_whitespace() {
207+
let result = parse_signed_num_max(" 10 ").unwrap();
208+
assert_eq!(result.value, 10);
209+
210+
let result = parse_signed_num_max(" +10 ").unwrap();
211+
assert_eq!(result.value, 10);
212+
assert!(result.has_plus());
213+
}
214+
215+
#[test]
216+
fn test_overflow_max() {
217+
// Should clamp to u64::MAX instead of error
218+
let result = parse_signed_num_max("99999999999999999999999999").unwrap();
219+
assert_eq!(result.value, u64::MAX);
220+
}
221+
222+
#[test]
223+
fn test_invalid() {
224+
assert!(parse_signed_num_max("").is_err());
225+
assert!(parse_signed_num_max("abc").is_err());
226+
assert!(parse_signed_num_max("++10").is_err());
227+
}
228+
}

0 commit comments

Comments
 (0)