mirror of
https://github.com/nushell/nushell.git
synced 2025-08-03 09:19:44 +02:00
Related: - #15683 - #14551 - #849 - #12701 - #11527 # Description Currently various commands have differing behavior regarding cell-paths ```nushell {a: 1, A: 2} | get a A # => ╭───┬───╮ # => │ 0 │ 2 │ # => │ 1 │ 2 │ # => ╰───┴───╯ {a: 1, A: 2} | select a A # => ╭───┬───╮ # => │ a │ 1 │ # => │ A │ 2 │ # => ╰───┴───╯ {A: 1} | update a 2 # => Error: nu:🐚:column_not_found # => # => × Cannot find column 'a' # => ╭─[entry #62:1:1] # => 1 │ {A: 1} | update a 2 # => · ───┬── ┬ # => · │ ╰── cannot find column 'a' # => · ╰── value originates here # => ╰──── ``` Proposal: making cell-path access case-sensitive by default and adding new syntax for case-insensitive parts, similar to optional (?) parts. ```nushell {FOO: BAR}.foo # => Error: nu:🐚:name_not_found # => # => × Name not found # => ╭─[entry #60:1:21] # => 1 │ {FOO: BAR}.foo # => · ─┬─ # => · ╰── did you mean 'FOO'? # => ╰──── {FOO: BAR}.foo! # => BAR ``` This would solve the problem of case sensitivity for all commands without causing an explosion of flags _and_ make it more granular Assigning to a field using a case-insensitive path is case-preserving. ```nushell mut val = {FOO: "I'm FOO"}; $val # => ╭─────┬─────────╮ # => │ FOO │ I'm FOO │ # => ╰─────┴─────────╯ $val.foo! = "I'm still FOO"; $val # => ╭─────┬───────────────╮ # => │ FOO │ I'm still FOO │ # => ╰─────┴───────────────╯ ``` For `update`, case-insensitive is case-preserving. ```nushell {FOO: 1} | update foo! { $in + 1 } # => ╭─────┬───╮ # => │ FOO │ 2 │ # => ╰─────┴───╯ ``` `insert` can insert values into nested values so accessing into existing columns is case-insensitive, but creating new columns uses the cell-path as it is. So `insert foo! ...` and `insert FOO! ...` would work exactly as they do without `!` ```nushell {FOO: {quox: 0}} # => ╭─────┬──────────────╮ # => │ │ ╭──────┬───╮ │ # => │ FOO │ │ quox │ 0 │ │ # => │ │ ╰──────┴───╯ │ # => ╰─────┴──────────────╯ {FOO: {quox: 0}} | insert foo.bar 1 # => ╭─────┬──────────────╮ # => │ │ ╭──────┬───╮ │ # => │ FOO │ │ quox │ 0 │ │ # => │ │ ╰──────┴───╯ │ # => │ │ ╭─────┬───╮ │ # => │ foo │ │ bar │ 1 │ │ # => │ │ ╰─────┴───╯ │ # => ╰─────┴──────────────╯ {FOO: {quox: 0}} | insert foo!.bar 1 # => ╭─────┬──────────────╮ # => │ │ ╭──────┬───╮ │ # => │ FOO │ │ quox │ 0 │ │ # => │ │ │ bar │ 1 │ │ # => │ │ ╰──────┴───╯ │ # => ╰─────┴──────────────╯ ``` `upsert` is tricky, depending on the input, the data might end up with different column names in rows. We can either forbid case-insensitive cell-paths for `upsert` or trust the user to keep their data in a sensible shape. This would be a breaking change as it would make existing cell-path accesses case-sensitive, however the case-sensitivity is already inconsistent and any attempt at making it consistent would be a breaking change. > What about `$env`? 1. Initially special case it so it keeps its current behavior. 2. Accessing environment variables with non-matching paths gives a deprecation warning urging users to either use exact casing or use the new explicit case-sensitivity syntax 3. Eventuall remove `$env`'s special case, making `$env` accesses case-sensitive by default as well. > `$env.ENV_CONVERSIONS`? In addition to `from_string` and `to_string` add an optional field to opt into case insensitive/preserving behavior. # User-Facing Changes - `get`, `where` and other previously case-insensitive commands are now case-sensitive by default. - `get`'s `--sensitive` flag removed, similar to `--ignore-errors` there is now an `--ignore-case` flag that treats all parts of the cell-path as case-insensitive. - Users can explicitly choose the case case-sensitivity of cell-path accesses or commands. # Tests + Formatting Existing tests required minimal modification. ***However, new tests are not yet added***. - 🟢 toolkit fmt - 🟢 toolkit clippy - 🟢 toolkit test - 🟢 toolkit test stdlib # After Submitting - Update the website to include the new syntax - Update [tree-sitter-nu](https://github.com/nushell/tree-sitter-nu) --------- Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com>
154 lines
4.8 KiB
Rust
154 lines
4.8 KiB
Rust
use std::borrow::Cow;
|
|
|
|
use crate::completions::{Completer, CompletionOptions, SemanticSuggestion, SuggestionKind};
|
|
use nu_engine::{column::get_columns, eval_variable};
|
|
use nu_protocol::{
|
|
ShellError, Span, Value,
|
|
ast::{Expr, Expression, FullCellPath, PathMember},
|
|
engine::{Stack, StateWorkingSet},
|
|
eval_const::eval_constant,
|
|
};
|
|
use reedline::Suggestion;
|
|
|
|
use super::completion_options::NuMatcher;
|
|
|
|
pub struct CellPathCompletion<'a> {
|
|
pub full_cell_path: &'a FullCellPath,
|
|
pub position: usize,
|
|
}
|
|
|
|
fn prefix_from_path_member(member: &PathMember, pos: usize) -> (String, Span) {
|
|
let (prefix_str, start) = match member {
|
|
PathMember::String { val, span, .. } => (val, span.start),
|
|
PathMember::Int { val, span, .. } => (&val.to_string(), span.start),
|
|
};
|
|
let prefix_str = prefix_str.get(..pos + 1 - start).unwrap_or(prefix_str);
|
|
// strip wrapping quotes
|
|
let quotations = ['"', '\'', '`'];
|
|
let prefix_str = prefix_str.strip_prefix(quotations).unwrap_or(prefix_str);
|
|
(prefix_str.to_string(), Span::new(start, pos + 1))
|
|
}
|
|
|
|
impl Completer for CellPathCompletion<'_> {
|
|
fn fetch(
|
|
&mut self,
|
|
working_set: &StateWorkingSet,
|
|
stack: &Stack,
|
|
_prefix: impl AsRef<str>,
|
|
_span: Span,
|
|
offset: usize,
|
|
options: &CompletionOptions,
|
|
) -> Vec<SemanticSuggestion> {
|
|
let mut prefix_str = String::new();
|
|
// position at dots, e.g. `$env.config.<TAB>`
|
|
let mut span = Span::new(self.position + 1, self.position + 1);
|
|
let mut path_member_num_before_pos = 0;
|
|
for member in self.full_cell_path.tail.iter() {
|
|
if member.span().end <= self.position {
|
|
path_member_num_before_pos += 1;
|
|
} else if member.span().contains(self.position) {
|
|
(prefix_str, span) = prefix_from_path_member(member, self.position);
|
|
break;
|
|
}
|
|
}
|
|
|
|
let current_span = reedline::Span {
|
|
start: span.start - offset,
|
|
end: span.end - offset,
|
|
};
|
|
|
|
let mut matcher = NuMatcher::new(prefix_str, options);
|
|
let path_members = self
|
|
.full_cell_path
|
|
.tail
|
|
.get(0..path_member_num_before_pos)
|
|
.unwrap_or_default();
|
|
let value = eval_cell_path(
|
|
working_set,
|
|
stack,
|
|
&self.full_cell_path.head,
|
|
path_members,
|
|
span,
|
|
)
|
|
.unwrap_or_default();
|
|
|
|
for suggestion in get_suggestions_by_value(&value, current_span) {
|
|
matcher.add_semantic_suggestion(suggestion);
|
|
}
|
|
matcher.results()
|
|
}
|
|
}
|
|
|
|
/// Follow cell path to get the value
|
|
/// NOTE: This is a relatively lightweight implementation,
|
|
/// so it may fail to get the exact value when the expression is complicated.
|
|
/// One failing example would be `[$foo].0`
|
|
pub(crate) fn eval_cell_path(
|
|
working_set: &StateWorkingSet,
|
|
stack: &Stack,
|
|
head: &Expression,
|
|
path_members: &[PathMember],
|
|
span: Span,
|
|
) -> Result<Value, ShellError> {
|
|
// evaluate the head expression to get its value
|
|
let head_value = if let Expr::Var(var_id) = head.expr {
|
|
working_set
|
|
.get_variable(var_id)
|
|
.const_val
|
|
.to_owned()
|
|
.map_or_else(
|
|
|| eval_variable(working_set.permanent_state, stack, var_id, span),
|
|
Ok,
|
|
)
|
|
} else {
|
|
eval_constant(working_set, head)
|
|
}?;
|
|
head_value
|
|
.follow_cell_path(path_members)
|
|
.map(Cow::into_owned)
|
|
}
|
|
|
|
fn get_suggestions_by_value(
|
|
value: &Value,
|
|
current_span: reedline::Span,
|
|
) -> Vec<SemanticSuggestion> {
|
|
let to_suggestion = |s: String, v: Option<&Value>| {
|
|
// Check if the string needs quoting
|
|
let value = if s.is_empty()
|
|
|| s.chars()
|
|
.any(|c: char| !(c.is_ascii_alphabetic() || ['_', '-'].contains(&c)))
|
|
{
|
|
format!("{:?}", s)
|
|
} else {
|
|
s
|
|
};
|
|
|
|
SemanticSuggestion {
|
|
suggestion: Suggestion {
|
|
value,
|
|
span: current_span,
|
|
description: v.map(|v| v.get_type().to_string()),
|
|
..Suggestion::default()
|
|
},
|
|
kind: Some(SuggestionKind::CellPath),
|
|
}
|
|
};
|
|
match value {
|
|
Value::Record { val, .. } => val
|
|
.columns()
|
|
.map(|s| to_suggestion(s.to_string(), val.get(s)))
|
|
.collect(),
|
|
Value::List { vals, .. } => get_columns(vals.as_slice())
|
|
.into_iter()
|
|
.map(|s| {
|
|
let sub_val = vals
|
|
.first()
|
|
.and_then(|v| v.as_record().ok())
|
|
.and_then(|rv| rv.get(&s));
|
|
to_suggestion(s, sub_val)
|
|
})
|
|
.collect(),
|
|
_ => vec![],
|
|
}
|
|
}
|