Skip to content

Commit 0d842a4

Browse files
committed
date: handle parentheses as comments like GNU date
1 parent 30239e6 commit 0d842a4

File tree

2 files changed

+94
-1
lines changed

2 files changed

+94
-1
lines changed

src/uu/date/src/date.rs

Lines changed: 59 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;
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,39 @@ enum DayDelta {
130131
Next,
131132
}
132133

134+
/// Strip parenthesized comments from a date string.
135+
///
136+
/// GNU date treats text enclosed in parentheses as comments.
137+
/// This function removes all `(...)` sequences from the input.
138+
/// If a `(` has no matching `)`, everything from `(` to end is removed.
139+
///
140+
/// Examples:
141+
/// - "2026(comment)-01-05" -> "2026-01-05"
142+
/// - "1(ignore comment to eol" -> "1"
143+
/// - "(" -> ""
144+
/// - "foo(a)bar(b)baz" -> "foobarbaz"
145+
fn strip_parenthesized_comments(input: &str) -> Cow<'_, str> {
146+
let Some(first_paren) = input.find('(') else {
147+
return Cow::Borrowed(input);
148+
};
149+
150+
let mut result = String::with_capacity(input.len());
151+
result.push_str(&input[..first_paren]);
152+
153+
let mut chars = input[first_paren..].chars();
154+
while let Some(c) = chars.next() {
155+
if c == '(' {
156+
if !chars.by_ref().any(|ch| ch == ')') {
157+
break;
158+
}
159+
} else {
160+
result.push(c);
161+
}
162+
}
163+
164+
Cow::Owned(result)
165+
}
166+
133167
/// Parse military timezone with optional hour offset.
134168
/// Pattern: single letter (a-z except j) optionally followed by 1-2 digits.
135169
/// Returns Some(total_hours_in_utc) or None if pattern doesn't match.
@@ -286,7 +320,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
286320
// Iterate over all dates - whether it's a single date or a file.
287321
let dates: Box<dyn Iterator<Item = _>> = match settings.date_source {
288322
DateSource::Human(ref input) => {
323+
// GNU compatibility (Comments in parentheses)
324+
let input = strip_parenthesized_comments(input);
289325
let input = input.trim();
326+
290327
// GNU compatibility (Empty string):
291328
// An empty string (or whitespace-only) should be treated as midnight today.
292329
let is_empty_or_whitespace = input.is_empty();
@@ -885,4 +922,25 @@ mod tests {
885922
assert_eq!(parse_military_timezone_with_offset("m999"), None); // Too long
886923
assert_eq!(parse_military_timezone_with_offset("9m"), None); // Starts with digit
887924
}
925+
926+
#[test]
927+
fn test_strip_parenthesized_comments() {
928+
assert_eq!(strip_parenthesized_comments("hello"), "hello");
929+
assert_eq!(strip_parenthesized_comments("2026-01-05"), "2026-01-05");
930+
assert_eq!(strip_parenthesized_comments("("), "");
931+
assert_eq!(strip_parenthesized_comments("1(comment"), "1");
932+
assert_eq!(
933+
strip_parenthesized_comments("2026-01-05(this is a comment"),
934+
"2026-01-05"
935+
);
936+
assert_eq!(
937+
strip_parenthesized_comments("2026(comment)-01-05"),
938+
"2026-01-05"
939+
);
940+
assert_eq!(strip_parenthesized_comments("a(b)c"), "ac");
941+
assert_eq!(strip_parenthesized_comments("()"), "");
942+
assert_eq!(strip_parenthesized_comments("a(b)c(d)e"), "ace");
943+
assert_eq!(strip_parenthesized_comments("(a)(b)"), "");
944+
assert_eq!(strip_parenthesized_comments("a(b)c(d"), "ac");
945+
}
888946
}

tests/by-util/test_date.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,3 +1446,38 @@ fn test_date_locale_fr_french() {
14461446
"Output should include timezone information, got: {stdout}"
14471447
);
14481448
}
1449+
1450+
#[test]
1451+
fn test_date_parenthesis_comment() {
1452+
// GNU compatibility: Text in parentheses is treated as a comment and removed.
1453+
let cases = [
1454+
// (input, format, expected_output)
1455+
("(", "+%H:%M:%S", "00:00:00\n"),
1456+
("1(ignore comment to eol", "+%H:%M:%S", "01:00:00\n"),
1457+
("2026-01-05(this is a comment", "+%Y-%m-%d", "2026-01-05\n"),
1458+
("2026(this is a comment)-01-05", "+%Y-%m-%d", "2026-01-05\n"),
1459+
];
1460+
1461+
for (input, format, expected) in cases {
1462+
new_ucmd!()
1463+
.env("TZ", "UTC")
1464+
.arg("-d")
1465+
.arg(input)
1466+
.arg("-u")
1467+
.arg(format)
1468+
.succeeds()
1469+
.stdout_only(expected);
1470+
}
1471+
}
1472+
1473+
#[test]
1474+
fn test_date_parenthesis_vs_other_special_chars() {
1475+
// Ensure parentheses are special but other chars like [, ., ^ are still rejected
1476+
for special_char in ["[", ".", "^"] {
1477+
new_ucmd!()
1478+
.arg("-d")
1479+
.arg(special_char)
1480+
.fails()
1481+
.stderr_contains("invalid date");
1482+
}
1483+
}

0 commit comments

Comments
 (0)