From 0294419c76fcc403655d1e7c61597086cc2c766d Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Sun, 4 May 2025 09:58:06 +0300 Subject: [PATCH 01/10] feat(cell-path): opt-in case insensitivity for cell-path members - Add `CellPath::String::insensitive: bool`, represented with an exclamation mark in `Display` - Update `into cell-path` and `split cell-path` to take case insensitivity into account while constructing and splitting cell-paths --- .../src/conversions/into/cell_path.rs | 25 ++++++--- .../src/conversions/split_cell_path.rs | 25 +++++++-- crates/nu-command/src/filters/empty.rs | 2 +- crates/nu-protocol/src/ast/cell_path.rs | 54 +++++++++++++++---- 4 files changed, 84 insertions(+), 22 deletions(-) diff --git a/crates/nu-command/src/conversions/into/cell_path.rs b/crates/nu-command/src/conversions/into/cell_path.rs index d96d76deab..462d425e35 100644 --- a/crates/nu-command/src/conversions/into/cell_path.rs +++ b/crates/nu-command/src/conversions/into/cell_path.rs @@ -17,7 +17,12 @@ impl Command for IntoCellPath { (Type::List(Box::new(Type::Any)), Type::CellPath), ( Type::List(Box::new(Type::Record( - [("value".into(), Type::Any), ("optional".into(), Type::Bool)].into(), + [ + ("value".into(), Type::Any), + ("optional".into(), Type::Bool), + ("insensitive".into(), Type::Bool), + ] + .into(), ))), Type::CellPath, ), @@ -69,8 +74,8 @@ impl Command for IntoCellPath { example: "'some.path' | split row '.' | into cell-path", result: Some(Value::test_cell_path(CellPath { members: vec![ - PathMember::test_string("some".into(), false), - PathMember::test_string("path".into(), false), + PathMember::test_string("some".into(), false, false), + PathMember::test_string("path".into(), false, false), ], })), }, @@ -80,9 +85,9 @@ impl Command for IntoCellPath { result: Some(Value::test_cell_path(CellPath { members: vec![ PathMember::test_int(5, false), - PathMember::test_string("c".into(), false), + PathMember::test_string("c".into(), false, false), PathMember::test_int(7, false), - PathMember::test_string("h".into(), false), + PathMember::test_string("h".into(), false, false), ], })), }, @@ -92,7 +97,7 @@ impl Command for IntoCellPath { result: Some(Value::test_cell_path(CellPath { members: vec![ PathMember::test_int(5, true), - PathMember::test_string("c".into(), false), + PathMember::test_string("c".into(), false, false), ], })), }, @@ -175,6 +180,12 @@ fn record_to_path_member( } }; + if let Some(insensitive) = record.get("insensitive") { + if insensitive.as_bool()? { + member.make_insensitive(); + } + }; + Ok(member) } @@ -196,7 +207,7 @@ fn value_to_path_member(val: &Value, span: Span) -> Result int_to_path_member(*val, val_span)?, - Value::String { val, .. } => PathMember::string(val.into(), false, val_span), + Value::String { val, .. } => PathMember::string(val.into(), false, false, val_span), Value::Record { val, .. } => record_to_path_member(val, val_span, span)?, other => { return Err(ShellError::CantConvert { diff --git a/crates/nu-command/src/conversions/split_cell_path.rs b/crates/nu-command/src/conversions/split_cell_path.rs index 4854bd9d03..2708a7d6ed 100644 --- a/crates/nu-command/src/conversions/split_cell_path.rs +++ b/crates/nu-command/src/conversions/split_cell_path.rs @@ -16,7 +16,12 @@ impl Command for SplitCellPath { ( Type::CellPath, Type::List(Box::new(Type::Record( - [("value".into(), Type::Any), ("optional".into(), Type::Bool)].into(), + [ + ("value".into(), Type::Any), + ("optional".into(), Type::Bool), + ("insensitive".into(), Type::Bool), + ] + .into(), ))), ), ]) @@ -114,19 +119,29 @@ fn split_cell_path(val: CellPath, span: Span) -> Result { struct PathMemberRecord { value: Value, optional: bool, + insensitive: bool, } impl PathMemberRecord { fn from_path_member(pm: PathMember) -> Self { - let (optional, internal_span) = match pm { - PathMember::String { optional, span, .. } - | PathMember::Int { optional, span, .. } => (optional, span), + let (optional, insensitive, internal_span) = match pm { + PathMember::String { + optional, + insensitive, + span, + .. + } => (optional, insensitive, span), + PathMember::Int { optional, span, .. } => (optional, false, span), }; let value = match pm { PathMember::String { val, .. } => Value::string(val, internal_span), PathMember::Int { val, .. } => Value::int(val as i64, internal_span), }; - Self { value, optional } + Self { + value, + optional, + insensitive, + } } } diff --git a/crates/nu-command/src/filters/empty.rs b/crates/nu-command/src/filters/empty.rs index b99289f6da..6758d1f269 100644 --- a/crates/nu-command/src/filters/empty.rs +++ b/crates/nu-command/src/filters/empty.rs @@ -15,7 +15,7 @@ pub fn empty( if !columns.is_empty() { for val in input { for column in &columns { - if !val.follow_cell_path(&column.members, false)?.is_nothing() { + if !val.follow_cell_path(&column.members)?.is_nothing() { return Ok(Value::bool(negate, head).into_pipeline_data()); } } diff --git a/crates/nu-protocol/src/ast/cell_path.rs b/crates/nu-protocol/src/ast/cell_path.rs index a1c66ddd7e..350474a53d 100644 --- a/crates/nu-protocol/src/ast/cell_path.rs +++ b/crates/nu-protocol/src/ast/cell_path.rs @@ -14,6 +14,8 @@ pub enum PathMember { /// If marked as optional don't throw an error if not found but perform default handling /// (e.g. return `Value::Nothing`) optional: bool, + /// If marked as insensitive, column lookup happens case insensitively + insensitive: bool, }, /// Accessing a member by index (i.e. row of a table or item in a list) Int { @@ -34,11 +36,12 @@ impl PathMember { } } - pub fn string(val: String, optional: bool, span: Span) -> Self { + pub fn string(val: String, optional: bool, insensitive: bool, span: Span) -> Self { PathMember::String { val, span, optional, + insensitive, } } @@ -50,10 +53,11 @@ impl PathMember { } } - pub fn test_string(val: String, optional: bool) -> Self { + pub fn test_string(val: String, optional: bool, insensitive: bool) -> Self { PathMember::String { val, optional, + insensitive, span: Span::test_data(), } } @@ -69,6 +73,16 @@ impl PathMember { } } + pub fn make_insensitive(&mut self) { + match self { + PathMember::String { + ref mut insensitive, + .. + } => *insensitive = true, + PathMember::Int { .. } => {} + } + } + pub fn span(&self) -> Span { match self { PathMember::String { span, .. } => *span, @@ -182,6 +196,12 @@ impl CellPath { } } + pub fn make_insensitive(&mut self) { + for member in &mut self.members { + member.make_insensitive(); + } + } + // 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(); @@ -213,14 +233,20 @@ impl Display for CellPath { let question_mark = if *optional { "?" } else { "" }; write!(f, ".{val}{question_mark}")? } - PathMember::String { val, optional, .. } => { + PathMember::String { + val, + optional, + insensitive, + .. + } => { let question_mark = if *optional { "?" } else { "" }; + let exclamation_mark = if *insensitive { "!" } else { "" }; let val = if needs_quoting(val) { &escape_quote_string(val) } else { val }; - write!(f, ".{val}{question_mark}")? + write!(f, ".{val}{exclamation_mark}{question_mark}")? } } } @@ -243,7 +269,11 @@ mod test { fn path_member_partial_ord() { assert_eq!( Some(Greater), - PathMember::test_int(5, true).partial_cmp(&PathMember::test_string("e".into(), true)) + PathMember::test_int(5, true).partial_cmp(&PathMember::test_string( + "e".into(), + true, + false + )) ); assert_eq!( @@ -258,14 +288,20 @@ mod test { assert_eq!( Some(Greater), - PathMember::test_string("e".into(), true) - .partial_cmp(&PathMember::test_string("e".into(), false)) + PathMember::test_string("e".into(), true, false).partial_cmp(&PathMember::test_string( + "e".into(), + false, + false + )) ); assert_eq!( Some(Greater), - PathMember::test_string("f".into(), true) - .partial_cmp(&PathMember::test_string("e".into(), true)) + PathMember::test_string("f".into(), true, false).partial_cmp(&PathMember::test_string( + "e".into(), + true, + false + )) ); } } From dec0f84623c8fb12cef1ab4fc7294e3caec3e8be Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Sun, 4 May 2025 10:14:28 +0300 Subject: [PATCH 02/10] refactor(cell-path): update constructors and call sites --- .../src/extra/strings/format/command.rs | 1 + crates/nu-command/src/filters/reject.rs | 1 + crates/nu-command/src/filters/select.rs | 1 + crates/nu-command/src/filters/sort.rs | 9 ++++++- .../tests/commands/database/into_sqlite.rs | 1 + crates/nu-command/tests/sort_utils.rs | 1 + crates/nu-protocol/src/value/from_value.rs | 1 + crates/nu-protocol/src/value/mod.rs | 27 ++++++++++--------- crates/nuon/src/lib.rs | 4 +-- 9 files changed, 31 insertions(+), 15 deletions(-) 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 e648c66298..57ca8402f2 100644 --- a/crates/nu-cmd-extra/src/extra/strings/format/command.rs +++ b/crates/nu-cmd-extra/src/extra/strings/format/command.rs @@ -251,6 +251,7 @@ fn format_record( val: path.to_string(), span: *span, optional: false, + insensitive: false, }) .collect(); diff --git a/crates/nu-command/src/filters/reject.rs b/crates/nu-command/src/filters/reject.rs index b728ca828e..ff9a7e60c8 100644 --- a/crates/nu-command/src/filters/reject.rs +++ b/crates/nu-command/src/filters/reject.rs @@ -63,6 +63,7 @@ impl Command for Reject { val: val.clone(), span: *col_span, optional: false, + insensitive: false, }], }; new_columns.push(cv.clone()); diff --git a/crates/nu-command/src/filters/select.rs b/crates/nu-command/src/filters/select.rs index b324a7e771..2c10dba856 100644 --- a/crates/nu-command/src/filters/select.rs +++ b/crates/nu-command/src/filters/select.rs @@ -67,6 +67,7 @@ produce a table, a list will produce a list, and a record will produce a record. val, span: col_span, optional: false, + insensitive: false, }], }; new_columns.push(cv); diff --git a/crates/nu-command/src/filters/sort.rs b/crates/nu-command/src/filters/sort.rs index 7eb37d5837..5988810583 100644 --- a/crates/nu-command/src/filters/sort.rs +++ b/crates/nu-command/src/filters/sort.rs @@ -164,7 +164,14 @@ impl Command for Sort { if let Type::Table(cols) = r#type { let columns: Vec = cols .iter() - .map(|col| vec![PathMember::string(col.0.clone(), false, Span::unknown())]) + .map(|col| { + vec![PathMember::string( + col.0.clone(), + false, + false, + Span::unknown(), + )] + }) .map(|members| CellPath { members }) .map(Comparator::CellPath) .collect(); diff --git a/crates/nu-command/tests/commands/database/into_sqlite.rs b/crates/nu-command/tests/commands/database/into_sqlite.rs index 93be9f27db..9176220cc8 100644 --- a/crates/nu-command/tests/commands/database/into_sqlite.rs +++ b/crates/nu-command/tests/commands/database/into_sqlite.rs @@ -327,6 +327,7 @@ fn into_sqlite_big_insert() { val: "somedate".into(), span: Span::unknown(), optional: false, + insensitive: false, }], Box::new(|dateval| { Value::string(dateval.coerce_string().unwrap(), dateval.span()) diff --git a/crates/nu-command/tests/sort_utils.rs b/crates/nu-command/tests/sort_utils.rs index be305588e2..7b92dc9127 100644 --- a/crates/nu-command/tests/sort_utils.rs +++ b/crates/nu-command/tests/sort_utils.rs @@ -526,6 +526,7 @@ fn test_sort_equivalent() { val: "value".to_string(), span: Span::test_data(), optional: false, + insensitive: false, }], }); diff --git a/crates/nu-protocol/src/value/from_value.rs b/crates/nu-protocol/src/value/from_value.rs index f617270d1d..22e9927a69 100644 --- a/crates/nu-protocol/src/value/from_value.rs +++ b/crates/nu-protocol/src/value/from_value.rs @@ -585,6 +585,7 @@ impl FromValue for CellPath { val, span, optional: false, + insensitive: false, }], }), Value::Int { val, .. } => { diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 7ff9218eea..357b5478ca 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -1373,6 +1373,7 @@ impl Value { val: col_name, span, optional, + insensitive, } => match self { Value::List { vals, .. } => { for val in vals.iter_mut() { @@ -1448,6 +1449,7 @@ impl Value { val: col_name, span, optional, + insensitive, } => match self { Value::List { vals, .. } => { for val in vals.iter_mut() { @@ -2092,6 +2094,7 @@ fn get_value_member<'a>( val: column_name, span: origin_span, optional, + insensitive, } => { let span = current.span(); match current { @@ -4100,10 +4103,10 @@ mod tests { assert_eq!( Value::with_data_at_cell_path( &[ - PathMember::test_string("a".to_string(), false), - PathMember::test_string("b".to_string(), false), - PathMember::test_string("c".to_string(), false), - PathMember::test_string("d".to_string(), false), + PathMember::test_string("a".to_string(), false, false), + PathMember::test_string("b".to_string(), false, false), + PathMember::test_string("c".to_string(), false, false), + PathMember::test_string("d".to_string(), false, false), ], value_to_insert, ), @@ -4147,13 +4150,13 @@ mod tests { assert_eq!( Value::with_data_at_cell_path( &[ - PathMember::test_string("a".to_string(), false), + PathMember::test_string("a".to_string(), false, false), PathMember::test_int(0, false), - PathMember::test_string("b".to_string(), false), + PathMember::test_string("b".to_string(), false, false), PathMember::test_int(0, false), - PathMember::test_string("c".to_string(), false), + PathMember::test_string("c".to_string(), false, false), PathMember::test_int(0, false), - PathMember::test_string("d".to_string(), false), + PathMember::test_string("d".to_string(), false, false), PathMember::test_int(0, false), ], value_to_insert.clone(), @@ -4183,9 +4186,9 @@ mod tests { let value_to_insert = Value::test_string("value"); let res = base_value.upsert_data_at_cell_path( &[ - PathMember::test_string("a".to_string(), false), + PathMember::test_string("a".to_string(), false, false), PathMember::test_int(0, false), - PathMember::test_string("b".to_string(), false), + PathMember::test_string("b".to_string(), false, false), PathMember::test_int(0, false), ], value_to_insert.clone(), @@ -4217,9 +4220,9 @@ mod tests { let value_to_insert = Value::test_string("value"); let res = base_value.insert_data_at_cell_path( &[ - PathMember::test_string("a".to_string(), false), + PathMember::test_string("a".to_string(), false, false), PathMember::test_int(0, false), - PathMember::test_string("b".to_string(), false), + PathMember::test_string("b".to_string(), false, false), PathMember::test_int(0, false), ], value_to_insert.clone(), diff --git a/crates/nuon/src/lib.rs b/crates/nuon/src/lib.rs index 99525959ab..11f812960f 100644 --- a/crates/nuon/src/lib.rs +++ b/crates/nuon/src/lib.rs @@ -396,8 +396,8 @@ mod tests { r#"$.foo.bar.0"#, Some(Value::test_cell_path(CellPath { members: vec![ - PathMember::string("foo".to_string(), false, Span::new(2, 5)), - PathMember::string("bar".to_string(), false, Span::new(6, 9)), + PathMember::string("foo".to_string(), false, false, Span::new(2, 5)), + PathMember::string("bar".to_string(), false, false, Span::new(6, 9)), PathMember::int(0, false, Span::new(10, 11)), ], })), From 662d7b566fe086fe54b659cd88be6d919319e26a Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Sun, 4 May 2025 10:18:19 +0300 Subject: [PATCH 03/10] fix(cell-path): quote cell-path members containing '?' and '!' --- crates/nu-utils/src/quoting.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/nu-utils/src/quoting.rs b/crates/nu-utils/src/quoting.rs index 35ca0711bd..0e713c9d80 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") }); From e10538bd8d84b79ceb95f9d6fb2b05a30e4e18e1 Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Sun, 4 May 2025 10:25:55 +0300 Subject: [PATCH 04/10] refactor(cell-path): update `Value::follow_cell_path` - remove `insensitive` parameter, case sensitivity is determined by the cell-path itself - update call sites --- .../src/completions/cell_path_completions.rs | 2 +- .../src/extra/strings/format/command.rs | 2 +- crates/nu-command/src/filters/get.rs | 13 +++---- crates/nu-command/src/filters/group_by.rs | 2 +- crates/nu-command/src/filters/insert.rs | 4 +-- crates/nu-command/src/filters/select.rs | 6 ++-- crates/nu-command/src/filters/update.rs | 4 +-- crates/nu-command/src/filters/upsert.rs | 4 +-- crates/nu-command/src/platform/input/list.rs | 2 +- crates/nu-command/src/sort_utils.rs | 4 +-- crates/nu-engine/src/eval.rs | 2 +- crates/nu-engine/src/eval_ir.rs | 4 +-- crates/nu-lsp/src/goto.rs | 2 +- crates/nu-lsp/src/hover.rs | 2 +- .../nu-protocol/src/pipeline/pipeline_data.rs | 7 ++-- crates/nu-protocol/src/value/mod.rs | 34 ++++++++----------- crates/nu_plugin_inc/src/inc.rs | 2 +- 17 files changed, 42 insertions(+), 54 deletions(-) diff --git a/crates/nu-cli/src/completions/cell_path_completions.rs b/crates/nu-cli/src/completions/cell_path_completions.rs index 34376b8ddb..420f0f0bba 100644 --- a/crates/nu-cli/src/completions/cell_path_completions.rs +++ b/crates/nu-cli/src/completions/cell_path_completions.rs @@ -104,7 +104,7 @@ pub(crate) fn eval_cell_path( eval_constant(working_set, head) }?; head_value - .follow_cell_path(path_members, false) + .follow_cell_path(path_members) .map(Cow::into_owned) } 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 57ca8402f2..905298b547 100644 --- a/crates/nu-cmd-extra/src/extra/strings/format/command.rs +++ b/crates/nu-cmd-extra/src/extra/strings/format/command.rs @@ -256,7 +256,7 @@ fn format_record( .collect(); let expanded_string = data_as_value - .follow_cell_path(&path_members, false)? + .follow_cell_path(&path_members)? .to_expanded_string(", ", config); output.push_str(expanded_string.as_str()) } diff --git a/crates/nu-command/src/filters/get.rs b/crates/nu-command/src/filters/get.rs index d681849448..44148302e7 100644 --- a/crates/nu-command/src/filters/get.rs +++ b/crates/nu-command/src/filters/get.rs @@ -181,7 +181,7 @@ fn action( } if rest.is_empty() { - follow_cell_path_into_stream(input, signals, cell_path.members, span, !sensitive) + follow_cell_path_into_stream(input, signals, cell_path.members, span) } else { let mut output = vec![]; @@ -190,11 +190,7 @@ fn action( let input = input.into_value(span)?; for path in paths { - output.push( - input - .follow_cell_path(&path.members, !sensitive)? - .into_owned(), - ); + output.push(input.follow_cell_path(&path.members)?.into_owned()); } Ok(output.into_iter().into_pipeline_data(span, signals)) @@ -213,7 +209,6 @@ pub fn follow_cell_path_into_stream( signals: Signals, cell_path: Vec, head: Span, - insensitive: bool, ) -> Result { // when given an integer/indexing, we fallback to // the default nushell indexing behaviour @@ -228,7 +223,7 @@ pub fn follow_cell_path_into_stream( let span = value.span(); value - .follow_cell_path(&cell_path, insensitive) + .follow_cell_path(&cell_path) .map(Cow::into_owned) .unwrap_or_else(|error| Value::error(error, span)) }) @@ -238,7 +233,7 @@ pub fn follow_cell_path_into_stream( } _ => data - .follow_cell_path(&cell_path, head, insensitive) + .follow_cell_path(&cell_path, head) .map(|x| x.into_pipeline_data()), } } diff --git a/crates/nu-command/src/filters/group_by.rs b/crates/nu-command/src/filters/group_by.rs index a8da1cedf3..7d732e25f3 100644 --- a/crates/nu-command/src/filters/group_by.rs +++ b/crates/nu-command/src/filters/group_by.rs @@ -322,7 +322,7 @@ fn group_cell_path( let mut groups = IndexMap::<_, Vec<_>>::new(); for value in values.into_iter() { - let key = value.follow_cell_path(&column_name.members, false)?; + let key = value.follow_cell_path(&column_name.members)?; if key.is_nothing() { continue; // likely the result of a failed optional access, ignore this value diff --git a/crates/nu-command/src/filters/insert.rs b/crates/nu-command/src/filters/insert.rs index e2801bf6ee..93d6d9da52 100644 --- a/crates/nu-command/src/filters/insert.rs +++ b/crates/nu-command/src/filters/insert.rs @@ -301,7 +301,7 @@ fn insert_value_by_closure( ) -> Result<(), ShellError> { let value_at_path = if first_path_member_int { value - .follow_cell_path(cell_path, false) + .follow_cell_path(cell_path) .map(Cow::into_owned) .unwrap_or(Value::nothing(span)) } else { @@ -321,7 +321,7 @@ fn insert_single_value_by_closure( ) -> Result<(), ShellError> { let value_at_path = if first_path_member_int { value - .follow_cell_path(cell_path, false) + .follow_cell_path(cell_path) .map(Cow::into_owned) .unwrap_or(Value::nothing(span)) } else { diff --git a/crates/nu-command/src/filters/select.rs b/crates/nu-command/src/filters/select.rs index 2c10dba856..1baf4081be 100644 --- a/crates/nu-command/src/filters/select.rs +++ b/crates/nu-command/src/filters/select.rs @@ -236,7 +236,7 @@ fn select( if !columns.is_empty() { let mut record = Record::new(); for path in &columns { - match input_val.follow_cell_path(&path.members, false) { + match input_val.follow_cell_path(&path.members) { Ok(fetcher) => { record.push(path.to_column_name(), fetcher.into_owned()); } @@ -259,7 +259,7 @@ fn select( let mut record = Record::new(); for cell_path in columns { - let result = v.follow_cell_path(&cell_path.members, false)?; + let result = v.follow_cell_path(&cell_path.members)?; record.push(cell_path.to_column_name(), result.into_owned()); } @@ -276,7 +276,7 @@ fn select( if !columns.is_empty() { let mut record = Record::new(); for path in &columns { - match x.follow_cell_path(&path.members, false) { + match x.follow_cell_path(&path.members) { Ok(value) => { record.push(path.to_column_name(), value.into_owned()); } diff --git a/crates/nu-command/src/filters/update.rs b/crates/nu-command/src/filters/update.rs index 3fe63ce676..b05c1dcf42 100644 --- a/crates/nu-command/src/filters/update.rs +++ b/crates/nu-command/src/filters/update.rs @@ -243,7 +243,7 @@ fn update_value_by_closure( cell_path: &[PathMember], first_path_member_int: bool, ) -> Result<(), ShellError> { - let value_at_path = value.follow_cell_path(cell_path, false)?; + let value_at_path = value.follow_cell_path(cell_path)?; let arg = if first_path_member_int { value_at_path.as_ref() @@ -266,7 +266,7 @@ fn update_single_value_by_closure( cell_path: &[PathMember], first_path_member_int: bool, ) -> Result<(), ShellError> { - let value_at_path = value.follow_cell_path(cell_path, false)?; + let value_at_path = value.follow_cell_path(cell_path)?; let arg = if first_path_member_int { value_at_path.as_ref() diff --git a/crates/nu-command/src/filters/upsert.rs b/crates/nu-command/src/filters/upsert.rs index 750263f402..d1575596a6 100644 --- a/crates/nu-command/src/filters/upsert.rs +++ b/crates/nu-command/src/filters/upsert.rs @@ -321,7 +321,7 @@ fn upsert_value_by_closure( cell_path: &[PathMember], first_path_member_int: bool, ) -> Result<(), ShellError> { - let value_at_path = value.follow_cell_path(cell_path, false); + let value_at_path = value.follow_cell_path(cell_path); let arg = if first_path_member_int { value_at_path @@ -352,7 +352,7 @@ fn upsert_single_value_by_closure( cell_path: &[PathMember], first_path_member_int: bool, ) -> Result<(), ShellError> { - let value_at_path = value.follow_cell_path(cell_path, false); + let value_at_path = value.follow_cell_path(cell_path); let arg = if first_path_member_int { value_at_path diff --git a/crates/nu-command/src/platform/input/list.rs b/crates/nu-command/src/platform/input/list.rs index df605f4a27..a6cb2fe68e 100644 --- a/crates/nu-command/src/platform/input/list.rs +++ b/crates/nu-command/src/platform/input/list.rs @@ -89,7 +89,7 @@ impl Command for InputList { .into_iter() .map(move |val| { let display_value = if let Some(ref cellpath) = display_path { - val.follow_cell_path(&cellpath.members, false)? + val.follow_cell_path(&cellpath.members)? .to_expanded_string(", ", &config) } else { val.to_expanded_string(", ", &config) diff --git a/crates/nu-command/src/sort_utils.rs b/crates/nu-command/src/sort_utils.rs index 0511e7cddf..9ad90e6eaa 100644 --- a/crates/nu-command/src/sort_utils.rs +++ b/crates/nu-command/src/sort_utils.rs @@ -239,8 +239,8 @@ pub fn compare_cell_path( insensitive: bool, natural: bool, ) -> Result { - let left = left.follow_cell_path(&cell_path.members, false)?; - let right = right.follow_cell_path(&cell_path.members, false)?; + let left = left.follow_cell_path(&cell_path.members)?; + let right = right.follow_cell_path(&cell_path.members)?; compare_values(&left, &right, insensitive, natural) } diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index c39dd0cf30..9b74626a01 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -268,7 +268,7 @@ pub fn eval_expression_with_input( // FIXME: protect this collect with ctrl-c input = eval_subexpression::(engine_state, stack, block, input)? .into_value(*span)? - .follow_cell_path(&full_cell_path.tail, false)? + .follow_cell_path(&full_cell_path.tail)? .into_owned() .into_pipeline_data() } else { diff --git a/crates/nu-engine/src/eval_ir.rs b/crates/nu-engine/src/eval_ir.rs index ffd0938079..01f0eead0a 100644 --- a/crates/nu-engine/src/eval_ir.rs +++ b/crates/nu-engine/src/eval_ir.rs @@ -678,7 +678,7 @@ fn eval_instruction( let data = ctx.take_reg(*src_dst); let path = ctx.take_reg(*path); if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path { - let value = data.follow_cell_path(&path.members, *span, true)?; + let value = data.follow_cell_path(&path.members, *span)?; ctx.put_reg(*src_dst, value.into_pipeline_data()); Ok(Continue) } else if let PipelineData::Value(Value::Error { error, .. }, _) = path { @@ -694,7 +694,7 @@ fn eval_instruction( let value = ctx.clone_reg_value(*src, *span)?; let path = ctx.take_reg(*path); if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path { - let value = value.follow_cell_path(&path.members, true)?; + let value = value.follow_cell_path(&path.members)?; ctx.put_reg(*dst, value.into_owned().into_pipeline_data()); Ok(Continue) } else if let PipelineData::Value(Value::Error { error, .. }, _) = path { diff --git a/crates/nu-lsp/src/goto.rs b/crates/nu-lsp/src/goto.rs index f8623ee0d7..436ae50300 100644 --- a/crates/nu-lsp/src/goto.rs +++ b/crates/nu-lsp/src/goto.rs @@ -64,7 +64,7 @@ impl LanguageServer { Some( var.const_val .as_ref() - .and_then(|val| val.follow_cell_path(cell_path, false).ok()) + .and_then(|val| val.follow_cell_path(cell_path).ok()) .map(|val| val.span()) .unwrap_or(var.declaration_span), ) diff --git a/crates/nu-lsp/src/hover.rs b/crates/nu-lsp/src/hover.rs index c75499f0c2..fe31809d74 100644 --- a/crates/nu-lsp/src/hover.rs +++ b/crates/nu-lsp/src/hover.rs @@ -161,7 +161,7 @@ impl LanguageServer { markdown_hover( var.const_val .as_ref() - .and_then(|val| val.follow_cell_path(&cell_path, false).ok()) + .and_then(|val| val.follow_cell_path(&cell_path).ok()) .map(|val| { let ty = val.get_type(); if let Ok(s) = val.coerce_str() { diff --git a/crates/nu-protocol/src/pipeline/pipeline_data.rs b/crates/nu-protocol/src/pipeline/pipeline_data.rs index a0ea603dc1..1216aa3a6a 100644 --- a/crates/nu-protocol/src/pipeline/pipeline_data.rs +++ b/crates/nu-protocol/src/pipeline/pipeline_data.rs @@ -411,16 +411,13 @@ impl PipelineData { self, cell_path: &[PathMember], head: Span, - insensitive: bool, ) -> Result { match self { // FIXME: there are probably better ways of doing this PipelineData::ListStream(stream, ..) => Value::list(stream.into_iter().collect(), head) - .follow_cell_path(cell_path, insensitive) - .map(Cow::into_owned), - PipelineData::Value(v, ..) => v - .follow_cell_path(cell_path, insensitive) + .follow_cell_path(cell_path) .map(Cow::into_owned), + PipelineData::Value(v, ..) => v.follow_cell_path(cell_path).map(Cow::into_owned), PipelineData::Empty => Err(ShellError::IncompatiblePathAccess { type_name: "empty pipeline".to_string(), span: head, diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 357b5478ca..7f1e3c2a57 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -1083,7 +1083,6 @@ impl Value { pub fn follow_cell_path<'out>( &'out self, cell_path: &[PathMember], - insensitive: bool, ) -> Result, ShellError> { enum MultiLife<'out, 'local, T> where @@ -1116,7 +1115,7 @@ impl Value { for member in cell_path { current = match current { - MultiLife::Out(current) => match get_value_member(current, member, insensitive)? { + MultiLife::Out(current) => match get_value_member(current, member)? { ControlFlow::Break(span) => return Ok(Cow::Owned(Value::nothing(span))), ControlFlow::Continue(x) => match x { Cow::Borrowed(x) => MultiLife::Out(x), @@ -1126,18 +1125,16 @@ impl Value { } }, }, - MultiLife::Local(current) => { - match get_value_member(current, member, insensitive)? { - ControlFlow::Break(span) => return Ok(Cow::Owned(Value::nothing(span))), - ControlFlow::Continue(x) => match x { - Cow::Borrowed(x) => MultiLife::Local(x), - Cow::Owned(x) => { - store = x; - MultiLife::Local(&store) - } - }, - } - } + MultiLife::Local(current) => match get_value_member(current, member)? { + ControlFlow::Break(span) => return Ok(Cow::Owned(Value::nothing(span))), + ControlFlow::Continue(x) => match x { + Cow::Borrowed(x) => MultiLife::Local(x), + Cow::Owned(x) => { + store = x; + MultiLife::Local(&store) + } + }, + }, }; } @@ -1166,7 +1163,7 @@ impl Value { cell_path: &[PathMember], callback: Box Value>, ) -> Result<(), ShellError> { - let new_val = callback(self.follow_cell_path(cell_path, false)?.as_ref()); + let new_val = callback(self.follow_cell_path(cell_path)?.as_ref()); match new_val { Value::Error { error, .. } => Err(*error), @@ -1266,7 +1263,7 @@ impl Value { cell_path: &[PathMember], callback: Box Value + 'a>, ) -> Result<(), ShellError> { - let new_val = callback(self.follow_cell_path(cell_path, false)?.as_ref()); + let new_val = callback(self.follow_cell_path(cell_path)?.as_ref()); match new_val { Value::Error { error, .. } => Err(*error), @@ -2007,7 +2004,6 @@ impl Value { fn get_value_member<'a>( current: &'a Value, member: &PathMember, - insensitive: bool, ) -> Result>, ShellError> { match member { PathMember::Int { @@ -2100,7 +2096,7 @@ fn get_value_member<'a>( match current { Value::Record { val, .. } => { if let Some(found) = val.iter().rev().find(|x| { - if insensitive { + if *insensitive { x.0.eq_ignore_case(column_name) } else { x.0 == column_name @@ -2134,7 +2130,7 @@ fn get_value_member<'a>( match val { Value::Record { val, .. } => { if let Some(found) = val.iter().rev().find(|x| { - if insensitive { + if *insensitive { x.0.eq_ignore_case(column_name) } else { x.0 == column_name diff --git a/crates/nu_plugin_inc/src/inc.rs b/crates/nu_plugin_inc/src/inc.rs index c479cd4457..f2a41b8a03 100644 --- a/crates/nu_plugin_inc/src/inc.rs +++ b/crates/nu_plugin_inc/src/inc.rs @@ -91,7 +91,7 @@ impl Inc { pub fn inc(&self, head: Span, value: &Value) -> Result { if let Some(cell_path) = &self.cell_path { - let cell_value = value.follow_cell_path(&cell_path.members, false)?; + let cell_value = value.follow_cell_path(&cell_path.members)?; let cell_value = self.inc_value(head, &cell_value)?; From e48ad0d531bed1cfcc970a90feb34ab9ca15275d Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Sun, 4 May 2025 10:35:22 +0300 Subject: [PATCH 05/10] fix(cell-path): special case `$env` --- crates/nu-engine/src/eval.rs | 7 +++++-- crates/nu-protocol/src/eval_base.rs | 12 ++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 9b74626a01..2ca15c1922 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -592,8 +592,11 @@ impl Eval for EvalRuntime { // Retrieve the updated environment value. lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?; - let value = - lhs.follow_cell_path(&[cell_path.tail[0].clone()], true)?; + let value = lhs.follow_cell_path(&[{ + let mut pm = cell_path.tail[0].clone(); + pm.make_insensitive(); + pm + }])?; // Reject attempts to set automatic environment variables. if is_automatic_env_var(&original_key) { diff --git a/crates/nu-protocol/src/eval_base.rs b/crates/nu-protocol/src/eval_base.rs index 65c9b5f82b..56cf2f4b8f 100644 --- a/crates/nu-protocol/src/eval_base.rs +++ b/crates/nu-protocol/src/eval_base.rs @@ -43,8 +43,16 @@ pub trait Eval { // Cell paths are usually case-sensitive, but we give $env // special treatment. - let insensitive = cell_path.head.expr == Expr::Var(ENV_VARIABLE_ID); - value.follow_cell_path(&cell_path.tail, insensitive).map(Cow::into_owned) + let tail = if cell_path.head.expr == Expr::Var(ENV_VARIABLE_ID) { + let mut tail = cell_path.tail.clone(); + if let Some(pm) = tail.first_mut() { + pm.make_insensitive(); + } + Cow::Owned(tail) + } else { + Cow::Borrowed(&cell_path.tail) + }; + value.follow_cell_path(&tail).map(Cow::into_owned) } Expr::DateTime(dt) => Ok(Value::date(*dt, expr_span)), Expr::List(list) => { From d411a1e9e6857be587743165704c4f06159c9818 Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Sun, 4 May 2025 10:38:00 +0300 Subject: [PATCH 06/10] fix(cell-path): incorrect serialization of empty cell-paths can round trip `$. | to nuon | from nuon` --- crates/nu-protocol/src/ast/cell_path.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/nu-protocol/src/ast/cell_path.rs b/crates/nu-protocol/src/ast/cell_path.rs index 350474a53d..7b28196e5c 100644 --- a/crates/nu-protocol/src/ast/cell_path.rs +++ b/crates/nu-protocol/src/ast/cell_path.rs @@ -250,6 +250,10 @@ impl Display for CellPath { } } } + // Empty cell-paths are `$.` not `$` + if self.members.is_empty() { + write!(f, ".")?; + } Ok(()) } } From e0812f6bc80160a0baad6d799ffc19a042071595 Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Sun, 4 May 2025 10:39:21 +0300 Subject: [PATCH 07/10] feat(cell-path): add insensitive column syntax and parsing --- crates/nu-parser/src/parser.rs | 187 +++++++++++++++++++++------------ 1 file changed, 120 insertions(+), 67 deletions(-) diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 06a38afe7e..14c39312f7 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1797,7 +1797,7 @@ pub fn parse_range(working_set: &mut StateWorkingSet, span: Span) -> Option Vec { enum TokenType { - Dot, // . - QuestionOrDot, // ? or . - PathMember, // an int or string, like `1` or `foo` + Dot, // . + DotOrSign, // . or ? or ! + DotOrExclamation, // . or ! + DotOrQuestion, // . or ? + PathMember, // an int or string, like `1` or `foo` + } + + enum ModifyMember { + No, + Optional, + Insensitive, + } + + impl TokenType { + fn expect(&mut self, byte: u8) -> Result { + match (&*self, byte) { + (Self::PathMember, _) => { + *self = Self::DotOrSign; + Ok(ModifyMember::No) + } + ( + Self::Dot | Self::DotOrSign | Self::DotOrExclamation | Self::DotOrQuestion, + b'.', + ) => { + *self = Self::PathMember; + Ok(ModifyMember::No) + } + (Self::DotOrSign, b'!') => { + *self = Self::DotOrQuestion; + Ok(ModifyMember::Insensitive) + } + (Self::DotOrSign, b'?') => { + *self = Self::DotOrExclamation; + Ok(ModifyMember::Optional) + } + (Self::DotOrSign, _) => Err(". or ! or ?"), + (Self::DotOrExclamation, b'!') => { + *self = Self::Dot; + Ok(ModifyMember::Insensitive) + } + (Self::DotOrExclamation, _) => Err(". or !"), + (Self::DotOrQuestion, b'?') => { + *self = Self::Dot; + Ok(ModifyMember::Optional) + } + (Self::DotOrQuestion, _) => Err(". or ?"), + (Self::Dot, _) => Err("."), + } + } } // Parsing a cell path is essentially a state machine, and this is the state @@ -2314,73 +2360,68 @@ pub fn parse_cell_path( for path_element in tokens { let bytes = working_set.get_span_contents(path_element.span); - match expected_token { - TokenType::Dot => { - if bytes.len() != 1 || bytes[0] != b'.' { - working_set.error(ParseError::Expected(".", path_element.span)); - return tail; - } - expected_token = TokenType::PathMember; - } - TokenType::QuestionOrDot => { - if bytes.len() == 1 && bytes[0] == b'.' { - expected_token = TokenType::PathMember; - } else if bytes.len() == 1 && bytes[0] == b'?' { - if let Some(last) = tail.last_mut() { - match last { - PathMember::String { - ref mut optional, .. - } => *optional = true, - PathMember::Int { - ref mut optional, .. - } => *optional = true, - } - } - expected_token = TokenType::Dot; - } else { - working_set.error(ParseError::Expected(". or ?", path_element.span)); - return tail; - } - } - TokenType::PathMember => { - let starting_error_count = working_set.parse_errors.len(); + // both parse_int and parse_string require their source to be non-empty + // all cases where `bytes` is empty is an error + let Some((&first, rest)) = bytes.split_first() else { + working_set.error(ParseError::Expected("string", path_element.span)); + return tail; + }; + let single_char = rest.is_empty(); - let expr = parse_int(working_set, path_element.span); - working_set.parse_errors.truncate(starting_error_count); + if let TokenType::PathMember = expected_token { + let starting_error_count = working_set.parse_errors.len(); - match expr { - Expression { - expr: Expr::Int(val), - span, - .. - } => tail.push(PathMember::Int { - val: val as usize, - span, - optional: false, - }), - _ => { - let result = parse_string(working_set, path_element.span); - match result { - Expression { - expr: Expr::String(string), + let expr = parse_int(working_set, path_element.span); + working_set.parse_errors.truncate(starting_error_count); + + match expr { + Expression { + expr: Expr::Int(val), + span, + .. + } => tail.push(PathMember::Int { + val: val as usize, + span, + optional: false, + }), + _ => { + let result = parse_string(working_set, path_element.span); + match result { + Expression { + expr: Expr::String(string), + span, + .. + } => { + tail.push(PathMember::String { + val: string, span, - .. - } => { - tail.push(PathMember::String { - val: string, - span, - optional: false, - }); - } - _ => { - working_set - .error(ParseError::Expected("string", path_element.span)); - return tail; - } + optional: false, + insensitive: false, + }); + } + _ => { + working_set.error(ParseError::Expected("string", path_element.span)); + return tail; } } } - expected_token = TokenType::QuestionOrDot; + } + expected_token = TokenType::DotOrSign; + } else { + match expected_token.expect(if single_char { first } else { b' ' }) { + Ok(modify) => { + if let Some(last) = tail.last_mut() { + match modify { + ModifyMember::No => {} + ModifyMember::Optional => last.make_optional(), + ModifyMember::Insensitive => last.make_insensitive(), + } + }; + } + Err(expected) => { + working_set.error(ParseError::Expected(expected, path_element.span)); + return tail; + } } } } @@ -2391,7 +2432,13 @@ pub fn parse_cell_path( pub fn parse_simple_cell_path(working_set: &mut StateWorkingSet, span: Span) -> Expression { let source = working_set.get_span_contents(span); - let (tokens, err) = lex(source, span.start, &[b'\n', b'\r'], &[b'.', b'?'], true); + let (tokens, err) = lex( + source, + span.start, + &[b'\n', b'\r'], + &[b'.', b'?', b'!'], + true, + ); if let Some(err) = err { working_set.error(err) } @@ -2417,7 +2464,13 @@ pub fn parse_full_cell_path( let full_cell_span = span; let source = working_set.get_span_contents(span); - let (tokens, err) = lex(source, span.start, &[b'\n', b'\r'], &[b'.', b'?'], true); + let (tokens, err) = lex( + source, + span.start, + &[b'\n', b'\r'], + &[b'.', b'?', b'!'], + true, + ); if let Some(err) = err { working_set.error(err) } From 05ff3c8e8e6c90b3eeedd1c7d489f8b3c06273cd Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Sun, 4 May 2025 10:57:12 +0300 Subject: [PATCH 08/10] refactor(get)!: case sensitive by default - remove `--sensitive(-s)` - add `--ignore-case(-I)` --- crates/nu-command/src/filters/get.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/crates/nu-command/src/filters/get.rs b/crates/nu-command/src/filters/get.rs index 44148302e7..78d01b1e74 100644 --- a/crates/nu-command/src/filters/get.rs +++ b/crates/nu-command/src/filters/get.rs @@ -46,9 +46,9 @@ If multiple cell paths are given, this will produce a list of values."# Some('i'), ) .switch( - "sensitive", - "get path in a case sensitive manner", - Some('s'), + "ignore-case", + "get path in a case insensitive manner (make all cell path members case insensitive)", + Some('I'), ) .allow_variants_without_examples(true) .category(Category::Filters) @@ -87,12 +87,12 @@ If multiple cell paths are given, this will produce a list of values."# }, Example { description: "Getting Path/PATH in a case insensitive way", - example: "$env | get paTH", + example: "$env | get --ignore-case paTH", result: None, }, Example { description: "Getting Path in a case sensitive way, won't work for 'PATH'", - example: "$env | get --sensitive Path", + example: "$env | get Path", result: None, }, ] @@ -111,14 +111,14 @@ If multiple cell paths are given, this will produce a list of values."# let cell_path: CellPath = call.req_const(working_set, 0)?; let rest: Vec = call.rest_const(working_set, 1)?; let ignore_errors = call.has_flag_const(working_set, "ignore-errors")?; - let sensitive = call.has_flag_const(working_set, "sensitive")?; + let ignore_case = call.has_flag_const(working_set, "ignore-case")?; let metadata = input.metadata(); action( input, cell_path, rest, ignore_errors, - sensitive, + ignore_case, working_set.permanent().signals().clone(), call.head, ) @@ -135,14 +135,14 @@ If multiple cell paths are given, this will produce a list of values."# let cell_path: CellPath = call.req(engine_state, stack, 0)?; let rest: Vec = call.rest(engine_state, stack, 1)?; let ignore_errors = call.has_flag(engine_state, stack, "ignore-errors")?; - let sensitive = call.has_flag(engine_state, stack, "sensitive")?; + let ignore_case = call.has_flag(engine_state, stack, "ignore-case")?; let metadata = input.metadata(); action( input, cell_path, rest, ignore_errors, - sensitive, + ignore_case, engine_state.signals().clone(), call.head, ) @@ -155,7 +155,7 @@ fn action( mut cell_path: CellPath, mut rest: Vec, ignore_errors: bool, - sensitive: bool, + ignore_case: bool, signals: Signals, span: Span, ) -> Result { @@ -166,6 +166,13 @@ fn action( } } + if ignore_case { + cell_path.make_insensitive(); + for path in &mut rest { + path.make_insensitive(); + } + } + match input { PipelineData::Empty => return Err(ShellError::PipelineEmpty { dst_span: span }), // Allow chaining of get -i From ea31bd3918609659076c8e3819b5b66e7746389c Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Sun, 4 May 2025 12:05:07 +0300 Subject: [PATCH 09/10] fix(cell-path): update tests and examples --- crates/nu-command/src/conversions/into/cell_path.rs | 3 ++- crates/nu-command/src/conversions/split_cell_path.rs | 9 ++++++++- tests/repl/test_table_operations.rs | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/nu-command/src/conversions/into/cell_path.rs b/crates/nu-command/src/conversions/into/cell_path.rs index 462d425e35..7f7e24ee99 100644 --- a/crates/nu-command/src/conversions/into/cell_path.rs +++ b/crates/nu-command/src/conversions/into/cell_path.rs @@ -93,11 +93,12 @@ impl Command for IntoCellPath { }, Example { description: "Convert table into cell path", - example: "[[value, optional]; [5 true] [c false]] | into cell-path", + example: "[[value, optional, insensitive]; [5 true false] [c false false] [d false true]] | into cell-path", result: Some(Value::test_cell_path(CellPath { members: vec![ PathMember::test_int(5, true), PathMember::test_string("c".into(), false, false), + PathMember::test_string("d".into(), false, true), ], })), }, diff --git a/crates/nu-command/src/conversions/split_cell_path.rs b/crates/nu-command/src/conversions/split_cell_path.rs index 2708a7d6ed..173190fb83 100644 --- a/crates/nu-command/src/conversions/split_cell_path.rs +++ b/crates/nu-command/src/conversions/split_cell_path.rs @@ -77,36 +77,43 @@ impl Command for SplitCellPath { Value::test_record(record! { "value" => Value::test_int(5), "optional" => Value::test_bool(true), + "insensitive" => Value::test_bool(false), }), Value::test_record(record! { "value" => Value::test_string("c"), "optional" => Value::test_bool(false), + "insensitive" => Value::test_bool(false), }), ])), }, Example { description: "Split a complex cell-path", - example: r#"$.a.b?.1."2"."c.d" | split cell-path"#, + example: r#"$.a!.b?.1."2"."c.d" | split cell-path"#, result: Some(Value::test_list(vec![ Value::test_record(record! { "value" => Value::test_string("a"), "optional" => Value::test_bool(false), + "insensitive" => Value::test_bool(true), }), Value::test_record(record! { "value" => Value::test_string("b"), "optional" => Value::test_bool(true), + "insensitive" => Value::test_bool(false), }), Value::test_record(record! { "value" => Value::test_int(1), "optional" => Value::test_bool(false), + "insensitive" => Value::test_bool(false), }), Value::test_record(record! { "value" => Value::test_string("2"), "optional" => Value::test_bool(false), + "insensitive" => Value::test_bool(false), }), Value::test_record(record! { "value" => Value::test_string("c.d"), "optional" => Value::test_bool(false), + "insensitive" => Value::test_bool(false), }), ])), }, diff --git a/tests/repl/test_table_operations.rs b/tests/repl/test_table_operations.rs index 5d61e0d902..0a975c2c1b 100644 --- a/tests/repl/test_table_operations.rs +++ b/tests/repl/test_table_operations.rs @@ -307,7 +307,7 @@ fn nullify_holes() -> TestResult { #[test] fn get_insensitive() -> TestResult { run_test( - r#"[[name, age]; [a, 1] [b, 2]] | get NAmE | select 0 | get 0"#, + r#"[[name, age]; [a, 1] [b, 2]] | get -I NAmE | select 0 | get 0"#, "a", ) } From e8b678352952c3c00b381c7b7c7ca397ef4414cc Mon Sep 17 00:00:00 2001 From: Bahex <17417311+Bahex@users.noreply.github.com> Date: Sun, 4 May 2025 16:28:52 +0300 Subject: [PATCH 10/10] feat(record): Add CasedRecord, Record wrapper that affects case sensitivity Make case sensitivity explicit in various places. --- crates/nu-protocol/src/value/mod.rs | 70 ++++++++++++++---------- crates/nu-protocol/src/value/record.rs | 75 ++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 27 deletions(-) diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 7f1e3c2a57..2c61afa514 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -31,7 +31,7 @@ use fancy_regex::Regex; use nu_utils::{ contains_emoji, locale::{get_system_locale_string, LOCALE_OVERRIDE_ENV_VAR}, - IgnoreCaseExt, SharedCow, + SharedCow, }; use serde::{Deserialize, Serialize}; use std::{ @@ -1182,6 +1182,7 @@ impl Value { PathMember::String { val: col_name, span, + insensitive, .. } => match self { Value::List { vals, .. } => { @@ -1189,7 +1190,9 @@ impl Value { match val { Value::Record { val: record, .. } => { let record = record.to_mut(); - if let Some(val) = record.get_mut(col_name) { + if let Some(val) = + record.cased_mut(*insensitive).get_mut(col_name) + { val.upsert_data_at_cell_path(path, new_val.clone())?; } else { let new_col = @@ -1210,7 +1213,7 @@ impl Value { } Value::Record { val: record, .. } => { let record = record.to_mut(); - if let Some(val) = record.get_mut(col_name) { + if let Some(val) = record.cased_mut(*insensitive).get_mut(col_name) { val.upsert_data_at_cell_path(path, new_val)?; } else { let new_col = Value::with_data_at_cell_path(path, new_val.clone())?; @@ -1282,6 +1285,7 @@ impl Value { PathMember::String { val: col_name, span, + insensitive, .. } => match self { Value::List { vals, .. } => { @@ -1289,7 +1293,9 @@ impl Value { let v_span = val.span(); match val { Value::Record { val: record, .. } => { - if let Some(val) = record.to_mut().get_mut(col_name) { + if let Some(val) = + record.to_mut().cased_mut(*insensitive).get_mut(col_name) + { val.update_data_at_cell_path(path, new_val.clone())?; } else { return Err(ShellError::CantFindColumn { @@ -1311,7 +1317,8 @@ impl Value { } } Value::Record { val: record, .. } => { - if let Some(val) = record.to_mut().get_mut(col_name) { + if let Some(val) = record.to_mut().cased_mut(*insensitive).get_mut(col_name) + { val.update_data_at_cell_path(path, new_val)?; } else { return Err(ShellError::CantFindColumn { @@ -1377,7 +1384,11 @@ impl Value { let v_span = val.span(); match val { Value::Record { val: record, .. } => { - if record.to_mut().remove(col_name).is_none() && !optional { + let value = record + .to_mut() + .cased_mut(*insensitive) + .remove(col_name); + if value.is_none() && !optional { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), span: Some(*span), @@ -1397,7 +1408,13 @@ impl Value { Ok(()) } Value::Record { val: record, .. } => { - if record.to_mut().remove(col_name).is_none() && !optional { + if record + .to_mut() + .cased_mut(*insensitive) + .remove(col_name) + .is_none() + && !optional + { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), span: Some(*span), @@ -1453,7 +1470,11 @@ impl Value { let v_span = val.span(); match val { Value::Record { val: record, .. } => { - if let Some(val) = record.to_mut().get_mut(col_name) { + let val = record + .to_mut() + .cased_mut(*insensitive) + .get_mut(col_name); + if let Some(val) = val { val.remove_data_at_cell_path(path)?; } else if !optional { return Err(ShellError::CantFindColumn { @@ -1475,7 +1496,9 @@ impl Value { Ok(()) } Value::Record { val: record, .. } => { - if let Some(val) = record.to_mut().get_mut(col_name) { + if let Some(val) = + record.to_mut().cased_mut(*insensitive).get_mut(col_name) + { val.remove_data_at_cell_path(path)?; } else if !optional { return Err(ShellError::CantFindColumn { @@ -1532,6 +1555,7 @@ impl Value { PathMember::String { val: col_name, span, + insensitive, .. } => match self { Value::List { vals, .. } => { @@ -1540,7 +1564,9 @@ impl Value { match val { Value::Record { val: record, .. } => { let record = record.to_mut(); - if let Some(val) = record.get_mut(col_name) { + if let Some(val) = + record.cased_mut(*insensitive).get_mut(col_name) + { if path.is_empty() { return Err(ShellError::ColumnAlreadyExists { col_name: col_name.clone(), @@ -1574,7 +1600,7 @@ impl Value { } Value::Record { val: record, .. } => { let record = record.to_mut(); - if let Some(val) = record.get_mut(col_name) { + if let Some(val) = record.cased_mut(*insensitive).get_mut(col_name) { if path.is_empty() { return Err(ShellError::ColumnAlreadyExists { col_name: col_name.clone(), @@ -2095,14 +2121,9 @@ fn get_value_member<'a>( let span = current.span(); match current { Value::Record { val, .. } => { - if let Some(found) = val.iter().rev().find(|x| { - if *insensitive { - x.0.eq_ignore_case(column_name) - } else { - x.0 == column_name - } - }) { - Ok(ControlFlow::Continue(Cow::Borrowed(found.1))) + let found = val.cased(*insensitive).get(column_name); + if let Some(found) = found { + Ok(ControlFlow::Continue(Cow::Borrowed(found))) } else if *optional { Ok(ControlFlow::Break(*origin_span)) // short-circuit @@ -2129,14 +2150,9 @@ fn get_value_member<'a>( let val_span = val.span(); match val { Value::Record { val, .. } => { - if let Some(found) = val.iter().rev().find(|x| { - if *insensitive { - x.0.eq_ignore_case(column_name) - } else { - x.0 == column_name - } - }) { - Ok(found.1.clone()) + let found = val.cased(*insensitive).get(column_name); + if let Some(found) = found { + Ok(found.clone()) } else if *optional { Ok(Value::nothing(*origin_span)) } else if let Some(suggestion) = diff --git a/crates/nu-protocol/src/value/record.rs b/crates/nu-protocol/src/value/record.rs index 2b667a7cf0..db86786a9a 100644 --- a/crates/nu-protocol/src/value/record.rs +++ b/crates/nu-protocol/src/value/record.rs @@ -3,6 +3,7 @@ use std::{iter::FusedIterator, ops::RangeBounds}; use crate::{ShellError, Span, Value}; +use nu_utils::IgnoreCaseExt; use serde::{de::Visitor, ser::SerializeMap, Deserialize, Serialize}; #[derive(Debug, Clone, Default)] @@ -10,6 +11,60 @@ pub struct Record { inner: Vec<(String, Value)>, } +pub struct CasedRecord { + record: R, + insensitive: bool, +} + +impl CasedRecord { + fn cmp(&self, lhs: &str, rhs: &str) -> bool { + if self.insensitive { + lhs.eq_ignore_case(rhs) + } else { + lhs == rhs + } + } +} + +impl<'a> CasedRecord<&'a Record> { + pub fn contains(&self, col: impl AsRef) -> bool { + self.record.columns().any(|k| self.cmp(k, col.as_ref())) + } + + pub fn index_of(&self, col: impl AsRef) -> Option { + self.record + .columns() + .rposition(|k| self.cmp(k, col.as_ref())) + } + + pub fn get(self, col: impl AsRef) -> Option<&'a Value> { + let idx = self.index_of(col)?; + let (_, value) = self.record.get_index(idx)?; + Some(value) + } +} + +impl<'a> CasedRecord<&'a mut Record> { + fn shared(&'a self) -> CasedRecord<&'a Record> { + CasedRecord { + record: &*self.record, + insensitive: self.insensitive, + } + } + + pub fn get_mut(self, col: impl AsRef) -> Option<&'a mut Value> { + let idx = self.shared().index_of(col)?; + let (_, value) = self.record.get_index_mut(idx)?; + Some(value) + } + + pub fn remove(&mut self, col: impl AsRef) -> Option { + let idx = self.shared().index_of(col)?; + let (_, val) = self.record.inner.remove(idx); + Some(val) + } +} + impl Record { pub fn new() -> Self { Self::default() @@ -21,6 +76,20 @@ impl Record { } } + pub fn cased(&self, insensitive: bool) -> CasedRecord<&Record> { + CasedRecord { + record: self, + insensitive, + } + } + + pub fn cased_mut(&mut self, insensitive: bool) -> CasedRecord<&mut Record> { + CasedRecord { + record: self, + insensitive, + } + } + /// Create a [`Record`] from a `Vec` of columns and a `Vec` of [`Value`]s /// /// Returns an error if `cols` and `vals` have different lengths. @@ -108,6 +177,12 @@ impl Record { self.inner.get(idx).map(|(col, val): &(_, _)| (col, val)) } + pub fn get_index_mut(&mut self, idx: usize) -> Option<(&mut String, &mut Value)> { + self.inner + .get_mut(idx) + .map(|(col, val): &mut (_, _)| (col, val)) + } + /// Remove single value by key /// /// Returns `None` if key not found