Add "move column" command. (#2123)

This commit is contained in:
Andrés N. Robalino 2020-07-06 10:27:01 -05:00 committed by GitHub
parent c3ba1e476f
commit 34e1e6e426
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 566 additions and 25 deletions

View File

@ -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),

View File

@ -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,

View File

@ -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<ColumnPath>,
after: Option<ColumnPath>,
before: Option<ColumnPath>,
}
#[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<OutputStream, ShellError> {
operate(args, registry).await
}
}
async fn operate(
raw_args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
let name = raw_args.call_info.name_tag.clone();
let registry = registry.clone();
let (
Arguments {
rest: mut columns,
before,
after,
},
input,
) = raw_args.process(&registry).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::<usize>()
> 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::<Vec<&ColumnPath>>();
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::<Vec<_>>();
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::<Vec<&ColumnPath>>();
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::<Vec<_>>();
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<Tag>,
) -> Result<Value, ShellError> {
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::<Vec<_>>();
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::<Vec<_>>(),
&tag,
))
}
fn move_before(
table: &Value,
columns: &[String],
from: &ColumnPath,
tag: impl Into<Tag>,
) -> Result<Value, ShellError> {
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::<Vec<_>>();
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::<Vec<_>>(),
&tag,
))
}
#[cfg(test)]
mod tests {
use super::SubCommand;
#[test]
fn examples_work_as_expected() {
use crate::examples::test as test_examples;
test_examples(SubCommand {})
}
}

View File

@ -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<OutputStream, ShellError> {
let registry = registry.clone();
Ok(OutputStream::one(Ok(ReturnSuccess::Value(
UntaggedValue::string(crate::commands::help::get_help(&Command, &registry))
.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 {})
}
}

View File

@ -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;

View File

@ -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<PathBuf>,
pub dst: Tagged<PathBuf>,
}
#[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<OutputStrea
#[cfg(test)]
mod tests {
use super::Move;
use super::Mv;
#[test]
fn examples_work_as_expected() {
use crate::examples::test as test_examples;
test_examples(Move {})
test_examples(Mv {})
}
}

View File

@ -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::dir_entry_dict;
use crate::path::canonicalize;
@ -441,7 +441,7 @@ impl Shell for FilesystemShell {
fn mv(
&self,
MoveArgs { src, dst }: MoveArgs,
MvArgs { src, dst }: MvArgs,
_name: Tag,
path: &str,
) -> Result<OutputStream, ShellError> {

View File

@ -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<OutputStream, ShellError> {
fn mv(&self, _args: MvArgs, _name: Tag, _path: &str) -> Result<OutputStream, ShellError> {
Ok(OutputStream::empty())
}

View File

@ -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<OutputStream, ShellError>;
fn cp(&self, args: CopyArgs, name: Tag, path: &str) -> Result<OutputStream, ShellError>;
fn mkdir(&self, args: MkdirArgs, name: Tag, path: &str) -> Result<OutputStream, ShellError>;
fn mv(&self, args: MoveArgs, name: Tag, path: &str) -> Result<OutputStream, ShellError>;
fn mv(&self, args: MvArgs, name: Tag, path: &str) -> Result<OutputStream, ShellError>;
fn rm(&self, args: RemoveArgs, name: Tag, path: &str) -> Result<OutputStream, ShellError>;
fn path(&self) -> String;
fn pwd(&self, args: EvaluatedWholeStreamCommandArgs) -> Result<OutputStream, ShellError>;

View File

@ -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<OutputStream, ShellError> {
pub fn mv(&self, args: MvArgs, name: Tag) -> Result<OutputStream, ShellError> {
let shells = self.shells.lock();
let path = shells[self.current_shell()].path();

View File

@ -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<OutputStream, ShellError> {
fn mv(&self, _args: MvArgs, name: Tag, _path: &str) -> Result<OutputStream, ShellError> {
Err(ShellError::labeled_error(
"mv not currently supported on values",
"not currently supported",

View File

@ -31,7 +31,7 @@ mod ls;
mod math;
mod merge;
mod mkdir;
mod mv;
mod move_;
mod open;
mod parse;
mod prepend;

View File

@ -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"));
})
}

View File

@ -0,0 +1,2 @@
mod column;
mod mv;

View File

@ -19,8 +19,7 @@ fn to_column() {
| lines
| trim
| split column ","
| pivot
| nth 1
| get Column2
| echo $it
"#
));

View File

@ -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<BigInt>, span: impl Into<Span>) -> 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