From c37bdcd1191111a3048767b6cb02e29bfc73f005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan?= <71919805+onthebridgetonowhere@users.noreply.github.com> Date: Sun, 19 Dec 2021 20:11:57 +0100 Subject: [PATCH] port empty command (#528) * port empty command * Pull upstream and use test_data() function for example tests --- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/filters/empty.rs | 235 +++++++++++++++++++++++ crates/nu-command/src/filters/mod.rs | 2 + crates/nu-protocol/src/value/mod.rs | 15 ++ 4 files changed, 253 insertions(+) create mode 100644 crates/nu-command/src/filters/empty.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 9f42ed1953..c2ae18f587 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -54,6 +54,7 @@ pub fn create_default_context() -> EngineState { DropColumn, DropNth, Each, + Empty, First, Flatten, Get, diff --git a/crates/nu-command/src/filters/empty.rs b/crates/nu-command/src/filters/empty.rs new file mode 100644 index 0000000000..b0e454550f --- /dev/null +++ b/crates/nu-command/src/filters/empty.rs @@ -0,0 +1,235 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::{Call, CellPath}; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Empty; + +impl Command for Empty { + fn name(&self) -> &str { + "empty?" + } + + fn signature(&self) -> Signature { + Signature::build("empty") + .rest( + "rest", + SyntaxShape::CellPath, + "the names of the columns to check emptiness", + ) + .named( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "an optional block to replace if empty", + Some('b'), + ) + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Check for empty values." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + empty(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Check if a value is empty", + example: "'' | empty?", + result: Some(Value::Bool { + val: true, + span: Span::test_data(), + }), + }, + Example { + description: "more than one column", + example: "[[meal size]; [arepa small] [taco '']] | empty? meal size", + result: Some( + Value::List { + vals: vec![ + Value::Record{cols: vec!["meal".to_string(), "size".to_string()], vals: vec![ + Value::Bool{val: false, span: Span::test_data()}, + Value::Bool{val: false, span: Span::test_data()} + ], span: Span::test_data()}, + Value::Record{cols: vec!["meal".to_string(), "size".to_string()], vals: vec![ + Value::Bool{val: false, span: Span::test_data()}, + Value::Bool{val: true, span: Span::test_data()} + ], span: Span::test_data()} + ], span: Span::test_data() + }) + }, + Example { + description: "use a block if setting the empty cell contents is wanted", + example: "[[2020/04/16 2020/07/10 2020/11/16]; ['' [27] [37]]] | empty? 2020/04/16 -b { [33 37] }", + result: Some( + Value::List { + vals: vec![ + Value::Record{ + cols: vec!["2020/04/16".to_string(), "2020/07/10".to_string(), "2020/11/16".to_string()], + vals: vec![ + Value::List{vals: vec![ + Value::Int{val: 33, span: Span::test_data()}, + Value::Int{val: 37, span: Span::test_data()} + ], span: Span::test_data()}, + Value::List{vals: vec![ + Value::Int{val: 27, span: Span::test_data()}, + ], span: Span::test_data()}, + Value::List{vals: vec![ + Value::Int{val: 37, span: Span::test_data()}, + ], span: Span::test_data()}, + ], span: Span::test_data()} + ], span: Span::test_data() + } + ) + } + ] + } +} + +fn empty( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let head = call.head; + let columns: Vec = call.rest(engine_state, stack, 0)?; + let has_block = call.has_flag("block"); + + let block = if has_block { + let block_expr = call + .get_flag_expr("block") + .expect("internal error: expected block"); + + let block_id = block_expr + .as_block() + .ok_or_else(|| ShellError::TypeMismatch("expected row condition".to_owned(), head))?; + + let b = engine_state.get_block(block_id); + let evaluated_block = eval_block(engine_state, stack, b, PipelineData::new(head))?; + Some(evaluated_block.into_value(head)) + } else { + None + }; + + input.map( + move |value| { + let columns = columns.clone(); + + process_row(value, block.as_ref(), columns, head) + }, + engine_state.ctrlc.clone(), + ) +} + +fn process_row( + input: Value, + default_block: Option<&Value>, + column_paths: Vec, + head: Span, +) -> Value { + match input { + Value::Record { + cols: _, + ref vals, + span, + } => { + if column_paths.is_empty() { + let is_empty = vals.iter().all(|v| v.clone().is_empty()); + if default_block.is_some() { + if is_empty { + Value::Bool { val: true, span } + } else { + input.clone() + } + } else { + Value::Bool { + val: is_empty, + span, + } + } + } else { + let mut obj = input.clone(); + for column in column_paths.clone() { + let path = column.into_string(); + let data = input.get_data_by_key(&path); + let is_empty = match data { + Some(x) => x.is_empty(), + None => true, + }; + + let default = if let Some(x) = default_block { + if is_empty { + x.clone() + } else { + Value::Bool { val: true, span } + } + } else { + Value::Bool { + val: is_empty, + span, + } + }; + let r = obj.update_cell_path(&column.members, Box::new(move |_| default)); + if let Err(error) = r { + return Value::Error { error }; + } + } + obj + } + } + Value::List { vals, .. } if vals.iter().all(|v| v.as_record().is_ok()) => { + { + // we have records + if column_paths.is_empty() { + let is_empty = vals.is_empty() && vals.iter().all(|v| v.clone().is_empty()); + + Value::Bool { + val: is_empty, + span: head, + } + } else { + Value::Bool { + val: true, + span: head, + } + } + } + } + Value::List { vals, .. } => { + let empty = vals.iter().all(|v| v.clone().is_empty()); + Value::Bool { + val: empty, + span: head, + } + } + other => Value::Bool { + val: other.is_empty(), + span: head, + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Empty {}) + } +} diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs index 5c1cacb003..a4a42649a4 100644 --- a/crates/nu-command/src/filters/mod.rs +++ b/crates/nu-command/src/filters/mod.rs @@ -5,6 +5,7 @@ mod collect; mod columns; mod drop; mod each; +mod empty; mod first; mod flatten; mod get; @@ -34,6 +35,7 @@ pub use collect::Collect; pub use columns::Columns; pub use drop::*; pub use each::Each; +pub use empty::Empty; pub use first::First; pub use flatten::Flatten; pub use get::Get; diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index b062a013e8..9a3c97e734 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -463,6 +463,21 @@ impl Value { } } + /// Check if the content is empty + pub fn is_empty(self) -> bool { + match self { + Value::String { val, .. } => val.is_empty(), + Value::List { vals, .. } => { + vals.is_empty() && vals.iter().all(|v| v.clone().is_empty()) + } + Value::Record { cols, vals, .. } => { + cols.iter().all(|v| v.is_empty()) && vals.iter().all(|v| v.clone().is_empty()) + } + Value::Nothing { .. } => true, + _ => false, + } + } + /// Create a new `Nothing` value pub fn nothing(span: Span) -> Value { Value::Nothing { span }