From 850bd9c32d91ea4f933d2f4d3aae5c4b79a71e6f Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 1 May 2020 23:58:35 +0200 Subject: [PATCH 1/2] fix(date): fix the date Date didn't work out of the box. Refactor it a bit to match other programs --- src/uu/date/src/date.rs | 217 ++++++++++++++++++++++++---------------- 1 file changed, 133 insertions(+), 84 deletions(-) diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index adb9726a8..afa4e3187 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -4,16 +4,20 @@ * This file is part of the uutils coreutils package. * * (c) Anthony Deschamps + * (c) Sylvestre Ledru * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ extern crate chrono; -#[macro_use] + extern crate clap; +#[macro_use] extern crate uucore; +use clap::{App, Arg}; + use chrono::offset::Utc; use chrono::{DateTime, FixedOffset, Local, Offset}; use std::fs::File; @@ -25,8 +29,27 @@ const DATE: &str = "date"; const HOURS: &str = "hours"; const MINUTES: &str = "minutes"; const SECONDS: &str = "seconds"; +const HOUR: &str = "hour"; +const MINUTE: &str = "minute"; +const SECOND: &str = "second"; const NS: &str = "ns"; +const NAME: &str = "date"; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const ABOUT: &str = "print or set the system date and time"; + +const OPT_DATE: &str = "date"; +const OPT_FORMAT: &str = "format"; +const OPT_FILE: &str = "file"; +const OPT_DEBUG: &str = "debug"; +const OPT_ISO_8601: &str = "iso-8601"; +const OPT_RFC_EMAIL: &str = "rfc-email"; +const OPT_RFC_3339: &str = "rfc-3339"; +const OPT_SET: &str = "set"; +const OPT_REFERENCE: &str = "reference"; +const OPT_UNIVERSAL: &str = "universal"; +const OPT_UNIVERSAL_2: &str = "utc"; + // Help strings static ISO_8601_HELP_STRING: &str = "output date/time in ISO 8601 format. @@ -35,7 +58,7 @@ static ISO_8601_HELP_STRING: &str = "output date/time in ISO 8601 format. for date and time to the indicated precision. Example: 2006-08-14T02:34:56-06:00"; -static RFC_2822_HELP_STRING: &str = "output date and time in RFC 2822 format. +static RFC_5322_HELP_STRING: &str = "output date and time in RFC 5322 format. Example: Mon, 14 Aug 2006 02:34:56 -0600"; static RFC_3339_HELP_STRING: &str = "output date/time in RFC 3339 format. @@ -54,7 +77,7 @@ struct Settings { /// Various ways of displaying the date enum Format { Iso8601(Iso8601Format), - Rfc2822, + Rfc5322, Rfc3339(Rfc3339Format), Custom(String), Default, @@ -78,9 +101,9 @@ enum Iso8601Format { impl<'a> From<&'a str> for Iso8601Format { fn from(s: &str) -> Self { match s { - HOURS => Iso8601Format::Hours, - MINUTES => Iso8601Format::Minutes, - SECONDS => Iso8601Format::Seconds, + HOURS | HOUR => Iso8601Format::Hours, + MINUTES | MINUTE => Iso8601Format::Minutes, + SECONDS | SECOND => Iso8601Format::Seconds, NS => Iso8601Format::Ns, DATE => Iso8601Format::Date, // Should be caught by clap @@ -99,7 +122,7 @@ impl<'a> From<&'a str> for Rfc3339Format { fn from(s: &str) -> Self { match s { DATE => Rfc3339Format::Date, - SECONDS => Rfc3339Format::Seconds, + SECONDS | SECOND => Rfc3339Format::Seconds, NS => Rfc3339Format::Ns, // Should be caught by clap _ => panic!("Invalid format: {}", s), @@ -108,7 +131,108 @@ impl<'a> From<&'a str> for Rfc3339Format { } pub fn uumain(args: Vec) -> i32 { - let settings = parse_cli(args); + let syntax = format!( + "{0} [OPTION]... [+FORMAT]... + {0} [OPTION]... [MMDDhhmm[[CC]YY][.ss]]", + NAME + ); + let matches = App::new(executable!()) + .version(VERSION) + .about(ABOUT) + .usage(&syntax[..]) + .arg( + Arg::with_name(OPT_DATE) + .short("d") + .long(OPT_DATE) + .takes_value(true) + .help("display time described by STRING, not 'now'"), + ) + .arg( + Arg::with_name(OPT_FILE) + .short("f") + .long(OPT_FILE) + .takes_value(true) + .help("like --date; once for each line of DATEFILE"), + ) + .arg( + Arg::with_name(OPT_ISO_8601) + .short("I") + .long(OPT_ISO_8601) + .takes_value(true) + .help(ISO_8601_HELP_STRING), + ) + .arg( + Arg::with_name(OPT_RFC_EMAIL) + .short("R") + .long(OPT_RFC_EMAIL) + .help(RFC_5322_HELP_STRING), + ) + .arg( + Arg::with_name(OPT_RFC_3339) + .long(OPT_RFC_3339) + .takes_value(true) + .help(RFC_3339_HELP_STRING), + ) + .arg( + Arg::with_name(OPT_DEBUG) + .long(OPT_DEBUG) + .help("annotate the parsed date, and warn about questionable usage to stderr"), + ) + .arg( + Arg::with_name(OPT_REFERENCE) + .short("r") + .long(OPT_REFERENCE) + .takes_value(true) + .help("display the last modification time of FILE"), + ) + .arg( + Arg::with_name(OPT_SET) + .short("s") + .long(OPT_SET) + .takes_value(true) + .help("set time described by STRING"), + ) + .arg( + Arg::with_name(OPT_UNIVERSAL) + .short("u") + .long(OPT_UNIVERSAL) + .alias(OPT_UNIVERSAL_2) + .help("print or set Coordinated Universal Time (UTC)"), + ) + .arg(Arg::with_name(OPT_FORMAT).multiple(true)) + .get_matches_from(&args); + + let format = if let Some(form) = matches.value_of(OPT_FORMAT) { + let form = form[1..].into(); + Format::Custom(form) + } else if let Some(fmt) = matches + .values_of(OPT_ISO_8601) + .map(|mut iter| iter.next().unwrap_or(DATE).into()) + { + Format::Iso8601(fmt) + } else if matches.is_present(OPT_RFC_EMAIL) { + Format::Rfc5322 + } else if let Some(fmt) = matches.value_of(OPT_RFC_3339).map(Into::into) { + Format::Rfc3339(fmt) + } else { + Format::Default + }; + + let date_source = if let Some(date) = matches.value_of(OPT_DATE) { + DateSource::Custom(date.into()) + } else if let Some(file) = matches.value_of(OPT_FILE) { + DateSource::File(file.into()) + } else { + DateSource::Now + }; + + let settings = Settings { + utc: matches.is_present(OPT_UNIVERSAL), + format, + date_source, + // TODO: Handle this option: + set_to: None, + }; if let Some(_time) = settings.set_to { unimplemented!(); @@ -174,81 +298,6 @@ pub fn uumain(args: Vec) -> i32 { 0 } -/// Handle command line arguments. -fn parse_cli(args: Vec) -> Settings { - let matches = clap_app!( - date => - (@group dates => - (@arg date: -d --date [STRING] - "display time described by STRING, not 'now'") - (@arg file: -f --file [DATEFILE] - "like --date; once for each line of DATEFILE")) - - (@group format => - (@arg iso_8601: -I --("iso-8601") - possible_value[date hours minutes seconds ns] - #{0, 1} - ISO_8601_HELP_STRING) - (@arg rfc_2822: -R --("rfc-2822") - RFC_2822_HELP_STRING) - (@arg rfc_3339: --("rfc-3339") - possible_value[date seconds ns] - RFC_3339_HELP_STRING) - (@arg custom_format: +takes_value { - |s| if s.starts_with('+') { - Ok(()) - } else { - Err(String::from("Date formats must start with a '+' character")) - } - })) - - (@arg debug: --debug - "annotate the parsed date, and warn about questionable usage to stderr") - (@arg reference: -r --reference [FILE] - "display the last modification time of FILE") - (@arg set: -s --set [STRING] - "set time described by STRING") - (@arg utc: -u --utc --universal - "print or set Coordinated Universal Time (UTC)")) - // TODO: Decide whether this is appropriate. - // The GNU date command has an explanation of all formatting options, - // but the `chrono` crate has a few differences (most notably, the %Z option) - // (after_help: include_str!("usage.txt"))) - .get_matches_from(args); - - let format = if let Some(form) = matches.value_of("custom_format") { - let form = form[1..].into(); - Format::Custom(form) - } else if let Some(fmt) = matches - .values_of("iso_8601") - .map(|mut iter| iter.next().unwrap_or(DATE).into()) - { - Format::Iso8601(fmt) - } else if matches.is_present("rfc_2822") { - Format::Rfc2822 - } else if let Some(fmt) = matches.value_of("rfc_3339").map(Into::into) { - Format::Rfc3339(fmt) - } else { - Format::Default - }; - - let date_source = if let Some(date) = matches.value_of("date") { - DateSource::Custom(date.into()) - } else if let Some(file) = matches.value_of("file") { - DateSource::File(file.into()) - } else { - DateSource::Now - }; - - Settings { - utc: matches.is_present("utc"), - format, - date_source, - // TODO: Handle this option: - set_to: None, - } -} - /// Return the appropriate format string for the given settings. fn make_format_string(settings: &Settings) -> &str { match settings.format { @@ -259,7 +308,7 @@ fn make_format_string(settings: &Settings) -> &str { Iso8601Format::Seconds => "%FT%T%:z", Iso8601Format::Ns => "%FT%T,%f%:z", }, - Format::Rfc2822 => "%a, %d %h %Y %T %z", + Format::Rfc5322 => "%a, %d %h %Y %T %z", Format::Rfc3339(ref fmt) => match *fmt { Rfc3339Format::Date => "%F", Rfc3339Format::Seconds => "%F %T%:z", From 1f1edc5a336052e59f0207c43072fbe189813036 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Sat, 2 May 2020 01:03:01 +0200 Subject: [PATCH 2/2] test(date): add tests --- tests/test_date.rs | 133 +++++++++++++++++++++++++++++++++++++++++++++ tests/tests.rs | 1 + 2 files changed, 134 insertions(+) create mode 100644 tests/test_date.rs diff --git a/tests/test_date.rs b/tests/test_date.rs new file mode 100644 index 000000000..5d0446b8d --- /dev/null +++ b/tests/test_date.rs @@ -0,0 +1,133 @@ +extern crate regex; + +use self::regex::Regex; +use common::util::*; + +#[test] +fn test_date_email() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--rfc-email").run(); + assert!(result.success); +} + +#[test] +fn test_date_email2() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("-R").run(); + assert!(result.success); +} + +#[test] +fn test_date_rfc_3339() { + let scene = TestScenario::new(util_name!()); + + let mut result = scene.ucmd().arg("--rfc-3339=ns").succeeds(); + + // Check that the output matches the regexp + let rfc_regexp = r"(\d+)-(0[1-9]|1[012])-(0[1-9]|[12]\d|3[01])\s([01]\d|2[0-3]):([0-5]\d):([0-5]\d|60)(\.\d+)?(([Zz])|([\+|\-]([01]\d|2[0-3])))"; + let re = Regex::new(rfc_regexp).unwrap(); + assert!(re.is_match(&result.stdout.trim())); + + result = scene.ucmd().arg("--rfc-3339=seconds").succeeds(); + + // Check that the output matches the regexp + let re = Regex::new(rfc_regexp).unwrap(); + assert!(re.is_match(&result.stdout.trim())); +} + +#[test] +fn test_date_rfc_8601() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--iso-8601=ns").run(); + assert!(result.success); +} + +#[test] +fn test_date_rfc_8601_second() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--iso-8601=second").run(); + assert!(result.success); +} + +#[test] +fn test_date_utc() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--utc").run(); + assert!(result.success); +} + +#[test] +fn test_date_universal() { + let (_, mut ucmd) = at_and_ucmd!(); + let result = ucmd.arg("--universal").run(); + assert!(result.success); +} + +#[test] +fn test_date_format_y() { + let scene = TestScenario::new(util_name!()); + + let mut result = scene.ucmd().arg("+%Y").succeeds(); + + assert!(result.success); + let mut re = Regex::new(r"^\d{4}$").unwrap(); + assert!(re.is_match(&result.stdout.trim())); + + result = scene.ucmd().arg("+%y").succeeds(); + + assert!(result.success); + re = Regex::new(r"^\d{2}$").unwrap(); + assert!(re.is_match(&result.stdout.trim())); +} + +#[test] +fn test_date_format_m() { + let scene = TestScenario::new(util_name!()); + + let mut result = scene.ucmd().arg("+%b").succeeds(); + + assert!(result.success); + let mut re = Regex::new(r"\S+").unwrap(); + assert!(re.is_match(&result.stdout.trim())); + + result = scene.ucmd().arg("+%m").succeeds(); + + assert!(result.success); + re = Regex::new(r"^\d{2}$").unwrap(); + assert!(re.is_match(&result.stdout.trim())); +} + +#[test] +fn test_date_format_day() { + let scene = TestScenario::new(util_name!()); + + let mut result = scene.ucmd().arg("+%a").succeeds(); + + assert!(result.success); + let mut re = Regex::new(r"\S+").unwrap(); + assert!(re.is_match(&result.stdout.trim())); + + result = scene.ucmd().arg("+%A").succeeds(); + + assert!(result.success); + + re = Regex::new(r"\S+").unwrap(); + assert!(re.is_match(&result.stdout.trim())); + + result = scene.ucmd().arg("+%u").succeeds(); + + assert!(result.success); + re = Regex::new(r"^\d{1}$").unwrap(); + assert!(re.is_match(&result.stdout.trim())); +} + +#[test] +fn test_date_format_full_day() { + let scene = TestScenario::new(util_name!()); + + let result = scene.ucmd().arg("+'%a %Y-%m-%d'").succeeds(); + + assert!(result.success); + let re = Regex::new(r"\S+ \d{4}-\d{2}-\d{2}").unwrap(); + assert!(re.is_match(&result.stdout.trim())); +} diff --git a/tests/tests.rs b/tests/tests.rs index 82c681608..1c44a3e14 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -52,6 +52,7 @@ generic! { "comm", test_comm; "cp", test_cp; "cut", test_cut; + "date", test_date; "dircolors", test_dircolors; "dirname", test_dirname; "df", test_df;