diff --git a/crates/nu-command/src/conversions/mod.rs b/crates/nu-command/src/conversions/mod.rs index 8c34392d57..b67c77936f 100644 --- a/crates/nu-command/src/conversions/mod.rs +++ b/crates/nu-command/src/conversions/mod.rs @@ -1,5 +1,7 @@ mod fill; pub(crate) mod into; +mod split_cell_path; pub use fill::Fill; pub use into::*; +pub use split_cell_path::SubCommand as SplitCellPath; diff --git a/crates/nu-command/src/conversions/split_cell_path.rs b/crates/nu-command/src/conversions/split_cell_path.rs new file mode 100644 index 0000000000..c66481bffd --- /dev/null +++ b/crates/nu-command/src/conversions/split_cell_path.rs @@ -0,0 +1,161 @@ +use nu_engine::command_prelude::*; +use nu_protocol::{ast::PathMember, IntoValue}; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "split cell-path" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .input_output_types(vec![ + (Type::CellPath, Type::List(Box::new(Type::Any))), + ( + Type::CellPath, + Type::List(Box::new(Type::Record( + [("value".into(), Type::Any), ("optional".into(), Type::Bool)].into(), + ))), + ), + ]) + .category(Category::Conversions) + .allow_variants_without_examples(true) + } + + fn description(&self) -> &str { + "Split a cell-path into its components." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["convert"] + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let head = call.head; + + let src_span = match input { + // Early return on correct type and empty pipeline + PipelineData::Value(Value::CellPath { val, .. }, _) => { + return Ok(split_cell_path(val, head)?.into_pipeline_data()) + } + PipelineData::Empty => return Err(ShellError::PipelineEmpty { dst_span: head }), + + // Extract span from incorrect pipeline types + // NOTE: Match arms can't be combined, `stream`s are of different types + PipelineData::Value(other, _) => other.span(), + PipelineData::ListStream(stream, ..) => stream.span(), + PipelineData::ByteStream(stream, ..) => stream.span(), + }; + Err(ShellError::PipelineMismatch { + exp_input_type: "cell-path".into(), + dst_span: head, + src_span, + }) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Split a cell-path into its components", + example: "$.5?.c | split cell-path", + result: Some(Value::test_list(vec![ + Value::test_record(record! { + "value" => Value::test_int(5), + "optional" => Value::test_bool(true), + }), + Value::test_record(record! { + "value" => Value::test_string("c"), + "optional" => Value::test_bool(false), + }), + ])), + }, + Example { + description: "Split a complex 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), + }), + Value::test_record(record! { + "value" => Value::test_string("b"), + "optional" => Value::test_bool(true), + }), + Value::test_record(record! { + "value" => Value::test_int(1), + "optional" => Value::test_bool(false), + }), + Value::test_record(record! { + "value" => Value::test_string("2"), + "optional" => Value::test_bool(false), + }), + Value::test_record(record! { + "value" => Value::test_string("c.d"), + "optional" => Value::test_bool(false), + }), + ])), + }, + ] + } +} + +fn split_cell_path(val: CellPath, span: Span) -> Result { + #[derive(IntoValue)] + struct PathMemberRecord { + value: Value, + optional: 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 value = match pm { + PathMember::String { val, .. } => Value::String { val, internal_span }, + PathMember::Int { val, .. } => Value::Int { + val: val as i64, + internal_span, + }, + }; + Self { value, optional } + } + } + + let members = val + .members + .into_iter() + .map(|pm| { + let span = match pm { + PathMember::String { span, .. } | PathMember::Int { span, .. } => span, + }; + PathMemberRecord::from_path_member(pm).into_value(span) + }) + .collect(); + + Ok(Value::List { + vals: members, + internal_span: span, + }) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index d98ac41f53..bf16c05e36 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -326,6 +326,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { IntoString, IntoGlob, IntoValue, + SplitCellPath, }; // Env