diff --git a/crates/nu-color-config/src/style_computer.rs b/crates/nu-color-config/src/style_computer.rs index 660156fe26..8ea7fc75b7 100644 --- a/crates/nu-color-config/src/style_computer.rs +++ b/crates/nu-color-config/src/style_computer.rs @@ -223,7 +223,7 @@ fn test_computable_style_closure_basic() { ]; let actual_repl = nu!(cwd: dirs.test(), nu_repl_code(&inp)); assert_eq!(actual_repl.err, ""); - assert_eq!(actual_repl.out, "[bell.obj, book.obj, candle.obj]"); + assert_eq!(actual_repl.out, r#"["bell.obj", "book.obj", "candle.obj"]"#); }); } diff --git a/crates/nu-command/src/filters/flatten.rs b/crates/nu-command/src/filters/flatten.rs index 3e285d21aa..982db17029 100644 --- a/crates/nu-command/src/filters/flatten.rs +++ b/crates/nu-command/src/filters/flatten.rs @@ -157,7 +157,7 @@ fn flat_value(columns: &[CellPath], item: Value, all: bool) -> Vec { let mut inner_table = None; for (column_index, (column, value)) in val.into_owned().into_iter().enumerate() { - let column_requested = columns.iter().find(|c| c.to_string() == column); + let column_requested = columns.iter().find(|c| c.to_column_name() == column); let need_flatten = { columns.is_empty() || column_requested.is_some() }; let span = value.span(); diff --git a/crates/nu-command/src/filters/select.rs b/crates/nu-command/src/filters/select.rs index 45b965861e..670def2d09 100644 --- a/crates/nu-command/src/filters/select.rs +++ b/crates/nu-command/src/filters/select.rs @@ -240,7 +240,7 @@ fn select( //FIXME: improve implementation to not clone match input_val.clone().follow_cell_path(&path.members, false) { Ok(fetcher) => { - record.push(path.to_string(), fetcher); + record.push(path.to_column_name(), fetcher); if !columns_with_value.contains(&path) { columns_with_value.push(path); } @@ -271,7 +271,7 @@ fn select( // FIXME: remove clone match v.clone().follow_cell_path(&cell_path.members, false) { Ok(result) => { - record.push(cell_path.to_string(), result); + record.push(cell_path.to_column_name(), result); } Err(e) => return Err(e), } @@ -295,7 +295,7 @@ fn select( //FIXME: improve implementation to not clone match x.clone().follow_cell_path(&path.members, false) { Ok(value) => { - record.push(path.to_string(), value); + record.push(path.to_column_name(), value); } Err(e) => return Err(e), } diff --git a/crates/nu-command/tests/commands/reject.rs b/crates/nu-command/tests/commands/reject.rs index a6ba9dd08f..a2c052b3e1 100644 --- a/crates/nu-command/tests/commands/reject.rs +++ b/crates/nu-command/tests/commands/reject.rs @@ -130,7 +130,10 @@ fn reject_optional_row() { #[test] fn reject_columns_with_list_spread() { let actual = nu!("let arg = [type size]; [[name type size];[Cargo.toml file 10mb] [Cargo.lock file 10mb] [src dir 100mb]] | reject ...$arg | to nuon"); - assert_eq!(actual.out, "[[name]; [Cargo.toml], [Cargo.lock], [src]]"); + assert_eq!( + actual.out, + r#"[[name]; ["Cargo.toml"], ["Cargo.lock"], [src]]"# + ); } #[test] @@ -138,7 +141,7 @@ fn reject_rows_with_list_spread() { let actual = nu!("let arg = [2 0]; [[name type size];[Cargo.toml file 10mb] [Cargo.lock file 10mb] [src dir 100mb]] | reject ...$arg | to nuon"); assert_eq!( actual.out, - "[[name, type, size]; [Cargo.lock, file, 10000000b]]" + r#"[[name, type, size]; ["Cargo.lock", file, 10000000b]]"# ); } @@ -147,7 +150,7 @@ fn reject_mixed_with_list_spread() { let actual = nu!("let arg = [type 2]; [[name type size];[Cargp.toml file 10mb] [ Cargo.lock file 10mb] [src dir 100mb]] | reject ...$arg | to nuon"); assert_eq!( actual.out, - "[[name, size]; [Cargp.toml, 10000000b], [Cargo.lock, 10000000b]]" + r#"[[name, size]; ["Cargp.toml", 10000000b], ["Cargo.lock", 10000000b]]"# ); } diff --git a/crates/nu-command/tests/commands/select.rs b/crates/nu-command/tests/commands/select.rs index f47845d6ed..004aa16d6b 100644 --- a/crates/nu-command/tests/commands/select.rs +++ b/crates/nu-command/tests/commands/select.rs @@ -166,7 +166,7 @@ fn select_ignores_errors_successfully1() { fn select_ignores_errors_successfully2() { let actual = nu!("[{a: 1} {a: 2} {a: 3}] | select b? | to nuon"); - assert_eq!(actual.out, "[[b?]; [null], [null], [null]]".to_string()); + assert_eq!(actual.out, "[[b]; [null], [null], [null]]".to_string()); assert!(actual.err.is_empty()); } @@ -174,7 +174,7 @@ fn select_ignores_errors_successfully2() { fn select_ignores_errors_successfully3() { let actual = nu!("{foo: bar} | select invalid_key? | to nuon"); - assert_eq!(actual.out, "{invalid_key?: null}".to_string()); + assert_eq!(actual.out, "{invalid_key: null}".to_string()); assert!(actual.err.is_empty()); } @@ -184,10 +184,7 @@ fn select_ignores_errors_successfully4() { r#""key val\na 1\nb 2\n" | lines | split column --collapse-empty " " | select foo? | to nuon"# ); - assert_eq!( - actual.out, - r#"[[foo?]; [null], [null], [null]]"#.to_string() - ); + assert_eq!(actual.out, r#"[[foo]; [null], [null], [null]]"#.to_string()); assert!(actual.err.is_empty()); } @@ -237,7 +234,7 @@ fn ignore_errors_works() { [{}] | select -i $path | to nuon "#); - assert_eq!(actual.out, "[[foo?]; [null]]"); + assert_eq!(actual.out, "[[foo]; [null]]"); } #[test] diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index 635874e0b0..a6b05474b1 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -59,6 +59,7 @@ serde_json = { workspace = true } strum = "0.26" strum_macros = "0.26" nu-test-support = { path = "../nu-test-support", version = "0.99.2" } +nu-utils = { path = "../nu-utils", version = "0.99.2" } pretty_assertions = { workspace = true } rstest = { workspace = true } tempfile = { workspace = true } diff --git a/crates/nu-protocol/src/ast/cell_path.rs b/crates/nu-protocol/src/ast/cell_path.rs index f208dd8120..3c2e9b3dd7 100644 --- a/crates/nu-protocol/src/ast/cell_path.rs +++ b/crates/nu-protocol/src/ast/cell_path.rs @@ -1,5 +1,6 @@ use super::Expression; use crate::Span; +use nu_utils::{escape_quote_string, needs_quoting}; use serde::{Deserialize, Serialize}; use std::{cmp::Ordering, fmt::Display}; @@ -173,22 +174,46 @@ impl CellPath { member.make_optional(); } } + + // Formats the cell-path as a column name, i.e. without quoting and optional markers ('?'). + pub fn to_column_name(&self) -> String { + let mut s = String::new(); + + for member in &self.members { + match member { + PathMember::Int { val, .. } => { + s += &val.to_string(); + } + PathMember::String { val, .. } => { + s += val; + } + } + + s.push('.'); + } + + s.pop(); // Easier than checking whether to insert the '.' on every iteration. + s + } } impl Display for CellPath { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (idx, elem) in self.members.iter().enumerate() { - if idx > 0 { - write!(f, ".")?; - } - match elem { + write!(f, "$")?; + for member in self.members.iter() { + match member { PathMember::Int { val, optional, .. } => { let question_mark = if *optional { "?" } else { "" }; - write!(f, "{val}{question_mark}")? + write!(f, ".{val}{question_mark}")? } PathMember::String { val, optional, .. } => { let question_mark = if *optional { "?" } else { "" }; - write!(f, "{val}{question_mark}")? + let val = if needs_quoting(val) { + &escape_quote_string(val) + } else { + val + }; + write!(f, ".{val}{question_mark}")? } } } diff --git a/crates/nu-utils/src/quoting.rs b/crates/nu-utils/src/quoting.rs index baff52caeb..35ca0711bd 100644 --- a/crates/nu-utils/src/quoting.rs +++ b/crates/nu-utils/src/quoting.rs @@ -2,12 +2,12 @@ use fancy_regex::Regex; use std::sync::LazyLock; // This hits, in order: -// • Any character of []:`{}#'";()|$, +// • Any character of []:`{}#'";()|$,. // • Any digit (\d) // • Any whitespace (\s) // • Case-insensitive sign-insensitive float "keywords" inf, infinity and nan. static NEEDS_QUOTING_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(r#"[\[\]:`\{\}#'";\(\)\|\$,\d\s]|(?i)^[+\-]?(inf(inity)?|nan)$"#) + Regex::new(r#"[\[\]:`\{\}#'";\(\)\|\$,\.\d\s]|(?i)^[+\-]?(inf(inity)?|nan)$"#) .expect("internal error: NEEDS_QUOTING_REGEX didn't compile") }); diff --git a/crates/nuon/src/to.rs b/crates/nuon/src/to.rs index c2af51d0bb..b6c4f7829b 100644 --- a/crates/nuon/src/to.rs +++ b/crates/nuon/src/to.rs @@ -97,7 +97,7 @@ fn value_to_string( Ok("false".to_string()) } } - Value::CellPath { val, .. } => Ok(format!("$.{}", val)), + Value::CellPath { val, .. } => Ok(val.to_string()), Value::Custom { .. } => Err(ShellError::UnsupportedInput { msg: "custom values are currently not nuon-compatible".to_string(), input: "value originates from here".into(), @@ -116,10 +116,10 @@ fn value_to_string( if &val.round() == val && val.is_finite() { Ok(format!("{}.0", *val)) } else { - Ok(format!("{}", *val)) + Ok(val.to_string()) } } - Value::Int { val, .. } => Ok(format!("{}", *val)), + Value::Int { val, .. } => Ok(val.to_string()), Value::List { vals, .. } => { let headers = get_columns(vals); if !headers.is_empty() && vals.iter().all(|x| x.columns().eq(headers.iter())) { diff --git a/tests/repl/test_spread.rs b/tests/repl/test_spread.rs index f0ce84d393..5c3311caf4 100644 --- a/tests/repl/test_spread.rs +++ b/tests/repl/test_spread.rs @@ -30,7 +30,7 @@ fn not_spread() -> TestResult { run_test(r#"def ... [x] { $x }; ... ..."#, "...").unwrap(); run_test( r#"let a = 4; [... $a ... [1] ... (5) ...bare ...] | to nuon"#, - "[..., 4, ..., [1], ..., 5, ...bare, ...]", + r#"["...", 4, "...", [1], "...", 5, "...bare", "..."]"#, ) }