diff --git a/crates/nu-cli/src/completions/cell_path_completions.rs b/crates/nu-cli/src/completions/cell_path_completions.rs index 8c09929bce..51ca0b5409 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 9eb2324630..9e3adaf3cf 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,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::{Config, ListStream, ast::PathMember, engine::StateWorkingSet}; +use nu_protocol::{Config, ListStream, ast::PathMember, casing::Casing, engine::StateWorkingSet}; #[derive(Clone)] pub struct FormatPattern; @@ -251,11 +251,12 @@ fn format_record( val: path.to_string(), span: *span, optional: false, + casing: Casing::Sensitive, }) .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/conversions/into/cell_path.rs b/crates/nu-command/src/conversions/into/cell_path.rs index d7ff08d3eb..3f99b73e04 100644 --- a/crates/nu-command/src/conversions/into/cell_path.rs +++ b/crates/nu-command/src/conversions/into/cell_path.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::ast::PathMember; +use nu_protocol::{ast::PathMember, casing::Casing}; #[derive(Clone)] pub struct IntoCellPath; @@ -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, Casing::Sensitive), + PathMember::test_string("path".into(), false, Casing::Sensitive), ], })), }, @@ -80,19 +85,20 @@ 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, Casing::Sensitive), PathMember::test_int(7, false), - PathMember::test_string("h".into(), false), + PathMember::test_string("h".into(), false, Casing::Sensitive), ], })), }, 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), + PathMember::test_string("c".into(), false, Casing::Sensitive), + PathMember::test_string("d".into(), false, Casing::Insensitive), ], })), }, @@ -175,6 +181,12 @@ fn record_to_path_member( } }; + if let Some(insensitive) = record.get("insensitive") { + if insensitive.as_bool()? { + member.make_insensitive(); + } + }; + Ok(member) } @@ -196,7 +208,9 @@ 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, Casing::Sensitive, 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 308df9eb19..efeedafc04 100644 --- a/crates/nu-command/src/conversions/split_cell_path.rs +++ b/crates/nu-command/src/conversions/split_cell_path.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::{IntoValue, ast::PathMember}; +use nu_protocol::{IntoValue, ast::PathMember, casing::Casing}; #[derive(Clone)] pub struct SplitCellPath; @@ -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(), ))), ), ]) @@ -72,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), }), ])), }, @@ -114,19 +126,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, + casing, + span, + .. + } => (optional, casing == Casing::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-command/src/filters/get.rs b/crates/nu-command/src/filters/get.rs index 436435478a..bece5a9231 100644 --- a/crates/nu-command/src/filters/get.rs +++ b/crates/nu-command/src/filters/get.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use nu_engine::command_prelude::*; -use nu_protocol::{Signals, ast::PathMember}; +use nu_protocol::{Signals, ast::PathMember, report_shell_warning}; #[derive(Clone)] pub struct Get; @@ -47,7 +47,7 @@ If multiple cell paths are given, this will produce a list of values."# ) .switch( "sensitive", - "get path in a case sensitive manner", + "get path in a case sensitive manner (deprecated)", Some('s'), ) .allow_variants_without_examples(true) @@ -86,12 +86,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 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, }, ] @@ -110,14 +110,12 @@ 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 metadata = input.metadata(); action( input, cell_path, rest, ignore_errors, - sensitive, working_set.permanent().signals().clone(), call.head, ) @@ -134,14 +132,24 @@ 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 sensitive_span = call.get_flag_span(stack, "sensitive"); let metadata = input.metadata(); + if let Some(span) = sensitive_span { + report_shell_warning( + engine_state, + &ShellError::Deprecated { + deprecated: "sensitive flag", + suggestion: "", + span, + help: Some("cell-paths are case-sensitive by default"), + }, + ); + } action( input, cell_path, rest, ignore_errors, - sensitive, engine_state.signals().clone(), call.head, ) @@ -154,7 +162,6 @@ fn action( mut cell_path: CellPath, mut rest: Vec, ignore_errors: bool, - sensitive: bool, signals: Signals, span: Span, ) -> Result { @@ -180,7 +187,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![]; @@ -189,11 +196,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)) @@ -212,7 +215,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 @@ -227,7 +229,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)) }) @@ -237,7 +239,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 7b9e5f85ef..0f666d7a19 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 d9a1b419a2..adfa1a2137 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/reject.rs b/crates/nu-command/src/filters/reject.rs index 4d930a7ecb..8672242b83 100644 --- a/crates/nu-command/src/filters/reject.rs +++ b/crates/nu-command/src/filters/reject.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::ast::PathMember; +use nu_protocol::{ast::PathMember, casing::Casing}; use std::{cmp::Reverse, collections::HashSet}; #[derive(Clone)] @@ -63,6 +63,7 @@ impl Command for Reject { val: val.clone(), span: *col_span, optional: false, + casing: Casing::Sensitive, }], }; new_columns.push(cv.clone()); diff --git a/crates/nu-command/src/filters/select.rs b/crates/nu-command/src/filters/select.rs index 4d5e2b936c..6055b38eea 100644 --- a/crates/nu-command/src/filters/select.rs +++ b/crates/nu-command/src/filters/select.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::{PipelineIterator, ast::PathMember}; +use nu_protocol::{PipelineIterator, ast::PathMember, casing::Casing}; use std::collections::BTreeSet; #[derive(Clone)] @@ -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, + casing: Casing::Sensitive, }], }; new_columns.push(cv); @@ -233,7 +234,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()); } @@ -256,7 +257,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()); } @@ -273,7 +274,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/sort.rs b/crates/nu-command/src/filters/sort.rs index fb3547aacb..3b314c1d69 100644 --- a/crates/nu-command/src/filters/sort.rs +++ b/crates/nu-command/src/filters/sort.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::ast::PathMember; +use nu_protocol::{ast::PathMember, casing::Casing}; use crate::Comparator; @@ -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, + Casing::Sensitive, + Span::unknown(), + )] + }) .map(|members| CellPath { members }) .map(Comparator::CellPath) .collect(); diff --git a/crates/nu-command/src/filters/update.rs b/crates/nu-command/src/filters/update.rs index 422a99bca4..947ea30ff2 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 1e56053979..8e8ae10e28 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 0c48f06659..a2557ef986 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 9052e1fd84..4bf9820467 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-command/tests/commands/database/into_sqlite.rs b/crates/nu-command/tests/commands/database/into_sqlite.rs index 4f2017045a..7065e0811c 100644 --- a/crates/nu-command/tests/commands/database/into_sqlite.rs +++ b/crates/nu-command/tests/commands/database/into_sqlite.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, FixedOffset}; use nu_path::AbsolutePathBuf; -use nu_protocol::{Span, Value, ast::PathMember, engine::EngineState, record}; +use nu_protocol::{Span, Value, ast::PathMember, casing::Casing, engine::EngineState, record}; use nu_test_support::{ fs::{Stub, line_ending}, nu, pipeline, @@ -327,6 +327,7 @@ fn into_sqlite_big_insert() { val: "somedate".into(), span: Span::unknown(), optional: false, + casing: Casing::Sensitive, }], 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 60e2ac6769..c7043a2968 100644 --- a/crates/nu-command/tests/sort_utils.rs +++ b/crates/nu-command/tests/sort_utils.rs @@ -2,6 +2,7 @@ use nu_command::{Comparator, sort, sort_by, sort_record}; use nu_protocol::{ Record, Span, Value, ast::{CellPath, PathMember}, + casing::Casing, record, }; @@ -527,6 +528,7 @@ fn test_sort_equivalent() { val: "value".to_string(), span: Span::test_data(), optional: false, + casing: Casing::Sensitive, }], }); diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 392042f384..7d54320338 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 { @@ -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-engine/src/eval_ir.rs b/crates/nu-engine/src/eval_ir.rs index 4d129d3542..8b04f1ab00 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 45419e927f..abf651c875 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 764985d87a..af1691d558 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-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 0be38f800c..49fcdfb445 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -15,7 +15,7 @@ use nu_engine::DIR_VAR_PARSER_INFO; use nu_protocol::{ BlockId, DeclId, DidYouMean, ENV_VARIABLE_ID, FilesizeUnit, Flag, IN_VARIABLE_ID, ParseError, PositionalArg, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, VarId, ast::*, - engine::StateWorkingSet, eval_const::eval_constant, + casing::Casing, engine::StateWorkingSet, eval_const::eval_constant, }; use std::{ collections::{HashMap, HashSet}, @@ -1799,7 +1799,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 @@ -2334,69 +2380,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 { optional, .. } => *optional = true, - PathMember::Int { 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, + casing: Casing::Sensitive, + }); + } + _ => { + 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; + } } } } @@ -2407,7 +2452,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) } @@ -2433,7 +2484,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) } diff --git a/crates/nu-protocol/src/ast/cell_path.rs b/crates/nu-protocol/src/ast/cell_path.rs index a050468d6c..41e7fc9c9d 100644 --- a/crates/nu-protocol/src/ast/cell_path.rs +++ b/crates/nu-protocol/src/ast/cell_path.rs @@ -1,5 +1,5 @@ use super::Expression; -use crate::Span; +use crate::{Span, casing::Casing}; use nu_utils::{escape_quote_string, needs_quoting}; use serde::{Deserialize, Serialize}; use std::{cmp::Ordering, fmt::Display}; @@ -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, + /// Affects column lookup + casing: Casing, }, /// 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, casing: Casing, span: Span) -> Self { PathMember::String { val, span, optional, + casing, } } @@ -50,10 +53,11 @@ impl PathMember { } } - pub fn test_string(val: String, optional: bool) -> Self { + pub fn test_string(val: String, optional: bool, casing: Casing) -> Self { PathMember::String { val, optional, + casing, span: Span::test_data(), } } @@ -65,6 +69,13 @@ impl PathMember { } } + pub fn make_insensitive(&mut self) { + match self { + PathMember::String { casing, .. } => *casing = Casing::Insensitive, + PathMember::Int { .. } => {} + } + } + pub fn span(&self) -> Span { match self { PathMember::String { span, .. } => *span, @@ -178,6 +189,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(); @@ -209,17 +226,31 @@ impl Display for CellPath { let question_mark = if *optional { "?" } else { "" }; write!(f, ".{val}{question_mark}")? } - PathMember::String { val, optional, .. } => { + PathMember::String { + val, + optional, + casing, + .. + } => { let question_mark = if *optional { "?" } else { "" }; + let exclamation_mark = if *casing == Casing::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}")? } } } + // Empty cell-paths are `$.` not `$` + if self.members.is_empty() { + write!(f, ".")?; + } Ok(()) } } @@ -239,7 +270,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, + Casing::Sensitive + )) ); assert_eq!( @@ -254,14 +289,16 @@ 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, Casing::Sensitive).partial_cmp( + &PathMember::test_string("e".into(), false, Casing::Sensitive) + ) ); assert_eq!( Some(Greater), - PathMember::test_string("f".into(), true) - .partial_cmp(&PathMember::test_string("e".into(), true)) + PathMember::test_string("f".into(), true, Casing::Sensitive).partial_cmp( + &PathMember::test_string("e".into(), true, Casing::Sensitive) + ) ); } } diff --git a/crates/nu-protocol/src/casing.rs b/crates/nu-protocol/src/casing.rs new file mode 100644 index 0000000000..1fed95a81b --- /dev/null +++ b/crates/nu-protocol/src/casing.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] +pub enum Casing { + #[default] + Sensitive, + Insensitive, +} diff --git a/crates/nu-protocol/src/eval_base.rs b/crates/nu-protocol/src/eval_base.rs index 574723d912..e642ddbf29 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) => { diff --git a/crates/nu-protocol/src/lib.rs b/crates/nu-protocol/src/lib.rs index 2708384bbd..f1d58c272d 100644 --- a/crates/nu-protocol/src/lib.rs +++ b/crates/nu-protocol/src/lib.rs @@ -2,6 +2,7 @@ #![doc = include_str!("../README.md")] mod alias; pub mod ast; +pub mod casing; pub mod config; pub mod debugger; mod did_you_mean; diff --git a/crates/nu-protocol/src/pipeline/pipeline_data.rs b/crates/nu-protocol/src/pipeline/pipeline_data.rs index 0cb4b6f6a4..a79afecc1a 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/from_value.rs b/crates/nu-protocol/src/value/from_value.rs index c9f71a782d..fa4ee132bf 100644 --- a/crates/nu-protocol/src/value/from_value.rs +++ b/crates/nu-protocol/src/value/from_value.rs @@ -1,6 +1,7 @@ use crate::{ NuGlob, Range, Record, ShellError, Span, Spanned, Type, Value, ast::{CellPath, PathMember}, + casing::Casing, engine::Closure, }; use chrono::{DateTime, FixedOffset}; @@ -585,6 +586,7 @@ impl FromValue for CellPath { val, span, optional: false, + casing: Casing::Sensitive, }], }), Value::Int { val, .. } => { diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 9bfe568a6b..cfd5adf72f 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -29,7 +29,7 @@ use chrono::{DateTime, Datelike, Duration, FixedOffset, Local, Locale, TimeZone} use chrono_humanize::HumanTime; use fancy_regex::Regex; use nu_utils::{ - IgnoreCaseExt, SharedCow, contains_emoji, + SharedCow, contains_emoji, locale::{LOCALE_OVERRIDE_ENV_VAR, get_system_locale_string}, }; use serde::{Deserialize, Serialize}; @@ -1082,7 +1082,6 @@ impl Value { pub fn follow_cell_path<'out>( &'out self, cell_path: &[PathMember], - insensitive: bool, ) -> Result, ShellError> { enum MultiLife<'out, 'local, T> where @@ -1115,7 +1114,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), @@ -1125,18 +1124,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) + } + }, + }, }; } @@ -1165,7 +1162,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), @@ -1184,6 +1181,7 @@ impl Value { PathMember::String { val: col_name, span, + casing, .. } => match self { Value::List { vals, .. } => { @@ -1191,7 +1189,7 @@ 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(*casing).get_mut(col_name) { val.upsert_data_at_cell_path(path, new_val.clone())?; } else { let new_col = @@ -1212,7 +1210,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(*casing).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())?; @@ -1265,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), @@ -1284,6 +1282,7 @@ impl Value { PathMember::String { val: col_name, span, + casing, .. } => match self { Value::List { vals, .. } => { @@ -1291,7 +1290,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(*casing).get_mut(col_name) + { val.update_data_at_cell_path(path, new_val.clone())?; } else { return Err(ShellError::CantFindColumn { @@ -1313,7 +1314,7 @@ 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(*casing).get_mut(col_name) { val.update_data_at_cell_path(path, new_val)?; } else { return Err(ShellError::CantFindColumn { @@ -1372,13 +1373,16 @@ impl Value { val: col_name, span, optional, + casing, } => match self { Value::List { vals, .. } => { for val in vals.iter_mut() { 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(*casing).remove(col_name); + if value.is_none() && !optional { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), span: Some(*span), @@ -1398,7 +1402,13 @@ impl Value { Ok(()) } Value::Record { val: record, .. } => { - if record.to_mut().remove(col_name).is_none() && !optional { + if record + .to_mut() + .cased_mut(*casing) + .remove(col_name) + .is_none() + && !optional + { return Err(ShellError::CantFindColumn { col_name: col_name.clone(), span: Some(*span), @@ -1447,13 +1457,16 @@ impl Value { val: col_name, span, optional, + casing, } => match self { Value::List { vals, .. } => { for val in vals.iter_mut() { 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(*casing).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 +1488,8 @@ 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(*casing).get_mut(col_name) + { val.remove_data_at_cell_path(path)?; } else if !optional { return Err(ShellError::CantFindColumn { @@ -1532,6 +1546,7 @@ impl Value { PathMember::String { val: col_name, span, + casing, .. } => match self { Value::List { vals, .. } => { @@ -1540,7 +1555,7 @@ 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(*casing).get_mut(col_name) { if path.is_empty() { return Err(ShellError::ColumnAlreadyExists { col_name: col_name.clone(), @@ -1574,7 +1589,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(*casing).get_mut(col_name) { if path.is_empty() { return Err(ShellError::ColumnAlreadyExists { col_name: col_name.clone(), @@ -2004,7 +2019,6 @@ impl Value { fn get_value_member<'a>( current: &'a Value, member: &PathMember, - insensitive: bool, ) -> Result>, ShellError> { match member { PathMember::Int { @@ -2091,18 +2105,14 @@ fn get_value_member<'a>( val: column_name, span: origin_span, optional, + casing, } => { 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(*casing).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 +2139,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(*casing).get(column_name); + if let Some(found) = found { + Ok(found.clone()) } else if *optional { Ok(Value::nothing(*origin_span)) } else if let Some(suggestion) = @@ -4089,6 +4094,8 @@ mod tests { use crate::record; mod at_cell_path { + use crate::casing::Casing; + use crate::{IntoValue, Span}; use super::super::PathMember; @@ -4101,10 +4108,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, Casing::Sensitive), + PathMember::test_string("b".to_string(), false, Casing::Sensitive), + PathMember::test_string("c".to_string(), false, Casing::Sensitive), + PathMember::test_string("d".to_string(), false, Casing::Sensitive), ], value_to_insert, ), @@ -4148,13 +4155,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, Casing::Sensitive), PathMember::test_int(0, false), - PathMember::test_string("b".to_string(), false), + PathMember::test_string("b".to_string(), false, Casing::Sensitive), PathMember::test_int(0, false), - PathMember::test_string("c".to_string(), false), + PathMember::test_string("c".to_string(), false, Casing::Sensitive), PathMember::test_int(0, false), - PathMember::test_string("d".to_string(), false), + PathMember::test_string("d".to_string(), false, Casing::Sensitive), PathMember::test_int(0, false), ], value_to_insert.clone(), @@ -4184,9 +4191,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, Casing::Sensitive), PathMember::test_int(0, false), - PathMember::test_string("b".to_string(), false), + PathMember::test_string("b".to_string(), false, Casing::Sensitive), PathMember::test_int(0, false), ], value_to_insert.clone(), @@ -4218,9 +4225,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, Casing::Sensitive), PathMember::test_int(0, false), - PathMember::test_string("b".to_string(), false), + PathMember::test_string("b".to_string(), false, Casing::Sensitive), PathMember::test_int(0, false), ], value_to_insert.clone(), diff --git a/crates/nu-protocol/src/value/record.rs b/crates/nu-protocol/src/value/record.rs index 212e598ce6..c57f295904 100644 --- a/crates/nu-protocol/src/value/record.rs +++ b/crates/nu-protocol/src/value/record.rs @@ -1,8 +1,9 @@ //! Our insertion ordered map-type [`Record`] use std::{iter::FusedIterator, ops::RangeBounds}; -use crate::{ShellError, Span, Value}; +use crate::{ShellError, Span, Value, casing::Casing}; +use nu_utils::IgnoreCaseExt; use serde::{Deserialize, Serialize, de::Visitor, ser::SerializeMap}; #[derive(Debug, Clone, Default)] @@ -10,6 +11,62 @@ pub struct Record { inner: Vec<(String, Value)>, } +/// A wrapper around [`Record`] that affects whether key comparisons are case sensitive or not. +/// +/// Implements commonly used methods of [`Record`]. +pub struct CasedRecord { + record: R, + casing: Casing, +} + +impl CasedRecord { + fn cmp(&self, lhs: &str, rhs: &str) -> bool { + match self.casing { + Casing::Sensitive => lhs == rhs, + Casing::Insensitive => lhs.eq_ignore_case(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, + casing: self.casing, + } + } + + 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 +78,20 @@ impl Record { } } + pub fn cased(&self, casing: Casing) -> CasedRecord<&Record> { + CasedRecord { + record: self, + casing, + } + } + + pub fn cased_mut(&mut self, casing: Casing) -> CasedRecord<&mut Record> { + CasedRecord { + record: self, + casing, + } + } + /// 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 +179,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 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") }); diff --git a/crates/nu_plugin_inc/src/inc.rs b/crates/nu_plugin_inc/src/inc.rs index 78f02431ad..d9bf4a46ba 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)?; diff --git a/crates/nuon/src/lib.rs b/crates/nuon/src/lib.rs index c044bafa8d..f3dd13929d 100644 --- a/crates/nuon/src/lib.rs +++ b/crates/nuon/src/lib.rs @@ -12,6 +12,7 @@ mod tests { use nu_protocol::{ BlockId, IntRange, Range, Span, Value, ast::{CellPath, PathMember, RangeInclusion}, + casing::Casing, engine::{Closure, EngineState}, record, }; @@ -401,8 +402,18 @@ 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, + Casing::Sensitive, + Span::new(2, 5), + ), + PathMember::string( + "bar".to_string(), + false, + Casing::Sensitive, + Span::new(6, 9), + ), PathMember::int(0, false, Span::new(10, 11)), ], })), diff --git a/tests/repl/test_table_operations.rs b/tests/repl/test_table_operations.rs index 6e32982929..737f07dc6d 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 NAmE! | select 0 | get 0"#, "a", ) }