diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 13ce4b2daa..8d75488fea 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -70,6 +70,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { GroupBy, Keep, Merge, + Move, KeepUntil, KeepWhile, Last, diff --git a/crates/nu-command/src/filters/merge.rs b/crates/nu-command/src/filters/merge.rs index 51db1f193a..5caf5e862d 100644 --- a/crates/nu-command/src/filters/merge.rs +++ b/crates/nu-command/src/filters/merge.rs @@ -72,6 +72,7 @@ impl Command for Merge { let block: CaptureBlock = call.req(engine_state, stack, 0)?; let mut stack = stack.captures_to_stack(&block.captures); + let metadata = input.metadata(); let ctrlc = engine_state.ctrlc.clone(); let block = engine_state.get_block(block.block_id); let call = call.clone(); @@ -96,25 +97,33 @@ impl Command for Merge { ) => { let mut table_iter = table.into_iter(); - Ok(input - .into_iter() - .map(move |inp| match (inp.as_record(), table_iter.next()) { - (Ok((inp_cols, inp_vals)), Some(to_merge)) => match to_merge.as_record() { - Ok((to_merge_cols, to_merge_vals)) => { - let cols = [inp_cols, to_merge_cols].concat(); - let vals = [inp_vals, to_merge_vals].concat(); - Value::Record { - cols, - vals, - span: call.head, + let res = + input + .into_iter() + .map(move |inp| match (inp.as_record(), table_iter.next()) { + (Ok((inp_cols, inp_vals)), Some(to_merge)) => { + match to_merge.as_record() { + Ok((to_merge_cols, to_merge_vals)) => { + let cols = [inp_cols, to_merge_cols].concat(); + let vals = [inp_vals, to_merge_vals].concat(); + Value::Record { + cols, + vals, + span: call.head, + } + } + Err(error) => Value::Error { error }, } } - Err(error) => Value::Error { error }, - }, - (_, None) => inp, - (Err(error), _) => Value::Error { error }, - }) - .into_pipeline_data(ctrlc)) + (_, None) => inp, + (Err(error), _) => Value::Error { error }, + }); + + if let Some(md) = metadata { + Ok(res.into_pipeline_data_with_metadata(md, ctrlc)) + } else { + Ok(res.into_pipeline_data(ctrlc)) + } } // record ( @@ -170,20 +179,6 @@ impl Command for Merge { } } -/* -fn merge_values( -left: &UntaggedValue, -right: &UntaggedValue, -) -> Result { -match (left, right) { -(UntaggedValue::Row(columns), UntaggedValue::Row(columns_b)) => { -Ok(UntaggedValue::Row(columns.merge_from(columns_b))) -} -(left, right) => Err((left.type_name(), right.type_name())), -} -} -*/ - #[cfg(test)] mod test { use super::*; diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs index a58659b442..c85c2583a4 100644 --- a/crates/nu-command/src/filters/mod.rs +++ b/crates/nu-command/src/filters/mod.rs @@ -18,6 +18,7 @@ mod last; mod length; mod lines; mod merge; +mod move_; mod nth; mod par_each; mod prepend; @@ -56,6 +57,7 @@ pub use last::Last; pub use length::Length; pub use lines::Lines; pub use merge::Merge; +pub use move_::Move; pub use nth::Nth; pub use par_each::ParEach; pub use prepend::Prepend; diff --git a/crates/nu-command/src/filters/move_.rs b/crates/nu-command/src/filters/move_.rs new file mode 100644 index 0000000000..bc5d570081 --- /dev/null +++ b/crates/nu-command/src/filters/move_.rs @@ -0,0 +1,302 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, + Signature, Span, Spanned, SyntaxShape, Value, +}; + +#[derive(Clone, Debug)] +enum BeforeOrAfter { + Before(String), + After(String), +} + +#[derive(Clone)] +pub struct Move; + +impl Command for Move { + fn name(&self) -> &str { + "move" + } + + fn usage(&self) -> &str { + "Move columns before or after other columns" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("move") + .rest("columns", SyntaxShape::String, "the columns to move") + .named( + "after", + SyntaxShape::String, + "the column that will precede the columns moved", + None, + ) + .named( + "before", + SyntaxShape::String, + "the column that will be the next after the columns moved", + None, + ) + .category(Category::Filters) + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[[name value index]; [foo a 1] [bar b 2] [baz c 3]] | move index --before name", + description: "Move a column before the first column", + result: + Some(Value::List { + vals: vec![ + Value::test_record( + vec!["index", "name", "value"], + vec![Value::test_int(1), Value::test_string("foo"), Value::test_string("a")], + ), + Value::test_record( + vec!["index", "name", "value"], + vec![Value::test_int(2), Value::test_string("bar"), Value::test_string("b")], + ), + Value::test_record( + vec!["index", "name", "value"], + vec![Value::test_int(3), Value::test_string("baz"), Value::test_string("c")], + ), + ], + span: Span::test_data(), + }) + }, + Example { + example: "[[name value index]; [foo a 1] [bar b 2] [baz c 3]] | move value name --after index", + description: "Move multiple columns after the last column and reorder them", + result: + Some(Value::List { + vals: vec![ + Value::test_record( + vec!["index", "value", "name"], + vec![Value::test_int(1), Value::test_string("a"), Value::test_string("foo")], + ), + Value::test_record( + vec!["index", "value", "name"], + vec![Value::test_int(2), Value::test_string("b"), Value::test_string("bar")], + ), + Value::test_record( + vec!["index", "value", "name"], + vec![Value::test_int(3), Value::test_string("c"), Value::test_string("baz")], + ), + ], + span: Span::test_data(), + }) + }, + Example { + example: "{ name: foo, value: a, index: 1 } | move name --before index", + description: "Move columns of a record", + result: Some(Value::test_record( + vec!["value", "name", "index"], + vec![Value::test_string("a"), Value::test_string("foo"), Value::test_int(1)], + )) + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let columns: Vec = call.rest(engine_state, stack, 0)?; + let after: Option = call.get_flag(engine_state, stack, "after")?; + let before: Option = call.get_flag(engine_state, stack, "before")?; + + let before_or_after = match (after, before) { + (Some(v), None) => Spanned { + item: BeforeOrAfter::After(v.as_string()?), + span: v.span()?, + }, + (None, Some(v)) => Spanned { + item: BeforeOrAfter::Before(v.as_string()?), + span: v.span()?, + }, + (Some(_), Some(_)) => { + return Err(ShellError::SpannedLabeledError( + "Cannot move columns".to_string(), + "Use either --after, or --before, not both".to_string(), + call.head, + )) + } + (None, None) => { + return Err(ShellError::SpannedLabeledError( + "Cannot move columns".to_string(), + "Missing --after or --before flag".to_string(), + call.head, + )) + } + }; + + let metadata = input.metadata(); + let ctrlc = engine_state.ctrlc.clone(); + let call = call.clone(); + + match input { + PipelineData::Value(Value::List { .. }, ..) | PipelineData::ListStream { .. } => { + let res = input.into_iter().map(move |x| match x.as_record() { + Ok((inp_cols, inp_vals)) => match move_record_columns( + inp_cols, + inp_vals, + &columns, + &before_or_after, + call.head, + ) { + Ok(val) => val, + Err(error) => Value::Error { error }, + }, + Err(error) => Value::Error { error }, + }); + + if let Some(md) = metadata { + Ok(res.into_pipeline_data_with_metadata(md, ctrlc)) + } else { + Ok(res.into_pipeline_data(ctrlc)) + } + } + PipelineData::Value( + Value::Record { + cols: inp_cols, + vals: inp_vals, + .. + }, + .., + ) => Ok(move_record_columns( + &inp_cols, + &inp_vals, + &columns, + &before_or_after, + call.head, + )? + .into_pipeline_data()), + _ => Err(ShellError::PipelineMismatch( + "record or table".to_string(), + call.head, + Span::new(call.head.start, call.head.start), + )), + } + } +} + +// Move columns within a record +fn move_record_columns( + inp_cols: &[String], + inp_vals: &[Value], + columns: &[Value], + before_or_after: &Spanned, + span: Span, +) -> Result { + let mut column_idx: Vec = Vec::with_capacity(columns.len()); + + // Check if before/after column exist + match &before_or_after.item { + BeforeOrAfter::After(after) => { + if !inp_cols.contains(after) { + return Err(ShellError::SpannedLabeledError( + "Cannot move columns".to_string(), + "column does not exist".to_string(), + before_or_after.span, + )); + } + } + BeforeOrAfter::Before(before) => { + if !inp_cols.contains(before) { + return Err(ShellError::SpannedLabeledError( + "Cannot move columns".to_string(), + "column does not exist".to_string(), + before_or_after.span, + )); + } + } + } + + // Find indices of columns to be moved + for column in columns.iter() { + let column_str = column.as_string()?; + + if let Some(idx) = inp_cols.iter().position(|inp_col| &column_str == inp_col) { + column_idx.push(idx); + } else { + return Err(ShellError::SpannedLabeledError( + "Cannot move columns".to_string(), + "column does not exist".to_string(), + column.span()?, + )); + } + } + + if columns.is_empty() {} + + let mut out_cols: Vec = Vec::with_capacity(inp_cols.len()); + let mut out_vals: Vec = Vec::with_capacity(inp_vals.len()); + + for (i, (inp_col, inp_val)) in inp_cols.iter().zip(inp_vals).enumerate() { + match &before_or_after.item { + BeforeOrAfter::After(after) if after == inp_col => { + out_cols.push(inp_col.into()); + out_vals.push(inp_val.clone()); + + for idx in column_idx.iter() { + if let (Some(col), Some(val)) = (inp_cols.get(*idx), inp_vals.get(*idx)) { + out_cols.push(col.into()); + out_vals.push(val.clone()); + } else { + return Err(ShellError::NushellFailedSpanned( + "Error indexing input columns".to_string(), + "originates from here".to_string(), + span, + )); + } + } + } + BeforeOrAfter::Before(before) if before == inp_col => { + for idx in column_idx.iter() { + if let (Some(col), Some(val)) = (inp_cols.get(*idx), inp_vals.get(*idx)) { + out_cols.push(col.into()); + out_vals.push(val.clone()); + } else { + return Err(ShellError::NushellFailedSpanned( + "Error indexing input columns".to_string(), + "originates from here".to_string(), + span, + )); + } + } + + out_cols.push(inp_col.into()); + out_vals.push(inp_val.clone()); + } + _ => { + if !column_idx.contains(&i) { + out_cols.push(inp_col.into()); + out_vals.push(inp_val.clone()); + } + } + } + } + + Ok(Value::Record { + cols: out_cols, + vals: out_vals, + span, + }) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Move {}) + } +} diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index c3b46becb5..43f75cafd7 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -91,6 +91,11 @@ pub enum ShellError { #[diagnostic(code(nu::shell::nushell_failed), url(docsrs))] NushellFailed(String), + // Only use this one if we Nushell completely falls over and hits a state that isn't possible or isn't recoverable + #[error("Nushell failed: {0}.")] + #[diagnostic(code(nu::shell::nushell_failed), url(docsrs))] + NushellFailedSpanned(String, String, #[label = "{1}"] Span), + #[error("Variable not found")] #[diagnostic(code(nu::shell::variable_not_found), url(docsrs))] VariableNotFoundAtRuntime(#[label = "variable not found"] Span),