Skip to content

Commit 19ce703

Browse files
committed
Consolidate legacy argument parsing for head/tail
1 parent 45f81bb commit 19ce703

File tree

4 files changed

+250
-46
lines changed

4 files changed

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

0 commit comments

Comments
 (0)