From 41d1dfaf440eabba3001595d15aaa150eb19d207 Mon Sep 17 00:00:00 2001 From: Anthony Deschamps Date: Sun, 26 Mar 2017 23:43:29 -0400 Subject: [PATCH] Partial implemantion of date. --- Cargo.toml | 2 + src/date/Cargo.toml | 17 +++ src/date/date.rs | 283 ++++++++++++++++++++++++++++++++++++++++++++ src/date/main.rs | 5 + src/date/usage.txt | 72 +++++++++++ 5 files changed, 379 insertions(+) create mode 100644 src/date/Cargo.toml create mode 100644 src/date/date.rs create mode 100644 src/date/main.rs create mode 100644 src/date/usage.txt diff --git a/Cargo.toml b/Cargo.toml index c5168addb..73849ec11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ redox = [ "comm", "cp", "cut", + "date", "dircolors", "dirname", "echo", @@ -157,6 +158,7 @@ cksum = { optional=true, path="src/cksum" } comm = { optional=true, path="src/comm" } cp = { optional=true, path="src/cp" } cut = { optional=true, path="src/cut" } +date = { optional=true, path="src/date" } dircolors= { optional=true, path="src/dircolors" } dirname = { optional=true, path="src/dirname" } du = { optional=true, path="src/du" } diff --git a/src/date/Cargo.toml b/src/date/Cargo.toml new file mode 100644 index 000000000..43584dc6c --- /dev/null +++ b/src/date/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "date" +version = "0.0.1" +authors = [] + +[lib] +name = "uu_date" +path = "date.rs" + +[dependencies] +chrono = "*" +clap = "*" +uucore = { path="../uucore" } + +[[bin]] +name = "date" +path = "main.rs" diff --git a/src/date/date.rs b/src/date/date.rs new file mode 100644 index 000000000..92197c23a --- /dev/null +++ b/src/date/date.rs @@ -0,0 +1,283 @@ +#![crate_name = "uu_date"] + +/* + * This file is part of the uutils coreutils package. + * + * (c) Anthony Deschamps + * + * 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; +extern crate uucore; + +use chrono::{DateTime, FixedOffset, Offset, Local}; +use chrono::offset::utc::UTC; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; + +// Options +const DATE: &'static str = "date"; +const HOURS: &'static str = "hours"; +const MINUTES: &'static str = "minutes"; +const SECONDS: &'static str = "seconds"; +const NS: &'static str = "ns"; + +// Help strings + +static ISO_8601_HELP_STRING: &'static str = "output date/time in ISO 8601 format. + FMT='date' for date only (the default), + 'hours', 'minutes', 'seconds', or 'ns' + for date and time to the indicated precision. + Example: 2006-08-14T02:34:56-06:00"; + +static RFC_2822_HELP_STRING: &'static str = "output date and time in RFC 2822 format. + Example: Mon, 14 Aug 2006 02:34:56 -0600"; + +static RFC_3339_HELP_STRING: &'static str = "output date/time in RFC 3339 format. + FMT='date', 'seconds', or 'ns' + for date and time to the indicated precision. + Example: 2006-08-14 02:34:56-06:00"; + +/// Settings for this program, parsed from the command line +struct Settings { + utc: bool, + format: Format, + date_source: DateSource, + set_to: Option>, +} + +/// Various ways of displaying the date +enum Format { + Iso8601(Iso8601Format), + Rfc2822, + Rfc3339(Rfc3339Format), + Custom(String), + Default, +} + +/// Various places that dates can come from +enum DateSource { + Now, + Custom(String), + File(PathBuf), +} + +enum Iso8601Format { + Date, + Hours, + Minutes, + Seconds, + Ns, +} + +impl<'a> From<&'a str> for Iso8601Format { + fn from(s: &str) -> Self { + match s { + HOURS => Iso8601Format::Hours, + MINUTES => Iso8601Format::Minutes, + SECONDS => Iso8601Format::Seconds, + NS => Iso8601Format::Ns, + DATE => Iso8601Format::Date, + // Should be caught by clap + _ => panic!("Invalid format: {}", s), + } + } +} + +enum Rfc3339Format { + Date, + Seconds, + Ns, +} + +impl<'a> From<&'a str> for Rfc3339Format { + fn from(s: &str) -> Self { + match s { + DATE => Rfc3339Format::Date, + SECONDS => Rfc3339Format::Seconds, + NS => Rfc3339Format::Ns, + // Should be caught by clap + _ => panic!("Invalid format: {}", s), + } + } +} + +pub fn uumain(args: Vec) -> i32 { + + let settings = parse_cli(args); + + if let Some(_time) = settings.set_to { + unimplemented!(); + // Probably need to use this syscall: + // https://doc.rust-lang.org/libc/i686-unknown-linux-gnu/libc/fn.clock_settime.html + + } else { + // Declare a file here because it needs to outlive the `dates` iterator. + let file: File; + + // Get the current time, either in the local time zone or UTC. + let now: DateTime = match settings.utc { + true => { + let now = UTC::now(); + now.with_timezone(&now.offset().fix()) + } + false => { + let now = Local::now(); + now.with_timezone(now.offset()) + } + }; + + /// Parse a `String` into a `DateTime`. + /// If it fails, return a tuple of the `String` along with its `ParseError`. + fn parse_date(s: String) + -> Result, (String, chrono::format::ParseError)> { + // TODO: The GNU date command can parse a wide variety of inputs. + s.parse().map_err(|e| (s, e)) + } + + // Iterate over all dates - whether it's a single date or a file. + let dates: Box> = match settings.date_source { + DateSource::Custom(ref input) => { + let date = parse_date(input.clone()); + let iter = std::iter::once(date); + Box::new(iter) + } + DateSource::File(ref path) => { + file = File::open(path).unwrap(); + let lines = BufReader::new(file).lines(); + let iter = lines.filter_map(Result::ok).map(parse_date); + Box::new(iter) + } + DateSource::Now => { + let iter = std::iter::once(Ok(now)); + Box::new(iter) + } + }; + + let format_string = make_format_string(&settings); + + // Format all the dates + for date in dates { + match date { + Ok(date) => { + let formatted = date.format(format_string); + println!("{}", formatted); + } + Err((input, _err)) => { + println!("date: invalid date '{}'", input); + } + } + } + } + + 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| match s.starts_with("+") { + true => Ok(()), + false => 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: format, + date_source: 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 { + Format::Iso8601(ref fmt) => { + match fmt { + &Iso8601Format::Date => "%F", + &Iso8601Format::Hours => "%FT%H%:z", + &Iso8601Format::Minutes => "%FT%H:%M%:z", + &Iso8601Format::Seconds => "%FT%T%:z", + &Iso8601Format::Ns => "%FT%T,%f%:z", + } + } + Format::Rfc2822 => "%a, %d %h %Y %T %z", + Format::Rfc3339(ref fmt) => { + match fmt { + &Rfc3339Format::Date => "%F", + &Rfc3339Format::Seconds => "%F %T%:z", + &Rfc3339Format::Ns => "%F %T.%f%:z", + } + } + Format::Custom(ref fmt) => fmt, + Format::Default => "%c", + } +} diff --git a/src/date/main.rs b/src/date/main.rs new file mode 100644 index 000000000..3b0db953e --- /dev/null +++ b/src/date/main.rs @@ -0,0 +1,5 @@ +extern crate uu_echo; + +fn main() { + std::process::exit(uu_date::uumain(std::env::args().collect())); +} diff --git a/src/date/usage.txt b/src/date/usage.txt new file mode 100644 index 000000000..12df1a03c --- /dev/null +++ b/src/date/usage.txt @@ -0,0 +1,72 @@ +FORMAT controls the output. Interpreted sequences are: + + %% a literal % + %a locale's abbreviated weekday name (e.g., Sun) + %A locale's full weekday name (e.g., Sunday) + %b locale's abbreviated month name (e.g., Jan) + %B locale's full month name (e.g., January) + %c locale's date and time (e.g., Thu Mar 3 23:05:25 2005) + %C century; like %Y, except omit last two digits (e.g., 20) + %d day of month (e.g., 01) + %D date; same as %m/%d/%y + %e day of month, space padded; same as %_d + %F full date; same as %Y-%m-%d + %g last two digits of year of ISO week number (see %G) + %G year of ISO week number (see %V); normally useful only with %V + %h same as %b + %H hour (00..23) + %I hour (01..12) + %j day of year (001..366) + %k hour, space padded ( 0..23); same as %_H + %l hour, space padded ( 1..12); same as %_I + %m month (01..12) + %M minute (00..59) + %n a newline + %N nanoseconds (000000000..999999999) + %p locale's equivalent of either AM or PM; blank if not known + %P like %p, but lower case + %q quarter of year (1..4) + %r locale's 12-hour clock time (e.g., 11:11:04 PM) + %R 24-hour hour and minute; same as %H:%M + %s seconds since 1970-01-01 00:00:00 UTC + %S second (00..60) + %t a tab + %T time; same as %H:%M:%S + %u day of week (1..7); 1 is Monday + %U week number of year, with Sunday as first day of week (00..53) + %V ISO week number, with Monday as first day of week (01..53) + %w day of week (0..6); 0 is Sunday + %W week number of year, with Monday as first day of week (00..53) + %x locale's date representation (e.g., 12/31/99) + %X locale's time representation (e.g., 23:13:48) + %y last two digits of year (00..99) + %Y year + %z +hhmm numeric time zone (e.g., -0400) + %:z +hh:mm numeric time zone (e.g., -04:00) + %::z +hh:mm:ss numeric time zone (e.g., -04:00:00) + %:::z numeric time zone with : to necessary precision (e.g., -04, +05:30) + %Z alphabetic time zone abbreviation (e.g., EDT) + +By default, date pads numeric fields with zeroes. +The following optional flags may follow '%': + + - (hyphen) do not pad the field + _ (underscore) pad with spaces + 0 (zero) pad with zeros + ^ use upper case if possible + # use opposite case if possible + +After any flags comes an optional field width, as a decimal number; +then an optional modifier, which is either +E to use the locale's alternate representations if available, or +O to use the locale's alternate numeric symbols if available. + +Examples: +Convert seconds since the epoch (1970-01-01 UTC) to a date + $ date --date='@2147483647' + +Show the time on the west coast of the US (use tzselect(1) to find TZ) + $ TZ='America/Los_Angeles' date + +Show the local time for 9AM next Friday on the west coast of the US + $ date --date='TZ="America/Los_Angeles" 09:00 next Fri'