diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 14b423c454..05add78bc5 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -320,6 +320,7 @@ pub fn create_default_context( whole_stream_command(GroupByDate), whole_stream_command(First), whole_stream_command(Last), + whole_stream_command(Every), whole_stream_command(Nth), whole_stream_command(Drop), whole_stream_command(Format), diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index 51acae5929..dbb6b605f2 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -30,6 +30,7 @@ pub(crate) mod echo; pub(crate) mod enter; #[allow(unused)] pub(crate) mod evaluate_by; +pub(crate) mod every; pub(crate) mod exit; pub(crate) mod first; pub(crate) mod format; @@ -160,6 +161,7 @@ pub(crate) mod touch; pub(crate) use enter::Enter; #[allow(unused_imports)] pub(crate) use evaluate_by::EvaluateBy; +pub(crate) use every::Every; pub(crate) use exit::Exit; pub(crate) use first::First; pub(crate) use format::Format; diff --git a/crates/nu-cli/src/commands/every.rs b/crates/nu-cli/src/commands/every.rs new file mode 100644 index 0000000000..2019ce3a5a --- /dev/null +++ b/crates/nu-cli/src/commands/every.rs @@ -0,0 +1,105 @@ +use crate::commands::WholeStreamCommand; +use crate::context::CommandRegistry; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ReturnSuccess, Signature, SyntaxShape, UntaggedValue}; +use nu_source::Tagged; + +pub struct Every; + +#[derive(Deserialize)] +pub struct EveryArgs { + stride: Tagged, + skip: Tagged, +} + +#[async_trait] +impl WholeStreamCommand for Every { + fn name(&self) -> &str { + "every" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "stride", + SyntaxShape::Int, + "how many rows to skip between (and including) each row returned", + ) + .switch( + "skip", + "skip the rows that would be returned, instead of selecting them", + Some('s'), + ) + } + + fn usage(&self) -> &str { + "Show (or skip) every n-th row, starting from the first one." + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + every(args, registry).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get every second row", + example: "echo [1 2 3 4 5] | every 2", + result: Some(vec![ + UntaggedValue::int(1).into(), + UntaggedValue::int(3).into(), + UntaggedValue::int(5).into(), + ]), + }, + Example { + description: "Skip every second row", + example: "echo [1 2 3 4 5] | every 2 --skip", + result: Some(vec![ + UntaggedValue::int(2).into(), + UntaggedValue::int(4).into(), + ]), + }, + ] + } +} + +async fn every(args: CommandArgs, registry: &CommandRegistry) -> Result { + let registry = registry.clone(); + let (EveryArgs { stride, skip }, input) = args.process(®istry).await?; + let v: Vec<_> = input.into_vec().await; + + let stride_desired = if stride.item < 1 { 1 } else { stride.item } as usize; + + let mut values_vec_deque = VecDeque::new(); + + for (i, x) in v.iter().enumerate() { + let should_include = if skip.item { + i % stride_desired != 0 + } else { + i % stride_desired == 0 + }; + + if should_include { + values_vec_deque.push_back(ReturnSuccess::value(x.clone())); + } + } + + Ok(futures::stream::iter(values_vec_deque).to_output_stream()) +} + +#[cfg(test)] +mod tests { + use super::Every; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(Every {}) + } +} diff --git a/crates/nu-cli/tests/commands/every.rs b/crates/nu-cli/tests/commands/every.rs new file mode 100644 index 0000000000..9992713460 --- /dev/null +++ b/crates/nu-cli/tests/commands/every.rs @@ -0,0 +1,245 @@ +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn gets_all_rows_by_every_zero() { + Playground::setup("every_test_1", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 0 + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ amigos.txt arepas.clu los.txt tres.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_no_rows_by_every_skip_zero() { + Playground::setup("every_test_2", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 0 --skip + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_all_rows_by_every_one() { + Playground::setup("every_test_3", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 1 + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ amigos.txt arepas.clu los.txt tres.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_no_rows_by_every_skip_one() { + Playground::setup("every_test_4", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 1 --skip + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_first_row_by_every_too_much() { + Playground::setup("every_test_5", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 999 + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ amigos.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_all_rows_except_first_by_every_skip_too_much() { + Playground::setup("every_test_6", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 999 --skip + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ arepas.clu los.txt tres.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn gets_every_third_row() { + Playground::setup("every_test_7", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("quatro.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 3 + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ amigos.txt quatro.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} + +#[test] +fn skips_every_third_row() { + Playground::setup("every_test_8", |dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("los.txt"), + EmptyFile("tres.txt"), + EmptyFile("quatro.txt"), + EmptyFile("amigos.txt"), + EmptyFile("arepas.clu"), + ]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + ls + | get name + | every 3 --skip + "# + )); + + let expected = nu!( + cwd: dirs.test(), pipeline( + r#" + echo [ arepas.clu los.txt tres.txt ] + "# + )); + + assert_eq!(actual.out, expected.out); + }) +} diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index e131e19b21..dd43d76201 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -10,6 +10,7 @@ mod default; mod drop; mod each; mod enter; +mod every; mod first; mod format; mod get; diff --git a/docs/commands/every.md b/docs/commands/every.md new file mode 100644 index 0000000000..1e93c42f5e --- /dev/null +++ b/docs/commands/every.md @@ -0,0 +1,46 @@ +# every + +Selects every n-th row of a table, starting from the first one. With the `--skip` flag, every n-th row will be skipped, inverting the original functionality. + +Syntax: `> [input-command] | every {flags}` + +## Flags + +* `--skip`, `-s`: Skip the rows that would be returned, instead of selecting them + + +## Examples + +```shell +> open contacts.csv +───┬─────────┬──────┬───────────────── + # │ first │ last │ email +───┼─────────┼──────┼───────────────── + 0 │ John │ Doe │ doe.1@email.com + 1 │ Jane │ Doe │ doe.2@email.com + 2 │ Chris │ Doe │ doe.3@email.com + 3 │ Francis │ Doe │ doe.4@email.com + 4 │ Stella │ Doe │ doe.5@email.com +───┴─────────┴──────┴───────────────── +``` + +```shell +> open contacts.csv | every 2 +───┬─────────┬──────┬───────────────── + # │ first │ last │ email +───┼─────────┼──────┼───────────────── + 0 │ John │ Doe │ doe.1@email.com + 2 │ Chris │ Doe │ doe.3@email.com + 4 │ Stella │ Doe │ doe.5@email.com +───┴─────────┴──────┴───────────────── +``` + +```shell +> open contacts.csv | every 2 --skip +───┬─────────┬──────┬───────────────── + # │ first │ last │ email +───┼─────────┼──────┼───────────────── + 1 │ Jane │ Doe │ doe.2@email.com + 3 │ Francis │ Doe │ doe.4@email.com +───┴─────────┴──────┴───────────────── +```