diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 4b2e236a3e..d0eaf1969c 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -260,7 +260,7 @@ pub fn create_default_context( whole_stream_command(Cal), whole_stream_command(Calc), whole_stream_command(Mkdir), - whole_stream_command(Move), + whole_stream_command(Mv), whole_stream_command(Kill), whole_stream_command(Version), whole_stream_command(Clear), @@ -310,6 +310,7 @@ pub fn create_default_context( whole_stream_command(Ansi), whole_stream_command(Char), // Column manipulation + whole_stream_command(MoveColumn), whole_stream_command(Reject), whole_stream_command(Select), whole_stream_command(Get), @@ -345,6 +346,7 @@ pub fn create_default_context( whole_stream_command(Each), whole_stream_command(IsEmpty), // Table manipulation + whole_stream_command(Move), whole_stream_command(Merge), whole_stream_command(Shuffle), whole_stream_command(Wrap), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index 56fc47d540..5d70260dfe 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -79,7 +79,7 @@ pub(crate) mod map_max_by; pub(crate) mod math; pub(crate) mod merge; pub(crate) mod mkdir; -pub(crate) mod mv; +pub(crate) mod move_; pub(crate) mod next; pub(crate) mod nth; pub(crate) mod open; @@ -219,7 +219,7 @@ pub(crate) use math::{ }; pub(crate) use merge::Merge; pub(crate) use mkdir::Mkdir; -pub(crate) use mv::Move; +pub(crate) use move_::{Move, MoveColumn, Mv}; pub(crate) use next::Next; pub(crate) use nth::Nth; pub(crate) use open::Open; @@ -246,10 +246,7 @@ pub(crate) use skip::Skip; pub(crate) use skip_until::SkipUntil; pub(crate) use skip_while::SkipWhile; pub(crate) use sort_by::SortBy; -pub(crate) use split::Split; -pub(crate) use split::SplitChars; -pub(crate) use split::SplitColumn; -pub(crate) use split::SplitRow; +pub(crate) use split::{Split, SplitChars, SplitColumn, SplitRow}; pub(crate) use split_by::SplitBy; pub(crate) use str_::{ Str, StrCapitalize, StrCollect, StrDowncase, StrFindReplace, StrLength, StrSet, StrSubstring, diff --git a/crates/nu-cli/src/commands/move_/column.rs b/crates/nu-cli/src/commands/move_/column.rs new file mode 100644 index 0000000000..563be0a551 --- /dev/null +++ b/crates/nu-cli/src/commands/move_/column.rs @@ -0,0 +1,335 @@ +use crate::commands::WholeStreamCommand; +use crate::context::CommandRegistry; +use crate::data::base::select_fields; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ColumnPath, ReturnSuccess, Signature, SyntaxShape, Value}; +use nu_source::span_for_spanned_list; + +pub struct SubCommand; + +#[derive(Deserialize)] +pub struct Arguments { + rest: Vec, + after: Option, + before: Option, +} + +#[async_trait] +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "move column" + } + + fn signature(&self) -> Signature { + Signature::build("move column") + .rest(SyntaxShape::ColumnPath, "the columns to move") + .named( + "after", + SyntaxShape::ColumnPath, + "the column that will precede the columns moved", + None, + ) + .named( + "before", + SyntaxShape::ColumnPath, + "the column that will be next the columns moved", + None, + ) + } + + fn usage(&self) -> &str { + "Move columns." + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + operate(args, registry).await + } +} + +async fn operate( + raw_args: CommandArgs, + registry: &CommandRegistry, +) -> Result { + let name = raw_args.call_info.name_tag.clone(); + let registry = registry.clone(); + let ( + Arguments { + rest: mut columns, + before, + after, + }, + input, + ) = raw_args.process(®istry).await?; + + if columns.is_empty() { + return Err(ShellError::labeled_error( + "expected columns", + "expected columns", + name, + )); + } + + if columns.iter().any(|c| c.members().len() > 1) { + return Err(ShellError::labeled_error( + "expected columns", + "expected columns", + name, + )); + } + + if vec![&after, &before] + .iter() + .map(|o| if o.is_some() { 1 } else { 0 }) + .sum::() + > 1 + { + return Err(ShellError::labeled_error( + "can't move column(s)", + "pick exactly one (before, after)", + name, + )); + } + + if let Some(after) = after { + let member = columns.remove(0); + + Ok(input + .map(move |item| { + let member = vec![member.clone()]; + let column_paths = vec![&member, &columns] + .into_iter() + .flatten() + .collect::>(); + + let after_span = span_for_spanned_list(after.members().iter().map(|p| p.span)); + + if after.members().len() == 1 { + let keys = column_paths + .iter() + .filter_map(|c| c.last()) + .map(|c| c.as_string()) + .collect::>(); + + if let Some(column) = after.last() { + if !keys.contains(&column.as_string()) { + ReturnSuccess::value(move_after(&item, &keys, &after, &name)?) + } else { + let msg = + format!("can't move column {} after itself", column.as_string()); + Err(ShellError::labeled_error( + "can't move column", + msg, + after_span, + )) + } + } else { + Err(ShellError::labeled_error( + "expected column", + "expected column", + after_span, + )) + } + } else { + Err(ShellError::labeled_error( + "expected column", + "expected column", + after_span, + )) + } + }) + .to_output_stream()) + } else if let Some(before) = before { + let member = columns.remove(0); + + Ok(input + .map(move |item| { + let member = vec![member.clone()]; + let column_paths = vec![&member, &columns] + .into_iter() + .flatten() + .collect::>(); + + let before_span = span_for_spanned_list(before.members().iter().map(|p| p.span)); + + if before.members().len() == 1 { + let keys = column_paths + .iter() + .filter_map(|c| c.last()) + .map(|c| c.as_string()) + .collect::>(); + + if let Some(column) = before.last() { + if !keys.contains(&column.as_string()) { + ReturnSuccess::value(move_before(&item, &keys, &before, &name)?) + } else { + let msg = + format!("can't move column {} before itself", column.as_string()); + Err(ShellError::labeled_error( + "can't move column", + msg, + before_span, + )) + } + } else { + Err(ShellError::labeled_error( + "expected column", + "expected column", + before_span, + )) + } + } else { + Err(ShellError::labeled_error( + "expected column", + "expected column", + before_span, + )) + } + }) + .to_output_stream()) + } else { + Err(ShellError::labeled_error( + "no columns given", + "no columns given", + name, + )) + } +} + +fn move_after( + table: &Value, + columns: &[String], + from: &ColumnPath, + tag: impl Into, +) -> Result { + let tag = tag.into(); + let from_fields = span_for_spanned_list(from.members().iter().map(|p| p.span)); + let from = if let Some((last, _)) = from.split_last() { + last.as_string() + } else { + return Err(ShellError::labeled_error( + "unknown column", + "unknown column", + from_fields, + )); + }; + + let columns_moved = table + .data_descriptors() + .into_iter() + .map(|name| { + if columns.contains(&name) { + None + } else { + Some(name) + } + }) + .collect::>(); + + let mut reordered_columns = vec![]; + let mut insert = false; + let mut inserted = false; + + for name in columns_moved.into_iter() { + if let Some(name) = name { + reordered_columns.push(Some(name.clone())); + + if !inserted && name == from { + insert = true; + } + } else { + reordered_columns.push(None); + } + + if insert { + for column in columns { + reordered_columns.push(Some(column.clone())); + } + inserted = true; + } + } + + Ok(select_fields( + table, + &reordered_columns + .into_iter() + .filter_map(|v| v) + .collect::>(), + &tag, + )) +} + +fn move_before( + table: &Value, + columns: &[String], + from: &ColumnPath, + tag: impl Into, +) -> Result { + let tag = tag.into(); + let from_fields = span_for_spanned_list(from.members().iter().map(|p| p.span)); + let from = if let Some((last, _)) = from.split_last() { + last.as_string() + } else { + return Err(ShellError::labeled_error( + "unknown column", + "unknown column", + from_fields, + )); + }; + + let columns_moved = table + .data_descriptors() + .into_iter() + .map(|name| { + if columns.contains(&name) { + None + } else { + Some(name) + } + }) + .collect::>(); + + let mut reordered_columns = vec![]; + let mut inserted = false; + + for name in columns_moved.into_iter() { + if let Some(name) = name { + if !inserted && name == from { + for column in columns { + reordered_columns.push(Some(column.clone())); + } + + inserted = true; + } + + reordered_columns.push(Some(name.clone())); + } else { + reordered_columns.push(None); + } + } + + Ok(select_fields( + table, + &reordered_columns + .into_iter() + .filter_map(|v| v) + .collect::>(), + &tag, + )) +} + +#[cfg(test)] +mod tests { + use super::SubCommand; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-cli/src/commands/move_/command.rs b/crates/nu-cli/src/commands/move_/command.rs new file mode 100644 index 0000000000..739b3183e7 --- /dev/null +++ b/crates/nu-cli/src/commands/move_/command.rs @@ -0,0 +1,46 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; + +#[derive(Clone)] +pub struct Command; + +#[async_trait] +impl WholeStreamCommand for Command { + fn name(&self) -> &str { + "move" + } + + fn signature(&self) -> Signature { + Signature::build("move") + } + + fn usage(&self) -> &str { + "moves across desired subcommand." + } + + async fn run( + &self, + _args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let registry = registry.clone(); + Ok(OutputStream::one(Ok(ReturnSuccess::Value( + UntaggedValue::string(crate::commands::help::get_help(&Command, ®istry)) + .into_value(Tag::unknown()), + )))) + } +} + +#[cfg(test)] +mod tests { + use super::Command; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(Command {}) + } +} diff --git a/crates/nu-cli/src/commands/move_/mod.rs b/crates/nu-cli/src/commands/move_/mod.rs new file mode 100644 index 0000000000..62269546b6 --- /dev/null +++ b/crates/nu-cli/src/commands/move_/mod.rs @@ -0,0 +1,7 @@ +mod column; +mod command; +pub mod mv; + +pub use column::SubCommand as MoveColumn; +pub use command::Command as Move; +pub use mv::Mv; diff --git a/crates/nu-cli/src/commands/mv.rs b/crates/nu-cli/src/commands/move_/mv.rs similarity index 94% rename from crates/nu-cli/src/commands/mv.rs rename to crates/nu-cli/src/commands/move_/mv.rs index 03b3dbe70e..47850efc7b 100644 --- a/crates/nu-cli/src/commands/mv.rs +++ b/crates/nu-cli/src/commands/move_/mv.rs @@ -6,16 +6,16 @@ use nu_protocol::{Signature, SyntaxShape}; use nu_source::Tagged; use std::path::PathBuf; -pub struct Move; +pub struct Mv; #[derive(Deserialize)] -pub struct MoveArgs { +pub struct Arguments { pub src: Tagged, pub dst: Tagged, } #[async_trait] -impl WholeStreamCommand for Move { +impl WholeStreamCommand for Mv { fn name(&self) -> &str { "mv" } @@ -78,12 +78,12 @@ async fn mv(args: CommandArgs, registry: &CommandRegistry) -> Result Result { diff --git a/crates/nu-cli/src/shell/help_shell.rs b/crates/nu-cli/src/shell/help_shell.rs index 42b6276e46..a92857705f 100644 --- a/crates/nu-cli/src/shell/help_shell.rs +++ b/crates/nu-cli/src/shell/help_shell.rs @@ -3,7 +3,7 @@ use crate::commands::command::EvaluatedWholeStreamCommandArgs; use crate::commands::cp::CopyArgs; use crate::commands::ls::LsArgs; use crate::commands::mkdir::MkdirArgs; -use crate::commands::mv::MoveArgs; +use crate::commands::move_::mv::Arguments as MvArgs; use crate::commands::rm::RemoveArgs; use crate::data::command_dict; use crate::prelude::*; @@ -185,7 +185,7 @@ impl Shell for HelpShell { Ok(OutputStream::empty()) } - fn mv(&self, _args: MoveArgs, _name: Tag, _path: &str) -> Result { + fn mv(&self, _args: MvArgs, _name: Tag, _path: &str) -> Result { Ok(OutputStream::empty()) } diff --git a/crates/nu-cli/src/shell/shell.rs b/crates/nu-cli/src/shell/shell.rs index ceb53d7418..2c8f0d50b8 100644 --- a/crates/nu-cli/src/shell/shell.rs +++ b/crates/nu-cli/src/shell/shell.rs @@ -3,7 +3,7 @@ use crate::commands::command::EvaluatedWholeStreamCommandArgs; use crate::commands::cp::CopyArgs; use crate::commands::ls::LsArgs; use crate::commands::mkdir::MkdirArgs; -use crate::commands::mv::MoveArgs; +use crate::commands::move_::mv::Arguments as MvArgs; use crate::commands::rm::RemoveArgs; use crate::prelude::*; use crate::stream::OutputStream; @@ -23,7 +23,7 @@ pub trait Shell: std::fmt::Debug { fn cd(&self, args: CdArgs, name: Tag) -> Result; fn cp(&self, args: CopyArgs, name: Tag, path: &str) -> Result; fn mkdir(&self, args: MkdirArgs, name: Tag, path: &str) -> Result; - fn mv(&self, args: MoveArgs, name: Tag, path: &str) -> Result; + fn mv(&self, args: MvArgs, name: Tag, path: &str) -> Result; fn rm(&self, args: RemoveArgs, name: Tag, path: &str) -> Result; fn path(&self) -> String; fn pwd(&self, args: EvaluatedWholeStreamCommandArgs) -> Result; diff --git a/crates/nu-cli/src/shell/shell_manager.rs b/crates/nu-cli/src/shell/shell_manager.rs index 5a4c593e2b..b9dd163f87 100644 --- a/crates/nu-cli/src/shell/shell_manager.rs +++ b/crates/nu-cli/src/shell/shell_manager.rs @@ -3,7 +3,7 @@ use crate::commands::command::EvaluatedWholeStreamCommandArgs; use crate::commands::cp::CopyArgs; use crate::commands::ls::LsArgs; use crate::commands::mkdir::MkdirArgs; -use crate::commands::mv::MoveArgs; +use crate::commands::move_::mv::Arguments as MvArgs; use crate::commands::rm::RemoveArgs; use crate::prelude::*; use crate::shell::filesystem_shell::FilesystemShell; @@ -170,7 +170,7 @@ impl ShellManager { shells[self.current_shell()].mkdir(args, name, &path) } - pub fn mv(&self, args: MoveArgs, name: Tag) -> Result { + pub fn mv(&self, args: MvArgs, name: Tag) -> Result { let shells = self.shells.lock(); let path = shells[self.current_shell()].path(); diff --git a/crates/nu-cli/src/shell/value_shell.rs b/crates/nu-cli/src/shell/value_shell.rs index e101fb4744..11fcdc9f49 100644 --- a/crates/nu-cli/src/shell/value_shell.rs +++ b/crates/nu-cli/src/shell/value_shell.rs @@ -3,7 +3,7 @@ use crate::commands::command::EvaluatedWholeStreamCommandArgs; use crate::commands::cp::CopyArgs; use crate::commands::ls::LsArgs; use crate::commands::mkdir::MkdirArgs; -use crate::commands::mv::MoveArgs; +use crate::commands::move_::mv::Arguments as MvArgs; use crate::commands::rm::RemoveArgs; use crate::prelude::*; use crate::shell::shell::Shell; @@ -189,7 +189,7 @@ impl Shell for ValueShell { )) } - fn mv(&self, _args: MoveArgs, name: Tag, _path: &str) -> Result { + fn mv(&self, _args: MvArgs, name: Tag, _path: &str) -> Result { Err(ShellError::labeled_error( "mv not currently supported on values", "not currently supported", diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index 6923d26109..1cfc9ca42d 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -31,7 +31,7 @@ mod ls; mod math; mod merge; mod mkdir; -mod mv; +mod move_; mod open; mod parse; mod prepend; diff --git a/crates/nu-cli/tests/commands/move_/column.rs b/crates/nu-cli/tests/commands/move_/column.rs new file mode 100644 index 0000000000..62c749ee84 --- /dev/null +++ b/crates/nu-cli/tests/commands/move_/column.rs @@ -0,0 +1,141 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn moves_a_column_before() { + Playground::setup("move_column_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "sample.csv", + r#" + column1,column2,column3,...,column98,column99,column100 + -------,-------,-------,---,--------, A ,--------- + -------,-------,-------,---,--------, N ,--------- + -------,-------,-------,---,--------, D ,--------- + -------,-------,-------,---,--------, R ,--------- + -------,-------,-------,---,--------, E ,--------- + -------,-------,-------,---,--------, S ,--------- + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.csv + | move column column99 --before column1 + | rename chars + | get chars + | trim + | str collect + | echo $it + "# + )); + + assert!(actual.out.contains("ANDRES")); + }) +} + +#[test] +fn moves_columns_before() { + Playground::setup("move_column_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "sample.csv", + r#" + column1,column2,column3,...,column98,column99,column100 + -------,-------, A ,---,--------, N ,--------- + -------,-------, D ,---,--------, R ,--------- + -------,-------, E ,---,--------, S ,--------- + -------,-------, : ,---,--------, : ,--------- + -------,-------, J ,---,--------, O ,--------- + -------,-------, N ,---,--------, A ,--------- + -------,-------, T ,---,--------, H ,--------- + -------,-------, A ,---,--------, N ,--------- + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.csv + | move column column99 column3 --before column2 + | rename _ chars_1 chars_2 + | get chars_2 chars_1 + | trim + | str collect + | echo $it + "# + )); + + assert!(actual.out.contains("ANDRES::JONATHAN")); + }) +} + +#[test] +fn moves_a_column_after() { + Playground::setup("move_column_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "sample.csv", + r#" + column1,column2,letters,...,column98,and_more,column100 + -------,-------, A ,---,--------, N ,--------- + -------,-------, D ,---,--------, R ,--------- + -------,-------, E ,---,--------, S ,--------- + -------,-------, : ,---,--------, : ,--------- + -------,-------, J ,---,--------, O ,--------- + -------,-------, N ,---,--------, A ,--------- + -------,-------, T ,---,--------, H ,--------- + -------,-------, A ,---,--------, N ,--------- + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.csv + | move column letters --after and_more + | move column letters and_more --before column2 + | rename _ chars_1 chars_2 + | get chars_1 chars_2 + | trim + | str collect + | echo $it + "# + )); + + assert!(actual.out.contains("ANDRES::JONATHAN")); + }) +} + +#[test] +fn moves_columns_after() { + Playground::setup("move_column_test_4", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "sample.csv", + r#" + column1,column2,letters,...,column98,and_more,column100 + -------,-------, A ,---,--------, N ,--------- + -------,-------, D ,---,--------, R ,--------- + -------,-------, E ,---,--------, S ,--------- + -------,-------, : ,---,--------, : ,--------- + -------,-------, J ,---,--------, O ,--------- + -------,-------, N ,---,--------, A ,--------- + -------,-------, T ,---,--------, H ,--------- + -------,-------, A ,---,--------, N ,--------- + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open sample.csv + | move column letters and_more --after column1 + | get + | nth 1 2 + | str collect + | echo $it + "# + )); + + assert!(actual.out.contains("lettersand_more")); + }) +} diff --git a/crates/nu-cli/tests/commands/move_/mod.rs b/crates/nu-cli/tests/commands/move_/mod.rs new file mode 100644 index 0000000000..58d0a7f6cd --- /dev/null +++ b/crates/nu-cli/tests/commands/move_/mod.rs @@ -0,0 +1,2 @@ +mod column; +mod mv; diff --git a/crates/nu-cli/tests/commands/mv.rs b/crates/nu-cli/tests/commands/move_/mv.rs similarity index 100% rename from crates/nu-cli/tests/commands/mv.rs rename to crates/nu-cli/tests/commands/move_/mv.rs diff --git a/crates/nu-cli/tests/commands/split_column.rs b/crates/nu-cli/tests/commands/split_column.rs index cefd7155b5..fe2358dc7b 100644 --- a/crates/nu-cli/tests/commands/split_column.rs +++ b/crates/nu-cli/tests/commands/split_column.rs @@ -19,8 +19,7 @@ fn to_column() { | lines | trim | split column "," - | pivot - | nth 1 + | get Column2 | echo $it "# )); diff --git a/crates/nu-protocol/src/value/column_path.rs b/crates/nu-protocol/src/value/column_path.rs index 9ee048751a..0c9e5a0c2c 100644 --- a/crates/nu-protocol/src/value/column_path.rs +++ b/crates/nu-protocol/src/value/column_path.rs @@ -61,6 +61,11 @@ impl ColumnPath { pub fn split_last(&self) -> Option<(&PathMember, &[PathMember])> { self.members.split_last() } + + /// Returns the last member + pub fn last(&self) -> Option<&PathMember> { + self.iter().last() + } } impl PrettyDebug for ColumnPath { @@ -99,6 +104,13 @@ impl PathMember { pub fn int(int: impl Into, span: impl Into) -> PathMember { UnspannedPathMember::Int(int.into()).into_path_member(span) } + + pub fn as_string(&self) -> String { + match &self.unspanned { + UnspannedPathMember::String(string) => string.clone(), + UnspannedPathMember::Int(int) => format!("{}", int), + } + } } /// Prepares a list of "sounds like" matches for the string you're trying to find