mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 08:23:24 +01:00
Add into cell-path
for dynamic cell-path creation (#11322)
# Description The `cell-path` is a type that can be created statically with `$.nested.structure.5`, but can't be created from user input. This makes it difficult to take advantage of commands that accept a cell-path to operate on data structures. This PR adds `into cell-path` for dynamic cell-path creation. `into cell-path` accepts the following input shapes: * Bare integer (equivalent to `$.1`) * List of strings and integers * List of records with entries `value` and `optional` * String (parsed into a cell-path) ## Example usage An example of where `into cell-path` can be used is in working with `git config --list`. The git configuration has a tree structure that maps well to nushell records. With dynamic cell paths it is easy to convert `git config list` to a record: ```nushell git config --list | lines | parse -r '^(?<key>[^=]+)=(?<value>.*)' | reduce --fold {} {|entry, result| let path = $entry.key | into cell-path $result | upsert $path {|| $entry.value } } | select remote ``` Output: ``` ╭────────┬──────────────────────────────────────────────────────────────────╮ │ │ ╭──────────┬───────────────────────────────────────────────────╮ │ │ remote │ │ │ ╭───────┬───────────────────────────────────────╮ │ │ │ │ │ upstream │ │ url │ git@github.com:nushell/nushell.git │ │ │ │ │ │ │ │ fetch │ +refs/heads/*:refs/remotes/upstream/* │ │ │ │ │ │ │ ╰───────┴───────────────────────────────────────╯ │ │ │ │ │ │ ╭───────┬─────────────────────────────────────╮ │ │ │ │ │ origin │ │ url │ git@github.com:drbrain/nushell │ │ │ │ │ │ │ │ fetch │ +refs/heads/*:refs/remotes/origin/* │ │ │ │ │ │ │ ╰───────┴─────────────────────────────────────╯ │ │ │ │ ╰──────────┴───────────────────────────────────────────────────╯ │ ╰────────┴──────────────────────────────────────────────────────────────────╯ ``` ## Errors `lex()` + `parse_cell_path()` are forgiving about what is allowed in a cell-path so it will allow what appears to be nonsense to become a cell-path: ```nushell let table = [["!@$%^&*" value]; [key value]] $table | get ("!@$%^&*.0" | into cell-path) # => key ``` But it will reject bad cell-paths: ``` ❯ "a b" | into cell-path Error: nu:🐚:cant_convert × Can't convert to cell-path. ╭─[entry #14:1:1] 1 │ "a b" | into cell-path · ───────┬────── · ╰── can't convert string to cell-path ╰──── help: "a b" is not a valid cell-path (Parse mismatch during operation.) ``` # User-Facing Changes New conversion command `into cell-path` # Tests + Formatting - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib` # After Submitting Automatic documentation updates
This commit is contained in:
parent
0aabe84460
commit
2a65d43c13
225
crates/nu-command/src/conversions/into/cell_path.rs
Normal file
225
crates/nu-command/src/conversions/into/cell_path.rs
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
use nu_protocol::{
|
||||||
|
ast::{Call, CellPath, PathMember},
|
||||||
|
engine::{Command, EngineState, Stack},
|
||||||
|
Category, Example, IntoPipelineData, PipelineData, Record, ShellError, Signature, Span, Type,
|
||||||
|
Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct IntoCellPath;
|
||||||
|
|
||||||
|
impl Command for IntoCellPath {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"into cell-path"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("into cell-path")
|
||||||
|
.input_output_types(vec![
|
||||||
|
(Type::Int, Type::CellPath),
|
||||||
|
(Type::List(Box::new(Type::Any)), Type::CellPath),
|
||||||
|
(
|
||||||
|
Type::List(Box::new(Type::Record(vec![
|
||||||
|
("value".into(), Type::Any),
|
||||||
|
("optional".into(), Type::Bool),
|
||||||
|
]))),
|
||||||
|
Type::CellPath,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
.category(Category::Conversions)
|
||||||
|
.allow_variants_without_examples(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> &str {
|
||||||
|
"Convert value to a cell-path."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_terms(&self) -> Vec<&str> {
|
||||||
|
vec!["convert"]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extra_usage(&self) -> &str {
|
||||||
|
"Converting a string directly into a cell path is intentionally not supported."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
_engine_state: &EngineState,
|
||||||
|
_stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
input: PipelineData,
|
||||||
|
) -> Result<PipelineData, ShellError> {
|
||||||
|
into_cell_path(call, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![
|
||||||
|
Example {
|
||||||
|
description: "Convert integer into cell path",
|
||||||
|
example: "5 | into cell-path",
|
||||||
|
result: Some(Value::test_cell_path(CellPath {
|
||||||
|
members: vec![PathMember::test_int(5, false)],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Convert string into cell path",
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Convert list into cell path",
|
||||||
|
example: "[5 c 7 h] | into cell-path",
|
||||||
|
result: Some(Value::test_cell_path(CellPath {
|
||||||
|
members: vec![
|
||||||
|
PathMember::test_int(5, false),
|
||||||
|
PathMember::test_string("c".into(), false),
|
||||||
|
PathMember::test_int(7, false),
|
||||||
|
PathMember::test_string("h".into(), false),
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
Example {
|
||||||
|
description: "Convert table into cell path",
|
||||||
|
example: "[[value, optional]; [5 true] [c false]] | into cell-path",
|
||||||
|
result: Some(Value::test_cell_path(CellPath {
|
||||||
|
members: vec![
|
||||||
|
PathMember::test_int(5, true),
|
||||||
|
PathMember::test_string("c".into(), false),
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_cell_path(call: &Call, input: PipelineData) -> Result<PipelineData, ShellError> {
|
||||||
|
let head = call.head;
|
||||||
|
|
||||||
|
match input {
|
||||||
|
PipelineData::Value(value, _) => Ok(value_to_cell_path(&value, head)?.into_pipeline_data()),
|
||||||
|
PipelineData::ListStream(stream, ..) => {
|
||||||
|
let list: Vec<_> = stream.collect();
|
||||||
|
Ok(list_to_cell_path(&list, head)?.into_pipeline_data())
|
||||||
|
}
|
||||||
|
PipelineData::ExternalStream { span, .. } => Err(ShellError::OnlySupportsThisInputType {
|
||||||
|
exp_input_type: "list, int".into(),
|
||||||
|
wrong_type: "raw data".into(),
|
||||||
|
dst_span: head,
|
||||||
|
src_span: span,
|
||||||
|
}),
|
||||||
|
PipelineData::Empty => Err(ShellError::PipelineEmpty { dst_span: head }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn int_to_cell_path(val: i64, span: Span) -> Value {
|
||||||
|
let member = match int_to_path_member(val, span) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => {
|
||||||
|
return Value::error(e, span);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = CellPath {
|
||||||
|
members: vec![member],
|
||||||
|
};
|
||||||
|
|
||||||
|
Value::cell_path(path, span)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn int_to_path_member(val: i64, span: Span) -> Result<PathMember, ShellError> {
|
||||||
|
let Ok(val) = val.try_into() else {
|
||||||
|
return Err(ShellError::NeedsPositiveValue { span });
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PathMember::int(val, false, span))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_to_cell_path(vals: &[Value], span: Span) -> Result<Value, ShellError> {
|
||||||
|
let mut members = vec![];
|
||||||
|
|
||||||
|
for val in vals {
|
||||||
|
members.push(value_to_path_member(val, span)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = CellPath { members };
|
||||||
|
|
||||||
|
Ok(Value::cell_path(path, span))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_to_path_member(
|
||||||
|
record: &Record,
|
||||||
|
val_span: Span,
|
||||||
|
span: Span,
|
||||||
|
) -> Result<PathMember, ShellError> {
|
||||||
|
let Some(value) = record.get("value") else {
|
||||||
|
return Err(ShellError::CantFindColumn {
|
||||||
|
col_name: "value".into(),
|
||||||
|
span: val_span,
|
||||||
|
src_span: span,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut member = value_to_path_member(value, span)?;
|
||||||
|
|
||||||
|
if let Some(optional) = record.get("optional") {
|
||||||
|
if optional.as_bool()? {
|
||||||
|
member.make_optional();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(member)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_to_cell_path(value: &Value, span: Span) -> Result<Value, ShellError> {
|
||||||
|
match value {
|
||||||
|
Value::Int { val, .. } => Ok(int_to_cell_path(*val, span)),
|
||||||
|
Value::List { vals, .. } => list_to_cell_path(vals, span),
|
||||||
|
other => Err(ShellError::OnlySupportsThisInputType {
|
||||||
|
exp_input_type: "int, list".into(),
|
||||||
|
wrong_type: other.get_type().to_string(),
|
||||||
|
dst_span: span,
|
||||||
|
src_span: other.span(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_to_path_member(val: &Value, span: Span) -> Result<PathMember, ShellError> {
|
||||||
|
let member = match val {
|
||||||
|
Value::Int {
|
||||||
|
val,
|
||||||
|
internal_span: span,
|
||||||
|
} => int_to_path_member(*val, *span)?,
|
||||||
|
Value::String {
|
||||||
|
val,
|
||||||
|
internal_span: span,
|
||||||
|
} => PathMember::string(val.into(), false, *span),
|
||||||
|
Value::Record { val, internal_span } => record_to_path_member(val, *internal_span, span)?,
|
||||||
|
other => {
|
||||||
|
return Err(ShellError::CantConvert {
|
||||||
|
to_type: "int or string".to_string(),
|
||||||
|
from_type: other.get_type().to_string(),
|
||||||
|
span: val.span(),
|
||||||
|
help: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(member)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_examples() {
|
||||||
|
use crate::test_examples;
|
||||||
|
|
||||||
|
test_examples(IntoCellPath {})
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
mod binary;
|
mod binary;
|
||||||
mod bool;
|
mod bool;
|
||||||
|
mod cell_path;
|
||||||
mod command;
|
mod command;
|
||||||
mod datetime;
|
mod datetime;
|
||||||
mod duration;
|
mod duration;
|
||||||
@ -13,6 +14,7 @@ mod value;
|
|||||||
pub use self::bool::SubCommand as IntoBool;
|
pub use self::bool::SubCommand as IntoBool;
|
||||||
pub use self::filesize::SubCommand as IntoFilesize;
|
pub use self::filesize::SubCommand as IntoFilesize;
|
||||||
pub use binary::SubCommand as IntoBinary;
|
pub use binary::SubCommand as IntoBinary;
|
||||||
|
pub use cell_path::IntoCellPath;
|
||||||
pub use command::Into;
|
pub use command::Into;
|
||||||
pub use datetime::SubCommand as IntoDatetime;
|
pub use datetime::SubCommand as IntoDatetime;
|
||||||
pub use duration::SubCommand as IntoDuration;
|
pub use duration::SubCommand as IntoDuration;
|
||||||
|
@ -293,6 +293,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||||||
Into,
|
Into,
|
||||||
IntoBool,
|
IntoBool,
|
||||||
IntoBinary,
|
IntoBinary,
|
||||||
|
IntoCellPath,
|
||||||
IntoDatetime,
|
IntoDatetime,
|
||||||
IntoDuration,
|
IntoDuration,
|
||||||
IntoFloat,
|
IntoFloat,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use super::Expression;
|
use super::Expression;
|
||||||
use crate::Span;
|
use crate::Span;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt::Display;
|
use std::{cmp::Ordering, fmt::Display};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialOrd, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum PathMember {
|
pub enum PathMember {
|
||||||
String {
|
String {
|
||||||
val: String,
|
val: String,
|
||||||
@ -17,6 +17,51 @@ pub enum PathMember {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PathMember {
|
||||||
|
pub fn int(val: usize, optional: bool, span: Span) -> Self {
|
||||||
|
PathMember::Int {
|
||||||
|
val,
|
||||||
|
span,
|
||||||
|
optional,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string(val: String, optional: bool, span: Span) -> Self {
|
||||||
|
PathMember::String {
|
||||||
|
val,
|
||||||
|
span,
|
||||||
|
optional,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_int(val: usize, optional: bool) -> Self {
|
||||||
|
PathMember::Int {
|
||||||
|
val,
|
||||||
|
optional,
|
||||||
|
span: Span::test_data(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_string(val: String, optional: bool) -> Self {
|
||||||
|
PathMember::String {
|
||||||
|
val,
|
||||||
|
optional,
|
||||||
|
span: Span::test_data(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_optional(&mut self) {
|
||||||
|
match self {
|
||||||
|
PathMember::String {
|
||||||
|
ref mut optional, ..
|
||||||
|
} => *optional = true,
|
||||||
|
PathMember::Int {
|
||||||
|
ref mut optional, ..
|
||||||
|
} => *optional = true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PartialEq for PathMember {
|
impl PartialEq for PathMember {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
match (self, other) {
|
match (self, other) {
|
||||||
@ -49,6 +94,55 @@ impl PartialEq for PathMember {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for PathMember {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
match (self, other) {
|
||||||
|
(
|
||||||
|
PathMember::String {
|
||||||
|
val: l_val,
|
||||||
|
optional: l_opt,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
PathMember::String {
|
||||||
|
val: r_val,
|
||||||
|
optional: r_opt,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let val_ord = Some(l_val.cmp(r_val));
|
||||||
|
|
||||||
|
if let Some(Ordering::Equal) = val_ord {
|
||||||
|
Some(l_opt.cmp(r_opt))
|
||||||
|
} else {
|
||||||
|
val_ord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(
|
||||||
|
PathMember::Int {
|
||||||
|
val: l_val,
|
||||||
|
optional: l_opt,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
PathMember::Int {
|
||||||
|
val: r_val,
|
||||||
|
optional: r_opt,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let val_ord = Some(l_val.cmp(r_val));
|
||||||
|
|
||||||
|
if let Some(Ordering::Equal) = val_ord {
|
||||||
|
Some(l_opt.cmp(r_opt))
|
||||||
|
} else {
|
||||||
|
val_ord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(PathMember::Int { .. }, PathMember::String { .. }) => Some(Ordering::Greater),
|
||||||
|
(PathMember::String { .. }, PathMember::Int { .. }) => Some(Ordering::Less),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||||
pub struct CellPath {
|
pub struct CellPath {
|
||||||
pub members: Vec<PathMember>,
|
pub members: Vec<PathMember>,
|
||||||
@ -57,14 +151,7 @@ pub struct CellPath {
|
|||||||
impl CellPath {
|
impl CellPath {
|
||||||
pub fn make_optional(&mut self) {
|
pub fn make_optional(&mut self) {
|
||||||
for member in &mut self.members {
|
for member in &mut self.members {
|
||||||
match member {
|
member.make_optional();
|
||||||
PathMember::String {
|
|
||||||
ref mut optional, ..
|
|
||||||
} => *optional = true,
|
|
||||||
PathMember::Int {
|
|
||||||
ref mut optional, ..
|
|
||||||
} => *optional = true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -89,3 +176,39 @@ pub struct FullCellPath {
|
|||||||
pub head: Expression,
|
pub head: Expression,
|
||||||
pub tail: Vec<PathMember>,
|
pub tail: Vec<PathMember>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use std::cmp::Ordering::Greater;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn path_member_partial_ord() {
|
||||||
|
assert_eq!(
|
||||||
|
Some(Greater),
|
||||||
|
PathMember::test_int(5, true).partial_cmp(&PathMember::test_string("e".into(), true))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Some(Greater),
|
||||||
|
PathMember::test_int(5, true).partial_cmp(&PathMember::test_int(5, false))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Some(Greater),
|
||||||
|
PathMember::test_int(6, true).partial_cmp(&PathMember::test_int(5, true))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Some(Greater),
|
||||||
|
PathMember::test_string("e".into(), true)
|
||||||
|
.partial_cmp(&PathMember::test_string("e".into(), false))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Some(Greater),
|
||||||
|
PathMember::test_string("f".into(), true)
|
||||||
|
.partial_cmp(&PathMember::test_string("e".into(), true))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user