From 71611dec4f6e2224143b23ea0a7fcc7914ff9504 Mon Sep 17 00:00:00 2001 From: Sygmei <3835355+Sygmei@users.noreply.github.com> Date: Fri, 14 Apr 2023 21:42:33 +0200 Subject: [PATCH] feat: added `items` command for Records (#8640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description This PR adds an `items` command which allows the user to iterate over both `columns` and `values` of a `Record<>` type at the same time. ![image](https://user-images.githubusercontent.com/3835355/227976277-c9badbb2-2e31-4243-8d00-7e28f2289587.png) # User-Facing Changes No breaking changes, only a new `items` command. # Formatting - `cargo fmt --all -- --check` 👌 - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` 👌 - `cargo test --workspace` 👌 --- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/filters/items.rs | 155 +++++++++++++++++++++++ crates/nu-command/src/filters/mod.rs | 2 + 3 files changed, 158 insertions(+) create mode 100644 crates/nu-command/src/filters/items.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index dd1e719ee..fb80c7989 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -56,6 +56,7 @@ pub fn create_default_context() -> EngineState { GroupBy, Headers, Insert, + Items, Join, SplitBy, Take, diff --git a/crates/nu-command/src/filters/items.rs b/crates/nu-command/src/filters/items.rs new file mode 100644 index 000000000..7a5988d2d --- /dev/null +++ b/crates/nu-command/src/filters/items.rs @@ -0,0 +1,155 @@ +use super::utils::chain_error_with_input; +use nu_engine::{eval_block_with_early_return, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Closure, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + SyntaxShape, Type, Value, +}; + +#[derive(Clone)] +pub struct Items; + +impl Command for Items { + fn name(&self) -> &str { + "items" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .input_output_types(vec![( + Type::Record(vec![]), + Type::List(Box::new(Type::String)), + )]) + .required( + "closure", + SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Any])), + "the closure to run", + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Given a record, iterate on each pair of column name and associated value." + } + + fn extra_usage(&self) -> &str { + "This is a the fusion of `columns`, `values` and `each`." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let capture_block: Closure = call.req(engine_state, stack, 0)?; + + let metadata = input.metadata(); + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + let block = engine_state.get_block(capture_block.block_id).clone(); + let mut stack = stack.captures_to_stack(&capture_block.captures); + let orig_env_vars = stack.env_vars.clone(); + let orig_env_hidden = stack.env_hidden.clone(); + let span = call.head; + let redirect_stdout = call.redirect_stdout; + let redirect_stderr = call.redirect_stderr; + + let input_span = input.span().unwrap_or(call.head); + let run_for_each_item = move |keyval: (String, Value)| -> Option { + // with_env() is used here to ensure that each iteration uses + // a different set of environment variables. + // Hence, a 'cd' in the first loop won't affect the next loop. + stack.with_env(&orig_env_vars, &orig_env_hidden); + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, Value::string(keyval.0.clone(), span)); + } + } + + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, keyval.1); + } + } + + match eval_block_with_early_return( + &engine_state, + &mut stack, + &block, + PipelineData::empty(), + redirect_stdout, + redirect_stderr, + ) { + Ok(v) => Some(v.into_value(span)), + Err(ShellError::Break(_)) => None, + Err(error) => { + let error = chain_error_with_input(error, Ok(input_span)); + Some(Value::Error { + error: Box::new(error), + }) + } + } + }; + match input { + PipelineData::Empty => Ok(PipelineData::Empty), + PipelineData::Value(Value::Record { cols, vals, .. }, ..) => Ok(cols + .into_iter() + .zip(vals.into_iter()) + .into_iter() + .map_while(run_for_each_item) + .into_pipeline_data(ctrlc)), + // Errors + PipelineData::ListStream(..) => Err(ShellError::OnlySupportsThisInputType { + exp_input_type: "record".into(), + wrong_type: "stream".into(), + dst_span: call.head, + src_span: input_span, + }), + PipelineData::Value(Value::Error { error }, ..) => Err(*error), + PipelineData::Value(other, ..) => Err(ShellError::OnlySupportsThisInputType { + exp_input_type: "record".into(), + wrong_type: other.get_type().to_string(), + dst_span: call.head, + src_span: other.expect_span(), + }), + PipelineData::ExternalStream { .. } => Err(ShellError::OnlySupportsThisInputType { + exp_input_type: "record".into(), + wrong_type: "raw data".into(), + dst_span: call.head, + src_span: input_span, + }), + } + .map(|x| x.set_metadata(metadata)) + } + + fn examples(&self) -> Vec { + vec![Example { + example: + "{ new: york, san: francisco } | items {|key, value| echo $'($key) ($value)' }", + description: "Iterate over each key-value pair of a record", + result: Some(Value::List { + vals: vec![ + Value::test_string("new york"), + Value::test_string("san francisco"), + ], + span: Span::test_data(), + }), + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Items {}) + } +} diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs index bb7a58653..e5f742a6c 100644 --- a/crates/nu-command/src/filters/mod.rs +++ b/crates/nu-command/src/filters/mod.rs @@ -19,6 +19,7 @@ mod group; mod group_by; mod headers; mod insert; +mod items; mod join; mod last; mod length; @@ -75,6 +76,7 @@ pub use group::Group; pub use group_by::GroupBy; pub use headers::Headers; pub use insert::Insert; +pub use items::Items; pub use join::Join; pub use last::Last; pub use length::Length;