From 14bf25da148b678f48aef9ee449181001aa6092d Mon Sep 17 00:00:00 2001 From: WindSoilder Date: Fri, 4 Aug 2023 02:06:00 +0800 Subject: [PATCH] rename from `date format` to `format date` (#9902) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description Closes: #9891 I also think it's good to keep command name consistency. And moving `date format` to deprecated. # User-Facing Changes Running `date format` will lead to deprecate message: ```nushell ❯ "2021-10-22 20:00:12 +01:00" | date format Error: nu::shell::deprecated_command × Deprecated command date format ╭─[entry #28:1:1] 1 │ "2021-10-22 20:00:12 +01:00" | date format · ─────┬───── · ╰── 'date format' is deprecated. Please use 'format date' instead. ╰──── ``` --- crates/nu-command/src/date/format.rs | 475 ------------------ crates/nu-command/src/date/mod.rs | 5 +- crates/nu-command/src/date/now.rs | 2 +- crates/nu-command/src/date/utils.rs | 277 ++++++++++ crates/nu-command/src/default_context.rs | 5 +- crates/nu-command/src/deprecated/format.rs | 50 ++ crates/nu-command/src/deprecated/mod.rs | 2 + crates/nu-command/src/strings/format/date.rs | 198 ++++++++ crates/nu-command/src/strings/format/mod.rs | 3 + crates/nu-command/src/strings/mod.rs | 2 + .../nu-command/tests/commands/date/format.rs | 16 +- crates/nu-std/std/log.nu | 2 +- crates/nu-std/tests/logger_tests/commons.nu | 4 +- .../nu-utils/src/sample_config/default_env.nu | 2 +- src/tests/test_type_check.rs | 4 +- 15 files changed, 551 insertions(+), 496 deletions(-) delete mode 100644 crates/nu-command/src/date/format.rs create mode 100644 crates/nu-command/src/deprecated/format.rs create mode 100644 crates/nu-command/src/strings/format/date.rs create mode 100644 crates/nu-command/src/strings/format/mod.rs diff --git a/crates/nu-command/src/date/format.rs b/crates/nu-command/src/date/format.rs deleted file mode 100644 index 8fb3e67b0e..0000000000 --- a/crates/nu-command/src/date/format.rs +++ /dev/null @@ -1,475 +0,0 @@ -use chrono::{DateTime, Local, Locale, TimeZone}; - -use nu_engine::CallExt; -use nu_protocol::{ - ast::Call, - engine::{Command, EngineState, Stack}, - Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, - Value, -}; -use nu_utils::locale::get_system_locale_string; -use std::fmt::{Display, Write}; - -use super::utils::parse_date_from_string; - -#[derive(Clone)] -pub struct SubCommand; - -impl Command for SubCommand { - fn name(&self) -> &str { - "date format" - } - - fn signature(&self) -> Signature { - Signature::build("date format") - .input_output_types(vec![ - (Type::Date, Type::String), - (Type::String, Type::String), - ]) - .allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032 - .switch("list", "lists strftime cheatsheet", Some('l')) - .optional( - "format string", - SyntaxShape::String, - "the desired date format", - ) - .category(Category::Date) - } - - fn usage(&self) -> &str { - "Format a given date using a format string." - } - - fn search_terms(&self) -> Vec<&str> { - vec!["fmt", "strftime"] - } - - fn run( - &self, - engine_state: &EngineState, - stack: &mut Stack, - call: &Call, - input: PipelineData, - ) -> Result { - let head = call.head; - if call.has_flag("list") { - return Ok(PipelineData::Value( - generate_strftime_list(head, false), - None, - )); - } - - let format = call.opt::>(engine_state, stack, 0)?; - - // This doesn't match explicit nulls - if matches!(input, PipelineData::Empty) { - return Err(ShellError::PipelineEmpty { dst_span: head }); - } - input.map( - move |value| match &format { - Some(format) => format_helper(value, format.item.as_str(), format.span, head), - None => format_helper_rfc2822(value, head), - }, - engine_state.ctrlc.clone(), - ) - } - - fn examples(&self) -> Vec { - vec![ - // TODO: This should work but does not; see https://github.com/nushell/nushell/issues/7032 - // Example { - // description: "Format a given date-time using the default format (RFC 2822).", - // example: r#"'2021-10-22 20:00:12 +01:00' | into datetime | date format"#, - // result: Some(Value::String { - // val: "Fri, 22 Oct 2021 20:00:12 +0100".to_string(), - // span: Span::test_data(), - // }), - // }, - Example { - description: - "Format a given date-time as a string using the default format (RFC 2822).", - example: r#""2021-10-22 20:00:12 +01:00" | date format"#, - result: Some(Value::String { - val: "Fri, 22 Oct 2021 20:00:12 +0100".to_string(), - span: Span::test_data(), - }), - }, - Example { - description: "Format the current date-time using a given format string.", - example: r#"date now | date format "%Y-%m-%d %H:%M:%S""#, - result: None, - }, - Example { - description: "Format the current date using a given format string.", - example: r#"date now | date format "%Y-%m-%d %H:%M:%S""#, - result: None, - }, - Example { - description: "Format a given date using a given format string.", - example: r#""2021-10-22 20:00:12 +01:00" | date format "%Y-%m-%d""#, - result: Some(Value::test_string("2021-10-22")), - }, - ] - } -} - -fn format_from(date_time: DateTime, formatter: &str, span: Span) -> Value -where - Tz::Offset: Display, -{ - let mut formatter_buf = String::new(); - let locale: Locale = get_system_locale_string() - .map(|l| l.replace('-', "_")) // `chrono::Locale` needs something like `xx_xx`, rather than `xx-xx` - .unwrap_or_else(|| String::from("en_US")) - .as_str() - .try_into() - .unwrap_or(Locale::en_US); - let format = date_time.format_localized(formatter, locale); - - match formatter_buf.write_fmt(format_args!("{format}")) { - Ok(_) => Value::String { - val: formatter_buf, - span, - }, - Err(_) => Value::Error { - error: Box::new(ShellError::TypeMismatch { - err_message: "invalid format".to_string(), - span, - }), - }, - } -} - -fn format_helper(value: Value, formatter: &str, formatter_span: Span, head_span: Span) -> Value { - match value { - Value::Date { val, .. } => format_from(val, formatter, formatter_span), - Value::String { val, .. } => { - let dt = parse_date_from_string(&val, formatter_span); - - match dt { - Ok(x) => format_from(x, formatter, formatter_span), - Err(e) => e, - } - } - _ => Value::Error { - error: Box::new(ShellError::DatetimeParseError( - value.debug_value(), - head_span, - )), - }, - } -} - -fn format_helper_rfc2822(value: Value, span: Span) -> Value { - match value { - Value::Date { val, span: _ } => Value::String { - val: val.to_rfc2822(), - span, - }, - Value::String { - val, - span: val_span, - } => { - let dt = parse_date_from_string(&val, val_span); - match dt { - Ok(x) => Value::String { - val: x.to_rfc2822(), - span, - }, - Err(e) => e, - } - } - _ => Value::Error { - error: Box::new(ShellError::DatetimeParseError(value.debug_value(), span)), - }, - } -} - -/// Generates a table containing available datetime format specifiers -/// -/// # Arguments -/// * `head` - use the call's head -/// * `show_parse_only_formats` - whether parse-only format specifiers (that can't be outputted) should be shown. Should only be used for `into datetime`, not `date format` -pub(crate) fn generate_strftime_list(head: Span, show_parse_only_formats: bool) -> Value { - let column_names = vec![ - "Specification".into(), - "Example".into(), - "Description".into(), - ]; - let now = Local::now(); - - struct FormatSpecification<'a> { - spec: &'a str, - description: &'a str, - } - - let specifications = vec![ - FormatSpecification { - spec: "%Y", - description: "The full proleptic Gregorian year, zero-padded to 4 digits.", - }, - FormatSpecification { - spec: "%C", - description: "The proleptic Gregorian year divided by 100, zero-padded to 2 digits.", - }, - FormatSpecification { - spec: "%y", - description: "The proleptic Gregorian year modulo 100, zero-padded to 2 digits.", - }, - FormatSpecification { - spec: "%m", - description: "Month number (01--12), zero-padded to 2 digits.", - }, - FormatSpecification { - spec: "%b", - description: "Abbreviated month name. Always 3 letters.", - }, - FormatSpecification { - spec: "%B", - description: "Full month name. Also accepts corresponding abbreviation in parsing.", - }, - FormatSpecification { - spec: "%h", - description: "Same as %b.", - }, - FormatSpecification { - spec: "%d", - description: "Day number (01--31), zero-padded to 2 digits.", - }, - FormatSpecification { - spec: "%e", - description: "Same as %d but space-padded. Same as %_d.", - }, - FormatSpecification { - spec: "%a", - description: "Abbreviated weekday name. Always 3 letters.", - }, - FormatSpecification { - spec: "%A", - description: "Full weekday name. Also accepts corresponding abbreviation in parsing.", - }, - FormatSpecification { - spec: "%w", - description: "Sunday = 0, Monday = 1, ..., Saturday = 6.", - }, - FormatSpecification { - spec: "%u", - description: "Monday = 1, Tuesday = 2, ..., Sunday = 7. (ISO 8601)", - }, - FormatSpecification { - spec: "%U", - description: "Week number starting with Sunday (00--53), zero-padded to 2 digits.", - }, - FormatSpecification { - spec: "%W", - description: - "Same as %U, but week 1 starts with the first Monday in that year instead.", - }, - FormatSpecification { - spec: "%G", - description: "Same as %Y but uses the year number in ISO 8601 week date.", - }, - FormatSpecification { - spec: "%g", - description: "Same as %y but uses the year number in ISO 8601 week date.", - }, - FormatSpecification { - spec: "%V", - description: "Same as %U but uses the week number in ISO 8601 week date (01--53).", - }, - FormatSpecification { - spec: "%j", - description: "Day of the year (001--366), zero-padded to 3 digits.", - }, - FormatSpecification { - spec: "%D", - description: "Month-day-year format. Same as %m/%d/%y.", - }, - FormatSpecification { - spec: "%x", - description: "Locale's date representation (e.g., 12/31/99).", - }, - FormatSpecification { - spec: "%F", - description: "Year-month-day format (ISO 8601). Same as %Y-%m-%d.", - }, - FormatSpecification { - spec: "%v", - description: "Day-month-year format. Same as %e-%b-%Y.", - }, - FormatSpecification { - spec: "%H", - description: "Hour number (00--23), zero-padded to 2 digits.", - }, - FormatSpecification { - spec: "%k", - description: "Same as %H but space-padded. Same as %_H.", - }, - FormatSpecification { - spec: "%I", - description: "Hour number in 12-hour clocks (01--12), zero-padded to 2 digits.", - }, - FormatSpecification { - spec: "%l", - description: "Same as %I but space-padded. Same as %_I.", - }, - FormatSpecification { - spec: "%P", - description: "am or pm in 12-hour clocks.", - }, - FormatSpecification { - spec: "%p", - description: "AM or PM in 12-hour clocks.", - }, - FormatSpecification { - spec: "%M", - description: "Minute number (00--59), zero-padded to 2 digits.", - }, - FormatSpecification { - spec: "%S", - description: "Second number (00--60), zero-padded to 2 digits.", - }, - FormatSpecification { - spec: "%f", - description: "The fractional seconds (in nanoseconds) since last whole second.", - }, - FormatSpecification { - spec: "%.f", - description: "Similar to .%f but left-aligned. These all consume the leading dot.", - }, - FormatSpecification { - spec: "%.3f", - description: "Similar to .%f but left-aligned but fixed to a length of 3.", - }, - FormatSpecification { - spec: "%.6f", - description: "Similar to .%f but left-aligned but fixed to a length of 6.", - }, - FormatSpecification { - spec: "%.9f", - description: "Similar to .%f but left-aligned but fixed to a length of 9.", - }, - FormatSpecification { - spec: "%3f", - description: "Similar to %.3f but without the leading dot.", - }, - FormatSpecification { - spec: "%6f", - description: "Similar to %.6f but without the leading dot.", - }, - FormatSpecification { - spec: "%9f", - description: "Similar to %.9f but without the leading dot.", - }, - FormatSpecification { - spec: "%R", - description: "Hour-minute format. Same as %H:%M.", - }, - FormatSpecification { - spec: "%T", - description: "Hour-minute-second format. Same as %H:%M:%S.", - }, - FormatSpecification { - spec: "%X", - description: "Locale's time representation (e.g., 23:13:48).", - }, - FormatSpecification { - spec: "%r", - description: "Hour-minute-second format in 12-hour clocks. Same as %I:%M:%S %p.", - }, - FormatSpecification { - spec: "%Z", - description: - "Local time zone name. Skips all non-whitespace characters during parsing.", - }, - FormatSpecification { - spec: "%z", - description: "Offset from the local time to UTC (with UTC being +0000).", - }, - FormatSpecification { - spec: "%:z", - description: "Same as %z but with a colon.", - }, - FormatSpecification { - spec: "%c", - description: "Locale's date and time (e.g., Thu Mar 3 23:05:25 2005).", - }, - FormatSpecification { - spec: "%+", - description: "ISO 8601 / RFC 3339 date & time format.", - }, - FormatSpecification { - spec: "%s", - description: "UNIX timestamp, the number of seconds since 1970-01-01", - }, - FormatSpecification { - spec: "%t", - description: "Literal tab (\\t).", - }, - FormatSpecification { - spec: "%n", - description: "Literal newline (\\n).", - }, - FormatSpecification { - spec: "%%", - description: "Literal percent sign.", - }, - ]; - - let mut records = specifications - .iter() - .map(|s| Value::Record { - cols: column_names.clone(), - vals: vec![ - Value::string(s.spec, head), - Value::string(now.format(s.spec).to_string(), head), - Value::string(s.description, head), - ], - span: head, - }) - .collect::>(); - - if show_parse_only_formats { - // now.format("%#z") will panic since it is parse-only - // so here we emulate how it will look: - let example = now - .format("%:z") // e.g. +09:30 - .to_string() - .get(0..3) // +09:30 -> +09 - .unwrap_or("") - .to_string(); - - records.push(Value::Record { - cols: column_names, - vals: vec![ - Value::string("%#z", head), - Value::String { - val: example, - span: head, - }, - Value::string( - "Parsing only: Same as %z but allows minutes to be missing or present.", - head, - ), - ], - span: head, - }); - } - - Value::List { - vals: records, - span: head, - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_examples() { - use crate::test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/date/mod.rs b/crates/nu-command/src/date/mod.rs index b964c59a60..dcb3d8bee8 100644 --- a/crates/nu-command/src/date/mod.rs +++ b/crates/nu-command/src/date/mod.rs @@ -1,5 +1,4 @@ mod date_; -mod format; mod humanize; mod list_timezone; mod now; @@ -10,12 +9,10 @@ mod to_timezone; mod utils; pub use date_::Date; -pub(crate) use format::generate_strftime_list; -pub use format::SubCommand as DateFormat; pub use humanize::SubCommand as DateHumanize; pub use list_timezone::SubCommand as DateListTimezones; pub use now::SubCommand as DateNow; pub use to_record::SubCommand as DateToRecord; pub use to_table::SubCommand as DateToTable; pub use to_timezone::SubCommand as DateToTimezone; -pub(crate) use utils::parse_date_from_string; +pub(crate) use utils::{generate_strftime_list, parse_date_from_string}; diff --git a/crates/nu-command/src/date/now.rs b/crates/nu-command/src/date/now.rs index 6126415cd2..275bbeb029 100644 --- a/crates/nu-command/src/date/now.rs +++ b/crates/nu-command/src/date/now.rs @@ -46,7 +46,7 @@ impl Command for SubCommand { vec![ Example { description: "Get the current date and display it in a given format string.", - example: r#"date now | date format "%Y-%m-%d %H:%M:%S""#, + example: r#"date now | format date "%Y-%m-%d %H:%M:%S""#, result: None, }, Example { diff --git a/crates/nu-command/src/date/utils.rs b/crates/nu-command/src/date/utils.rs index 75bb152315..e1f3c546fd 100644 --- a/crates/nu-command/src/date/utils.rs +++ b/crates/nu-command/src/date/utils.rs @@ -24,3 +24,280 @@ pub(crate) fn parse_date_from_string( }), } } + +/// Generates a table containing available datetime format specifiers +/// +/// # Arguments +/// * `head` - use the call's head +/// * `show_parse_only_formats` - whether parse-only format specifiers (that can't be outputted) should be shown. Should only be used for `into datetime`, not `format date` +pub(crate) fn generate_strftime_list(head: Span, show_parse_only_formats: bool) -> Value { + let column_names = vec![ + "Specification".into(), + "Example".into(), + "Description".into(), + ]; + let now = Local::now(); + + struct FormatSpecification<'a> { + spec: &'a str, + description: &'a str, + } + + let specifications = vec![ + FormatSpecification { + spec: "%Y", + description: "The full proleptic Gregorian year, zero-padded to 4 digits.", + }, + FormatSpecification { + spec: "%C", + description: "The proleptic Gregorian year divided by 100, zero-padded to 2 digits.", + }, + FormatSpecification { + spec: "%y", + description: "The proleptic Gregorian year modulo 100, zero-padded to 2 digits.", + }, + FormatSpecification { + spec: "%m", + description: "Month number (01--12), zero-padded to 2 digits.", + }, + FormatSpecification { + spec: "%b", + description: "Abbreviated month name. Always 3 letters.", + }, + FormatSpecification { + spec: "%B", + description: "Full month name. Also accepts corresponding abbreviation in parsing.", + }, + FormatSpecification { + spec: "%h", + description: "Same as %b.", + }, + FormatSpecification { + spec: "%d", + description: "Day number (01--31), zero-padded to 2 digits.", + }, + FormatSpecification { + spec: "%e", + description: "Same as %d but space-padded. Same as %_d.", + }, + FormatSpecification { + spec: "%a", + description: "Abbreviated weekday name. Always 3 letters.", + }, + FormatSpecification { + spec: "%A", + description: "Full weekday name. Also accepts corresponding abbreviation in parsing.", + }, + FormatSpecification { + spec: "%w", + description: "Sunday = 0, Monday = 1, ..., Saturday = 6.", + }, + FormatSpecification { + spec: "%u", + description: "Monday = 1, Tuesday = 2, ..., Sunday = 7. (ISO 8601)", + }, + FormatSpecification { + spec: "%U", + description: "Week number starting with Sunday (00--53), zero-padded to 2 digits.", + }, + FormatSpecification { + spec: "%W", + description: + "Same as %U, but week 1 starts with the first Monday in that year instead.", + }, + FormatSpecification { + spec: "%G", + description: "Same as %Y but uses the year number in ISO 8601 week date.", + }, + FormatSpecification { + spec: "%g", + description: "Same as %y but uses the year number in ISO 8601 week date.", + }, + FormatSpecification { + spec: "%V", + description: "Same as %U but uses the week number in ISO 8601 week date (01--53).", + }, + FormatSpecification { + spec: "%j", + description: "Day of the year (001--366), zero-padded to 3 digits.", + }, + FormatSpecification { + spec: "%D", + description: "Month-day-year format. Same as %m/%d/%y.", + }, + FormatSpecification { + spec: "%x", + description: "Locale's date representation (e.g., 12/31/99).", + }, + FormatSpecification { + spec: "%F", + description: "Year-month-day format (ISO 8601). Same as %Y-%m-%d.", + }, + FormatSpecification { + spec: "%v", + description: "Day-month-year format. Same as %e-%b-%Y.", + }, + FormatSpecification { + spec: "%H", + description: "Hour number (00--23), zero-padded to 2 digits.", + }, + FormatSpecification { + spec: "%k", + description: "Same as %H but space-padded. Same as %_H.", + }, + FormatSpecification { + spec: "%I", + description: "Hour number in 12-hour clocks (01--12), zero-padded to 2 digits.", + }, + FormatSpecification { + spec: "%l", + description: "Same as %I but space-padded. Same as %_I.", + }, + FormatSpecification { + spec: "%P", + description: "am or pm in 12-hour clocks.", + }, + FormatSpecification { + spec: "%p", + description: "AM or PM in 12-hour clocks.", + }, + FormatSpecification { + spec: "%M", + description: "Minute number (00--59), zero-padded to 2 digits.", + }, + FormatSpecification { + spec: "%S", + description: "Second number (00--60), zero-padded to 2 digits.", + }, + FormatSpecification { + spec: "%f", + description: "The fractional seconds (in nanoseconds) since last whole second.", + }, + FormatSpecification { + spec: "%.f", + description: "Similar to .%f but left-aligned. These all consume the leading dot.", + }, + FormatSpecification { + spec: "%.3f", + description: "Similar to .%f but left-aligned but fixed to a length of 3.", + }, + FormatSpecification { + spec: "%.6f", + description: "Similar to .%f but left-aligned but fixed to a length of 6.", + }, + FormatSpecification { + spec: "%.9f", + description: "Similar to .%f but left-aligned but fixed to a length of 9.", + }, + FormatSpecification { + spec: "%3f", + description: "Similar to %.3f but without the leading dot.", + }, + FormatSpecification { + spec: "%6f", + description: "Similar to %.6f but without the leading dot.", + }, + FormatSpecification { + spec: "%9f", + description: "Similar to %.9f but without the leading dot.", + }, + FormatSpecification { + spec: "%R", + description: "Hour-minute format. Same as %H:%M.", + }, + FormatSpecification { + spec: "%T", + description: "Hour-minute-second format. Same as %H:%M:%S.", + }, + FormatSpecification { + spec: "%X", + description: "Locale's time representation (e.g., 23:13:48).", + }, + FormatSpecification { + spec: "%r", + description: "Hour-minute-second format in 12-hour clocks. Same as %I:%M:%S %p.", + }, + FormatSpecification { + spec: "%Z", + description: + "Local time zone name. Skips all non-whitespace characters during parsing.", + }, + FormatSpecification { + spec: "%z", + description: "Offset from the local time to UTC (with UTC being +0000).", + }, + FormatSpecification { + spec: "%:z", + description: "Same as %z but with a colon.", + }, + FormatSpecification { + spec: "%c", + description: "Locale's date and time (e.g., Thu Mar 3 23:05:25 2005).", + }, + FormatSpecification { + spec: "%+", + description: "ISO 8601 / RFC 3339 date & time format.", + }, + FormatSpecification { + spec: "%s", + description: "UNIX timestamp, the number of seconds since 1970-01-01", + }, + FormatSpecification { + spec: "%t", + description: "Literal tab (\\t).", + }, + FormatSpecification { + spec: "%n", + description: "Literal newline (\\n).", + }, + FormatSpecification { + spec: "%%", + description: "Literal percent sign.", + }, + ]; + + let mut records = specifications + .iter() + .map(|s| Value::Record { + cols: column_names.clone(), + vals: vec![ + Value::string(s.spec, head), + Value::string(now.format(s.spec).to_string(), head), + Value::string(s.description, head), + ], + span: head, + }) + .collect::>(); + + if show_parse_only_formats { + // now.format("%#z") will panic since it is parse-only + // so here we emulate how it will look: + let example = now + .format("%:z") // e.g. +09:30 + .to_string() + .get(0..3) // +09:30 -> +09 + .unwrap_or("") + .to_string(); + + records.push(Value::Record { + cols: column_names, + vals: vec![ + Value::string("%#z", head), + Value::String { + val: example, + span: head, + }, + Value::string( + "Parsing only: Same as %z but allows minutes to be missing or present.", + head, + ), + ], + span: head, + }); + } + + Value::List { + vals: records, + span: head, + } +} diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 66b64137dc..5cb1a6edc7 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -197,7 +197,8 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { StrSubstring, StrTrim, StrTitleCase, - StrUpcase + StrUpcase, + FormatDate }; // FileSystem @@ -233,7 +234,6 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { // Date bind_command! { Date, - DateFormat, DateHumanize, DateListTimezones, DateNow, @@ -300,6 +300,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { // Env bind_command! { LetEnvDeprecated, + DateFormat, ExportEnv, LoadEnv, SourceEnv, diff --git a/crates/nu-command/src/deprecated/format.rs b/crates/nu-command/src/deprecated/format.rs new file mode 100644 index 0000000000..9562ca08bf --- /dev/null +++ b/crates/nu-command/src/deprecated/format.rs @@ -0,0 +1,50 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape, Type}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "date format" + } + + fn signature(&self) -> Signature { + Signature::build("date format") + .input_output_types(vec![ + (Type::Date, Type::String), + (Type::String, Type::String), + ]) + .allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032 + .switch("list", "lists strftime cheatsheet", Some('l')) + .optional( + "format string", + SyntaxShape::String, + "the desired date format", + ) + .category(Category::Date) + } + + fn usage(&self) -> &str { + "Format a given date using a format string." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["fmt", "strftime"] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Err(nu_protocol::ShellError::DeprecatedCommand( + self.name().to_string(), + "format date".to_owned(), + call.head, + )) + } +} diff --git a/crates/nu-command/src/deprecated/mod.rs b/crates/nu-command/src/deprecated/mod.rs index 40e993f189..d72efcee85 100644 --- a/crates/nu-command/src/deprecated/mod.rs +++ b/crates/nu-command/src/deprecated/mod.rs @@ -1,5 +1,6 @@ mod collect; mod deprecated_commands; +mod format; mod hash_base64; mod let_env; mod lpad; @@ -12,6 +13,7 @@ mod str_int; pub use collect::StrCollectDeprecated; pub use deprecated_commands::*; +pub use format::SubCommand as DateFormat; pub use hash_base64::HashBase64; pub use let_env::LetEnvDeprecated; pub use lpad::LPadDeprecated; diff --git a/crates/nu-command/src/strings/format/date.rs b/crates/nu-command/src/strings/format/date.rs new file mode 100644 index 0000000000..0ede5a8a1c --- /dev/null +++ b/crates/nu-command/src/strings/format/date.rs @@ -0,0 +1,198 @@ +use chrono::{DateTime, Locale, TimeZone}; + +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, + Value, +}; +use nu_utils::locale::get_system_locale_string; +use std::fmt::{Display, Write}; + +use crate::{generate_strftime_list, parse_date_from_string}; + +#[derive(Clone)] +pub struct FormatDate; + +impl Command for FormatDate { + fn name(&self) -> &str { + "format date" + } + + fn signature(&self) -> Signature { + Signature::build("format date") + .input_output_types(vec![ + (Type::Date, Type::String), + (Type::String, Type::String), + ]) + .allow_variants_without_examples(true) // https://github.com/nushell/nushell/issues/7032 + .switch("list", "lists strftime cheatsheet", Some('l')) + .optional( + "format string", + SyntaxShape::String, + "the desired format date", + ) + .category(Category::Date) + } + + fn usage(&self) -> &str { + "Format a given date using a format string." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["fmt", "strftime"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + if call.has_flag("list") { + return Ok(PipelineData::Value( + generate_strftime_list(head, false), + None, + )); + } + + let format = call.opt::>(engine_state, stack, 0)?; + + // This doesn't match explicit nulls + if matches!(input, PipelineData::Empty) { + return Err(ShellError::PipelineEmpty { dst_span: head }); + } + input.map( + move |value| match &format { + Some(format) => format_helper(value, format.item.as_str(), format.span, head), + None => format_helper_rfc2822(value, head), + }, + engine_state.ctrlc.clone(), + ) + } + + fn examples(&self) -> Vec { + vec![ + // TODO: This should work but does not; see https://github.com/nushell/nushell/issues/7032 + // Example { + // description: "Format a given date-time using the default format (RFC 2822).", + // example: r#"'2021-10-22 20:00:12 +01:00' | into datetime | format date"#, + // result: Some(Value::String { + // val: "Fri, 22 Oct 2021 20:00:12 +0100".to_string(), + // span: Span::test_data(), + // }), + // }, + Example { + description: + "Format a given date-time as a string using the default format (RFC 2822).", + example: r#""2021-10-22 20:00:12 +01:00" | format date"#, + result: Some(Value::String { + val: "Fri, 22 Oct 2021 20:00:12 +0100".to_string(), + span: Span::test_data(), + }), + }, + Example { + description: "Format the current date-time using a given format string.", + example: r#"date now | format date "%Y-%m-%d %H:%M:%S""#, + result: None, + }, + Example { + description: "Format the current date using a given format string.", + example: r#"date now | format date "%Y-%m-%d %H:%M:%S""#, + result: None, + }, + Example { + description: "Format a given date using a given format string.", + example: r#""2021-10-22 20:00:12 +01:00" | format date "%Y-%m-%d""#, + result: Some(Value::test_string("2021-10-22")), + }, + ] + } +} + +fn format_from(date_time: DateTime, formatter: &str, span: Span) -> Value +where + Tz::Offset: Display, +{ + let mut formatter_buf = String::new(); + let locale: Locale = get_system_locale_string() + .map(|l| l.replace('-', "_")) // `chrono::Locale` needs something like `xx_xx`, rather than `xx-xx` + .unwrap_or_else(|| String::from("en_US")) + .as_str() + .try_into() + .unwrap_or(Locale::en_US); + let format = date_time.format_localized(formatter, locale); + + match formatter_buf.write_fmt(format_args!("{format}")) { + Ok(_) => Value::String { + val: formatter_buf, + span, + }, + Err(_) => Value::Error { + error: Box::new(ShellError::TypeMismatch { + err_message: "invalid format".to_string(), + span, + }), + }, + } +} + +fn format_helper(value: Value, formatter: &str, formatter_span: Span, head_span: Span) -> Value { + match value { + Value::Date { val, .. } => format_from(val, formatter, formatter_span), + Value::String { val, .. } => { + let dt = parse_date_from_string(&val, formatter_span); + + match dt { + Ok(x) => format_from(x, formatter, formatter_span), + Err(e) => e, + } + } + _ => Value::Error { + error: Box::new(ShellError::DatetimeParseError( + value.debug_value(), + head_span, + )), + }, + } +} + +fn format_helper_rfc2822(value: Value, span: Span) -> Value { + match value { + Value::Date { val, span: _ } => Value::String { + val: val.to_rfc2822(), + span, + }, + Value::String { + val, + span: val_span, + } => { + let dt = parse_date_from_string(&val, val_span); + match dt { + Ok(x) => Value::String { + val: x.to_rfc2822(), + span, + }, + Err(e) => e, + } + } + _ => Value::Error { + error: Box::new(ShellError::DatetimeParseError(value.debug_value(), span)), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(FormatDate {}) + } +} diff --git a/crates/nu-command/src/strings/format/mod.rs b/crates/nu-command/src/strings/format/mod.rs new file mode 100644 index 0000000000..635a9568f3 --- /dev/null +++ b/crates/nu-command/src/strings/format/mod.rs @@ -0,0 +1,3 @@ +mod date; + +pub use date::FormatDate; diff --git a/crates/nu-command/src/strings/mod.rs b/crates/nu-command/src/strings/mod.rs index fa4cda4563..5f1694a024 100644 --- a/crates/nu-command/src/strings/mod.rs +++ b/crates/nu-command/src/strings/mod.rs @@ -1,6 +1,7 @@ mod char_; mod detect_columns; mod encode_decode; +mod format; mod parse; mod size; mod split; @@ -9,6 +10,7 @@ mod str_; pub use char_::Char; pub use detect_columns::*; pub use encode_decode::*; +pub use format::FormatDate; pub use parse::*; pub use size::Size; pub use split::*; diff --git a/crates/nu-command/tests/commands/date/format.rs b/crates/nu-command/tests/commands/date/format.rs index 21311663c2..b7698c2629 100644 --- a/crates/nu-command/tests/commands/date/format.rs +++ b/crates/nu-command/tests/commands/date/format.rs @@ -3,7 +3,7 @@ use nu_test_support::{nu, pipeline}; #[test] fn formatter_not_valid() { let actual = nu!(r#" - date now | date format '%N' + date now | format date '%N' "#); assert!(actual.err.contains("invalid format")); @@ -12,7 +12,7 @@ fn formatter_not_valid() { #[test] fn fails_without_input() { let actual = nu!(r#" - date format "%c" + format date "%c" "#); assert!(actual.err.contains("Pipeline empty")); @@ -24,7 +24,7 @@ fn locale_format_respect_different_locale() { locale: "en_US", pipeline( r#" - "2021-10-22 20:00:12 +01:00" | date format "%c" + "2021-10-22 20:00:12 +01:00" | format date "%c" "# ) ); @@ -34,7 +34,7 @@ fn locale_format_respect_different_locale() { locale: "en_GB", pipeline( r#" - "2021-10-22 20:00:12 +01:00" | date format "%c" + "2021-10-22 20:00:12 +01:00" | format date "%c" "# ) ); @@ -44,7 +44,7 @@ fn locale_format_respect_different_locale() { locale: "de_DE", pipeline( r#" - "2021-10-22 20:00:12 +01:00" | date format "%c" + "2021-10-22 20:00:12 +01:00" | format date "%c" "# ) ); @@ -54,7 +54,7 @@ fn locale_format_respect_different_locale() { locale: "zh_CN", pipeline( r#" - "2021-10-22 20:00:12 +01:00" | date format "%c" + "2021-10-22 20:00:12 +01:00" | format date "%c" "# ) ); @@ -64,7 +64,7 @@ fn locale_format_respect_different_locale() { locale: "ja_JP", pipeline( r#" - "2021-10-22 20:00:12 +01:00" | date format "%c" + "2021-10-22 20:00:12 +01:00" | format date "%c" "# ) ); @@ -74,7 +74,7 @@ fn locale_format_respect_different_locale() { locale: "fr_FR", pipeline( r#" - "2021-10-22 20:00:12 +01:00" | date format "%c" + "2021-10-22 20:00:12 +01:00" | format date "%c" "# ) ); diff --git a/crates/nu-std/std/log.nu b/crates/nu-std/std/log.nu index 93384a65e2..0c46a83c67 100644 --- a/crates/nu-std/std/log.nu +++ b/crates/nu-std/std/log.nu @@ -139,7 +139,7 @@ def current-log-level [] { } def now [] { - date now | date format "%Y-%m-%dT%H:%M:%S%.3f" + date now | format date "%Y-%m-%dT%H:%M:%S%.3f" } def handle-log [ diff --git a/crates/nu-std/tests/logger_tests/commons.nu b/crates/nu-std/tests/logger_tests/commons.nu index b07c5c1c0a..7538d50eb9 100644 --- a/crates/nu-std/tests/logger_tests/commons.nu +++ b/crates/nu-std/tests/logger_tests/commons.nu @@ -1,5 +1,5 @@ export def now [] { - date now | date format "%Y-%m-%dT%H:%M:%S%.3f" + date now | format date "%Y-%m-%dT%H:%M:%S%.3f" } export def format-message [ @@ -17,4 +17,4 @@ export def format-message [ ] | reduce --fold $format { |it, acc| $acc | str replace --all $it.0 $it.1 } -} \ No newline at end of file +} diff --git a/crates/nu-utils/src/sample_config/default_env.nu b/crates/nu-utils/src/sample_config/default_env.nu index 0dbbf1b5b1..f78b2fa2be 100644 --- a/crates/nu-utils/src/sample_config/default_env.nu +++ b/crates/nu-utils/src/sample_config/default_env.nu @@ -29,7 +29,7 @@ def create_right_prompt [] { let time_segment = ([ (ansi reset) (ansi magenta) - (date now | date format '%Y/%m/%d %r') + (date now | format date '%Y/%m/%d %r') ] | str join | str replace --all "([/:])" $"(ansi green)${1}(ansi magenta)" | str replace --all "([AP]M)" $"(ansi magenta_underline)${1}") diff --git a/src/tests/test_type_check.rs b/src/tests/test_type_check.rs index cc7da49ce3..aeb9861a6f 100644 --- a/src/tests/test_type_check.rs +++ b/src/tests/test_type_check.rs @@ -32,14 +32,14 @@ fn number_float() -> TestResult { #[test] fn date_minus_duration() -> TestResult { - let input = "2023-04-22 - 2day | date format %Y-%m-%d"; + let input = "2023-04-22 - 2day | format date %Y-%m-%d"; let expected = "2023-04-20"; run_test(input, expected) } #[test] fn date_plus_duration() -> TestResult { - let input = "2023-04-18 + 2day | date format %Y-%m-%d"; + let input = "2023-04-18 + 2day | format date %Y-%m-%d"; let expected = "2023-04-20"; run_test(input, expected) }