diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index cf67a2a091..5689afc0e9 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -62,6 +62,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { Each, Empty, Every, + Find, First, Flatten, Get, diff --git a/crates/nu-command/src/filters/find.rs b/crates/nu-command/src/filters/find.rs new file mode 100644 index 0000000000..25de200964 --- /dev/null +++ b/crates/nu-command/src/filters/find.rs @@ -0,0 +1,171 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::{ + ast::Call, + engine::{CaptureBlock, Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Find; + +impl Command for Find { + fn name(&self) -> &str { + "find" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .named( + "predicate", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "the predicate to satisfy", + Some('p'), + ) + .rest("rest", SyntaxShape::Any, "terms to search") + .category(Category::Filters) + } + + fn usage(&self) -> &str { + "Searches terms in the input or for elements of the input that satisfies the predicate." + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Search for multiple terms in a command output", + example: r#"ls | find toml md sh"#, + result: None, + }, + Example { + description: "Search for a term in a string", + example: r#"echo Cargo.toml | find toml"#, + result: Some(Value::test_string("Cargo.toml".to_owned())) + }, + Example { + description: "Search a number or a file size in a list of numbers", + example: r#"[1 5 3kb 4 3Mb] | find 5 3kb"#, + result: Some(Value::List { + vals: vec![Value::test_int(5), Value::test_filesize(3000)], + span: Span::test_data() + }), + }, + Example { + description: "Search a char in a list of string", + example: r#"[moe larry curly] | find l"#, + result: Some(Value::List { + vals: vec![Value::test_string("larry"), Value::test_string("curly")], + span: Span::test_data() + }) + }, + Example { + description: "Find the first odd value", + example: "echo [2 4 3 6 5 8] | find --predicate { ($it mod 2) == 1 }", + result: Some(Value::List { + vals: vec![Value::test_int(3), Value::test_int(5)], + span: Span::test_data() + }) + }, + Example { + description: "Find if a service is not running", + example: "echo [[version patch]; [0.1.0 $false] [0.1.1 $true] [0.2.0 $false]] | find -p { $it.patch }", + result: Some(Value::List { + vals: vec![Value::test_record( + vec!["version", "patch"], + vec![Value::test_string("0.1.1"), Value::test_bool(true)] + )], + span: Span::test_data() + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + let span = call.head; + + let ctrlc = engine_state.ctrlc.clone(); + let engine_state = engine_state.clone(); + + match call.get_flag::(&engine_state, stack, "predicate")? { + Some(predicate) => { + let capture_block = predicate; + let block_id = capture_block.block_id; + + if !call.rest::(&engine_state, stack, 0)?.is_empty() { + return Err(ShellError::IncompatibleParametersSingle( + "expected either a predicate or terms, not both".to_owned(), + span, + )); + } + + let block = engine_state.get_block(block_id).clone(); + let var_id = block.signature.get_positional(0).and_then(|arg| arg.var_id); + + let mut stack = stack.captures_to_stack(&capture_block.captures); + + input.filter( + move |value| { + if let Some(var_id) = var_id { + stack.add_var(var_id, value.clone()); + } + + eval_block(&engine_state, &mut stack, &block, PipelineData::new(span)) + .map_or(false, |pipeline_data| { + pipeline_data.into_value(span).is_true() + }) + }, + ctrlc, + ) + } + None => { + let terms = call.rest::(&engine_state, stack, 0)?; + input.filter( + move |value| { + terms.iter().any(|term| match value { + Value::Bool { .. } + | Value::Int { .. } + | Value::Filesize { .. } + | Value::Duration { .. } + | Value::Date { .. } + | Value::Range { .. } + | Value::Float { .. } + | Value::Block { .. } + | Value::Nothing { .. } + | Value::Error { .. } => { + value.eq(span, term).map_or(false, |value| value.is_true()) + } + Value::String { .. } + | Value::List { .. } + | Value::CellPath { .. } + | Value::CustomValue { .. } => term + .r#in(span, value) + .map_or(false, |value| value.is_true()), + Value::Record { vals, .. } => vals.iter().any(|val| { + term.r#in(span, val).map_or(false, |value| value.is_true()) + }), + Value::Binary { .. } => false, + }) + }, + ctrlc, + ) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Find) + } +} diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs index 64f84c071a..a58659b442 100644 --- a/crates/nu-command/src/filters/mod.rs +++ b/crates/nu-command/src/filters/mod.rs @@ -8,6 +8,7 @@ mod drop; mod each; mod empty; mod every; +mod find; mod first; mod flatten; mod get; @@ -45,6 +46,7 @@ pub use drop::*; pub use each::Each; pub use empty::Empty; pub use every::Every; +pub use find::Find; pub use first::First; pub use flatten::Flatten; pub use get::Get; diff --git a/crates/nu-protocol/src/ast/cell_path.rs b/crates/nu-protocol/src/ast/cell_path.rs index 078b64919f..e21c8216bd 100644 --- a/crates/nu-protocol/src/ast/cell_path.rs +++ b/crates/nu-protocol/src/ast/cell_path.rs @@ -8,6 +8,16 @@ pub enum PathMember { Int { val: usize, span: Span }, } +impl PartialEq for PathMember { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::String { val: l_val, .. }, Self::String { val: r_val, .. }) => l_val == r_val, + (Self::Int { val: l_val, .. }, Self::Int { val: r_val, .. }) => l_val == r_val, + _ => false, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CellPath { pub members: Vec, diff --git a/crates/nu-protocol/src/span.rs b/crates/nu-protocol/src/span.rs index 350962587b..3b383df0ed 100644 --- a/crates/nu-protocol/src/span.rs +++ b/crates/nu-protocol/src/span.rs @@ -31,7 +31,8 @@ impl Span { Span { start, end } } - /// Note: Only use this for test data, *not* live data, as it will point into unknown source when used in errors + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. pub fn test_data() -> Span { Span { start: 0, end: 0 } } diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index c0e7e6db58..c9d020ee7d 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -766,7 +766,8 @@ impl Value { Value::Bool { val, span } } - // Only use these for test data. Should not be used in user data + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. pub fn test_string(s: impl Into) -> Value { Value::String { val: s.into(), @@ -774,7 +775,8 @@ impl Value { } } - // Only use these for test data. Should not be used in user data + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. pub fn test_int(val: i64) -> Value { Value::Int { val, @@ -782,7 +784,8 @@ impl Value { } } - // Only use these for test data. Should not be used in user data + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. pub fn test_float(val: f64) -> Value { Value::Float { val, @@ -790,7 +793,8 @@ impl Value { } } - // Only use these for test data. Should not be used in user data + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. pub fn test_bool(val: bool) -> Value { Value::Bool { val, @@ -798,11 +802,30 @@ impl Value { } } - // Only use these for test data. Should not be used in user data + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. + pub fn test_filesize(val: i64) -> Value { + Value::Filesize { + val, + span: Span::test_data(), + } + } + + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. + pub fn test_nothing() -> Value { + Value::Nothing { + span: Span::test_data(), + } + } + + /// Note: Only use this for test data, *not* live data, as it will point into unknown source + /// when used in errors. pub fn test_record(cols: Vec>, vals: Vec) -> Value { Value::Record { cols: cols.into_iter().map(|s| s.into()).collect(), vals, + span: Span::test_data(), } } @@ -1323,6 +1346,32 @@ impl Value { val: rhs.contains(lhs), span, }), + (Value::String { .. } | Value::Int { .. }, Value::CellPath { val: rhs, .. }) => { + let val = rhs.members.iter().any(|member| match (self, member) { + (Value::Int { val: lhs, .. }, PathMember::Int { val: rhs, .. }) => { + *lhs == *rhs as i64 + } + (Value::String { val: lhs, .. }, PathMember::String { val: rhs, .. }) => { + lhs == rhs + } + (Value::String { .. }, PathMember::Int { .. }) + | (Value::Int { .. }, PathMember::String { .. }) => false, + _ => unreachable!( + "outer match arm ensures `self` is either a `String` or `Int` variant" + ), + }); + + Ok(Value::Bool { val, span }) + } + (Value::CellPath { val: lhs, .. }, Value::CellPath { val: rhs, .. }) => { + Ok(Value::Bool { + val: rhs + .members + .windows(lhs.members.len()) + .any(|member_window| member_window == rhs.members), + span, + }) + } (Value::CustomValue { val: lhs, span }, rhs) => { lhs.operation(*span, Operator::In, op, rhs) } @@ -1356,6 +1405,32 @@ impl Value { val: !rhs.contains(lhs), span, }), + (Value::String { .. } | Value::Int { .. }, Value::CellPath { val: rhs, .. }) => { + let val = rhs.members.iter().any(|member| match (self, member) { + (Value::Int { val: lhs, .. }, PathMember::Int { val: rhs, .. }) => { + *lhs != *rhs as i64 + } + (Value::String { val: lhs, .. }, PathMember::String { val: rhs, .. }) => { + lhs != rhs + } + (Value::String { .. }, PathMember::Int { .. }) + | (Value::Int { .. }, PathMember::String { .. }) => true, + _ => unreachable!( + "outer match arm ensures `self` is either a `String` or `Int` variant" + ), + }); + + Ok(Value::Bool { val, span }) + } + (Value::CellPath { val: lhs, .. }, Value::CellPath { val: rhs, .. }) => { + Ok(Value::Bool { + val: rhs + .members + .windows(lhs.members.len()) + .all(|member_window| member_window != rhs.members), + span, + }) + } (Value::CustomValue { val: lhs, span }, rhs) => { lhs.operation(*span, Operator::NotIn, op, rhs) }