diff --git a/Cargo.lock b/Cargo.lock index e4b2a19d63..c935485000 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2846,6 +2846,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" @@ -3593,6 +3602,7 @@ dependencies = [ "rust-embed", "serde", "serde_urlencoded", + "uucore", "v_htmlescape", ] @@ -7787,6 +7797,7 @@ dependencies = [ "dunce", "glob", "iana-time-zone", + "itertools 0.14.0", "libc", "nix 0.29.0", "number_prefix", diff --git a/crates/nu-cmd-extra/Cargo.toml b/crates/nu-cmd-extra/Cargo.toml index c68724e429..99786be66b 100644 --- a/crates/nu-cmd-extra/Cargo.toml +++ b/crates/nu-cmd-extra/Cargo.toml @@ -35,6 +35,7 @@ serde_urlencoded = { workspace = true } v_htmlescape = { workspace = true } itertools = { workspace = true } mime = { workspace = true } +uucore = {workspace = true, features = ["format"]} [dev-dependencies] nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.103.1" } diff --git a/crates/nu-cmd-extra/src/extra/strings/format/command.rs b/crates/nu-cmd-extra/src/extra/strings/format/command.rs index 18206acba6..35cdfd529a 100644 --- a/crates/nu-cmd-extra/src/extra/strings/format/command.rs +++ b/crates/nu-cmd-extra/src/extra/strings/format/command.rs @@ -1,5 +1,6 @@ use nu_engine::command_prelude::*; use nu_protocol::{ast::PathMember, engine::StateWorkingSet, Config, ListStream}; +use uucore::format::{parse_spec_and_escape, FormatArgument, FormatError, FormatItem}; #[derive(Clone)] pub struct FormatPattern; @@ -12,20 +13,26 @@ impl Command for FormatPattern { fn signature(&self) -> Signature { Signature::build("format pattern") .input_output_types(vec![ + (Type::list(Type::Any), Type::String), (Type::table(), Type::List(Box::new(Type::String))), (Type::record(), Type::Any), + (Type::Any, Type::String), ]) .required( "pattern", SyntaxShape::String, "The pattern to output. e.g.) \"{foo}: {bar}\".", ) + .switch("printf", "Use printf style pattern.", None) .allow_variants_without_examples(true) .category(Category::Strings) } fn description(&self) -> &str { - "Format columns into a string using a simple pattern." + r"Format values into a string using either a simple pattern or `printf`-compatible pattern. +Simple pattern supports input of type list, table, and record; +`printf` pattern supports a limited set of primitive types as input, namely string, int and float, and composite types such as list, table + " } fn run( @@ -37,6 +44,7 @@ impl Command for FormatPattern { ) -> Result { let mut working_set = StateWorkingSet::new(engine_state); + let use_printf = call.has_flag(engine_state, stack, "printf")?; let specified_pattern: Result = call.req(engine_state, stack, 0); let input_val = input.into_value(call.head)?; // add '$it' variable to support format like this: $it.column1.column2. @@ -45,9 +53,9 @@ impl Command for FormatPattern { let config = stack.get_config(engine_state); - match specified_pattern { - Err(e) => Err(e), - Ok(pattern) => { + match (specified_pattern, use_printf) { + (Err(e), _) => Err(e), + (Ok(pattern), false) => { let string_span = pattern.span(); let string_pattern = pattern.coerce_into_string()?; // the string span is start as `"`, we don't need the character @@ -60,6 +68,7 @@ impl Command for FormatPattern { format(input_val, &ops, engine_state, &config, call.head) } + (Ok(pattern), true) => format_printf(input_val, pattern, call.head), } } @@ -78,6 +87,16 @@ impl Command for FormatPattern { Span::test_data(), )), }, + Example { + description: "Unescape a fully quoted json using printf", + example: r#""\{\\\"foo\\\": \\\"bar\\\"\}" | format pattern --printf "%b""#, + result: Some(Value::test_string(r#"{"foo": "bar"}"#)), + }, + Example { + description: "Using printf to substitute multiple args", + example: r#"["a", 1] | format pattern --printf "first = %s, second = %d""#, + result: Some(Value::test_string("first = a, second = 1")), + }, ] } } @@ -265,6 +284,86 @@ fn format_record( Ok(output) } +fn assert_specifier_count_eq_arg_count( + spec_count: usize, + arg_count: usize, + span: Span, +) -> Result<(), ShellError> { + if spec_count != arg_count { + Err(ShellError::IncompatibleParametersSingle { + msg: format!( + "Number of arguments ({}) provided does not match the number of specifiers ({}) within the pattern.", + arg_count, spec_count, + ) + .into(), + span: span, + }) + } else { + Ok(()) + } +} + +fn format_printf( + input_data: Value, + pattern: Value, + head_span: Span, +) -> Result { + let pattern_str = pattern.coerce_into_string()?; + let spec_count = parse_spec_and_escape(pattern_str.as_ref()) + .filter_map(|item| match item { + Ok(FormatItem::Spec(_)) => Some(()), + _ => None, + }) + .count(); + let args: Vec = match input_data { + v @ Value::List { .. } => { + let span = v.span(); + let vals = v.into_list()?; + let arg_count = Vec::len(&vals); + assert_specifier_count_eq_arg_count(spec_count, arg_count, span)?; + vals.into_iter() + .map(Value::coerce_into_string) + .collect::, _>>()? + } + v @ Value::Nothing {..} => { + assert_specifier_count_eq_arg_count(spec_count, 0, v.span())?; + vec![] + } + v => { + assert_specifier_count_eq_arg_count(spec_count, 1, v.span())?; + vec![v.coerce_into_string()?] + } + }; + + match printf_spec_escape(pattern_str, args) { + Ok(value) => Ok(PipelineData::Value(Value::string(value, head_span), None)), + Err(err) => Err(ShellError::GenericError { + error: err.to_string(), + msg: err.to_string(), + span: Some(head_span), + help: None, + inner: vec![], + }), + } +} + +pub fn printf_spec_escape(pattern: String, args: Vec) -> Result { + let mut writer: Vec<_> = Vec::new(); + let args: Vec = args.into_iter().map(FormatArgument::Unparsed).collect(); + let mut args = args.iter().peekable(); + + for item in parse_spec_and_escape(pattern.as_ref()) { + match item { + Ok(item) => { + item.write(&mut writer, &mut args)?; + } + Err(e) => return Err(e), + } + } + + Ok(String::from_utf8_lossy(&writer).to_string()) +} + #[cfg(test)] mod test { #[test]