Skip to content

Commit 75b9ad4

Browse files
sylvestrecakebaker
andauthored
date: handle parentheses as comments like GNU date (#10133)
* date: handle parentheses as comments like GNU date * simplify the code Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com> --------- Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
1 parent aa218a3 commit 75b9ad4

2 files changed

Lines changed: 112 additions & 1 deletion

File tree

src/uu/date/src/date.rs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6-
// spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST
6+
// spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST foobarbaz
77

88
mod locale;
99

1010
use clap::{Arg, ArgAction, Command};
1111
use jiff::fmt::strtime::{self, BrokenDownTime, Config, PosixCustom};
1212
use jiff::tz::{TimeZone, TimeZoneDatabase};
1313
use jiff::{Timestamp, Zoned};
14+
use std::borrow::Cow;
1415
use std::collections::HashMap;
1516
use std::fs::File;
1617
use std::io::{BufRead, BufReader, BufWriter, Write};
@@ -130,6 +131,42 @@ enum DayDelta {
130131
Next,
131132
}
132133

134+
/// Strip parenthesized comments from a date string.
135+
///
136+
/// GNU date removes balanced parentheses and their content, treating them as comments.
137+
/// If parentheses are unbalanced, everything from the unmatched '(' onwards is ignored.
138+
///
139+
/// Examples:
140+
/// - "2026(comment)-01-05" -> "2026-01-05"
141+
/// - "1(ignore comment to eol" -> "1"
142+
/// - "(" -> ""
143+
/// - "((foo)2026-01-05)" -> ""
144+
fn strip_parenthesized_comments(input: &str) -> Cow<'_, str> {
145+
if !input.contains('(') {
146+
return Cow::Borrowed(input);
147+
}
148+
149+
let mut result = String::with_capacity(input.len());
150+
let mut depth = 0;
151+
152+
for c in input.chars() {
153+
match c {
154+
'(' => {
155+
depth += 1;
156+
}
157+
')' if depth > 0 => {
158+
depth -= 1;
159+
}
160+
_ if depth == 0 => {
161+
result.push(c);
162+
}
163+
_ => {}
164+
}
165+
}
166+
167+
Cow::Owned(result)
168+
}
169+
133170
/// Parse military timezone with optional hour offset.
134171
/// Pattern: single letter (a-z except j) optionally followed by 1-2 digits.
135172
/// Returns Some(total_hours_in_utc) or None if pattern doesn't match.
@@ -286,7 +323,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
286323
// Iterate over all dates - whether it's a single date or a file.
287324
let dates: Box<dyn Iterator<Item = _>> = match settings.date_source {
288325
DateSource::Human(ref input) => {
326+
// GNU compatibility (Comments in parentheses)
327+
let input = strip_parenthesized_comments(input);
289328
let input = input.trim();
329+
290330
// GNU compatibility (Empty string):
291331
// An empty string (or whitespace-only) should be treated as midnight today.
292332
let is_empty_or_whitespace = input.is_empty();
@@ -887,4 +927,38 @@ mod tests {
887927
assert_eq!(parse_military_timezone_with_offset("m999"), None); // Too long
888928
assert_eq!(parse_military_timezone_with_offset("9m"), None); // Starts with digit
889929
}
930+
931+
#[test]
932+
fn test_strip_parenthesized_comments() {
933+
assert_eq!(strip_parenthesized_comments("hello"), "hello");
934+
assert_eq!(strip_parenthesized_comments("2026-01-05"), "2026-01-05");
935+
assert_eq!(strip_parenthesized_comments("("), "");
936+
assert_eq!(strip_parenthesized_comments("1(comment"), "1");
937+
assert_eq!(
938+
strip_parenthesized_comments("2026-01-05(this is a comment"),
939+
"2026-01-05"
940+
);
941+
assert_eq!(
942+
strip_parenthesized_comments("2026(comment)-01-05"),
943+
"2026-01-05"
944+
);
945+
assert_eq!(strip_parenthesized_comments("()"), "");
946+
assert_eq!(strip_parenthesized_comments("((foo)2026-01-05)"), "");
947+
948+
// These cases test the balanced parentheses removal feature
949+
// which extends beyond what GNU date strictly supports
950+
assert_eq!(strip_parenthesized_comments("a(b)c"), "ac");
951+
assert_eq!(strip_parenthesized_comments("a(b)c(d)e"), "ace");
952+
assert_eq!(strip_parenthesized_comments("(a)(b)"), "");
953+
954+
// When parentheses are unmatched, processing stops at the unmatched opening paren
955+
// In this case "a(b)c(d", the (b) is balanced but (d is unmatched
956+
// We process "a(b)c" and stop at the unmatched "(d"
957+
assert_eq!(strip_parenthesized_comments("a(b)c(d"), "ac");
958+
959+
// Additional edge cases for nested and complex parentheses
960+
assert_eq!(strip_parenthesized_comments("a(b(c)d)e"), "ae"); // Nested balanced
961+
assert_eq!(strip_parenthesized_comments("a(b(c)d"), "a"); // Nested unbalanced
962+
assert_eq!(strip_parenthesized_comments("a(b)c(d)e(f"), "ace"); // Multiple groups, last unmatched
963+
}
890964
}

tests/by-util/test_date.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,3 +1497,40 @@ fn test_date_format_x_locale_aware() {
14971497
.succeeds()
14981498
.stdout_is("19/01/1997\n");
14991499
}
1500+
1501+
#[test]
1502+
fn test_date_parenthesis_comment() {
1503+
// GNU compatibility: Text in parentheses is treated as a comment and removed.
1504+
let cases = [
1505+
// (input, format, expected_output)
1506+
("(", "+%H:%M:%S", "00:00:00\n"),
1507+
("1(ignore comment to eol", "+%H:%M:%S", "01:00:00\n"),
1508+
("2026-01-05(this is a comment", "+%Y-%m-%d", "2026-01-05\n"),
1509+
("2026(this is a comment)-01-05", "+%Y-%m-%d", "2026-01-05\n"),
1510+
("((foo)2026-01-05)", "+%H:%M:%S", "00:00:00\n"), // Nested/unbalanced case
1511+
("(2026-01-05(foo))", "+%H:%M:%S", "00:00:00\n"), // Balanced parentheses removed (empty result)
1512+
];
1513+
1514+
for (input, format, expected) in cases {
1515+
new_ucmd!()
1516+
.env("TZ", "UTC")
1517+
.arg("-d")
1518+
.arg(input)
1519+
.arg("-u")
1520+
.arg(format)
1521+
.succeeds()
1522+
.stdout_only(expected);
1523+
}
1524+
}
1525+
1526+
#[test]
1527+
fn test_date_parenthesis_vs_other_special_chars() {
1528+
// Ensure parentheses are special but other chars like [, ., ^ are still rejected
1529+
for special_char in ["[", ".", "^"] {
1530+
new_ucmd!()
1531+
.arg("-d")
1532+
.arg(special_char)
1533+
.fails()
1534+
.stderr_contains("invalid date");
1535+
}
1536+
}

0 commit comments

Comments
 (0)