diff --git a/crates/nu-command/src/filters/find.rs b/crates/nu-command/src/filters/find.rs index 419c25b97b..b0aa285149 100644 --- a/crates/nu-command/src/filters/find.rs +++ b/crates/nu-command/src/filters/find.rs @@ -5,6 +5,7 @@ use nu_protocol::{ Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, }; +use regex::Regex; #[derive(Clone)] pub struct Find; @@ -22,6 +23,12 @@ impl Command for Find { "the predicate to satisfy", Some('p'), ) + .named( + "regex", + SyntaxShape::String, + "regex to match with", + Some('r'), + ) .rest("rest", SyntaxShape::Any, "terms to search") .category(Category::Filters) } @@ -59,8 +66,8 @@ impl Command for Find { }) }, Example { - description: "Find the first odd value", - example: "echo [2 4 3 6 5 8] | find --predicate { |it| ($it mod 2) == 1 }", + description: "Find odd values", + example: "[2 4 3 6 5 8] | find --predicate { |it| ($it mod 2) == 1 }", result: Some(Value::List { vals: vec![Value::test_int(3), Value::test_int(5)], span: Span::test_data() @@ -68,7 +75,7 @@ impl Command for Find { }, 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| $it.patch }", + example: "[[version patch]; [0.1.0 $false] [0.1.1 $true] [0.2.0 $false]] | find -p { |it| $it.patch }", result: Some(Value::List { vals: vec![Value::test_record( vec!["version", "patch"], @@ -77,6 +84,33 @@ impl Command for Find { span: Span::test_data() }), }, + Example { + description: "Find using regex", + example: r#"[abc bde arc abf] | find --regex "ab""#, + result: Some(Value::List { + vals: vec![Value::test_string("abc".to_string()), Value::test_string("abf".to_string())], + span: Span::test_data() + }) + }, + Example { + description: "Find using regex case insensitive", + example: r#"[aBc bde Arc abf] | find --regex "(?i)ab""#, + result: Some(Value::List { + vals: vec![Value::test_string("aBc".to_string()), Value::test_string("abf".to_string())], + span: Span::test_data() + }) + }, + Example { + description: "Find value in records", + example: r#"[[version name]; [0.1.0 nushell] [0.1.1 fish] [0.2.0 zsh]] | find -r "nu""#, + result: Some(Value::List { + vals: vec![Value::test_record( + vec!["version", "name"], + vec![Value::test_string("0.1.0"), Value::test_string("nushell".to_string())] + )], + span: Span::test_data() + }), + }, ] } @@ -87,121 +121,173 @@ impl Command for Find { call: &Call, input: PipelineData, ) -> Result { - let span = call.head; - let ctrlc = engine_state.ctrlc.clone(); - let engine_state = engine_state.clone(); - let metadata = input.metadata(); - let config = stack.get_config()?; + let predicate = call.get_flag::(engine_state, stack, "predicate")?; + let regex = call.get_flag::(engine_state, stack, "regex")?; - let redirect_stdout = call.redirect_stdout; - let redirect_stderr = call.redirect_stderr; - - 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_with_metadata(metadata.clone(), span), - redirect_stdout, - redirect_stderr, - ) - .map_or(false, |pipeline_data| { - pipeline_data.into_value(span).is_true() - }) - }, - ctrlc, - ) - } - None => { - let terms = call.rest::(&engine_state, stack, 0)?; - let lower_terms = terms - .iter() - .map(|v| { - if let Ok(span) = v.span() { - Value::string(v.into_string("", &config).to_lowercase(), span) - } else { - v.clone() - } - }) - .collect::>(); - - let pipe = input.filter( - move |value| { - let lower_value = if let Ok(span) = value.span() { - Value::string(value.into_string("", &config).to_lowercase(), span) - } else { - value.clone() - }; - lower_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 { .. } => lower_value - .eq(span, term) - .map_or(false, |value| value.is_true()), - Value::String { .. } - | Value::List { .. } - | Value::CellPath { .. } - | Value::CustomValue { .. } => term - .r#in(span, &lower_value) - .map_or(false, |value| value.is_true()), - Value::Record { vals, .. } => vals.iter().any(|val| { - if let Ok(span) = val.span() { - let lower_val = Value::string( - val.into_string("", &config).to_lowercase(), - Span::test_data(), - ); - - term.r#in(span, &lower_val) - .map_or(false, |value| value.is_true()) - } else { - term.r#in(span, val).map_or(false, |value| value.is_true()) - } - }), - Value::Binary { .. } => false, - }) - }, - ctrlc, - )?; - match metadata { - Some(m) => { - Ok(pipe.into_pipeline_data_with_metadata(m, engine_state.ctrlc.clone())) - } - None => Ok(pipe), - } + match (regex, predicate) { + (None, Some(predicate)) => { + find_with_predicate(predicate, engine_state, stack, call, input) } + (Some(regex), None) => find_with_regex(regex, engine_state, stack, call, input), + (None, None) => find_with_rest(engine_state, stack, call, input), + (Some(_), Some(_)) => Err(ShellError::IncompatibleParametersSingle( + "expected either predicate or regex flag, not both".to_owned(), + call.head, + )), } } } +fn find_with_regex( + regex: String, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let span = call.head; + let ctrlc = engine_state.ctrlc.clone(); + let config = stack.get_config()?; + + let re = Regex::new(regex.as_str()) + .map_err(|e| ShellError::UnsupportedInput(format!("incorrect regex: {}", e), span))?; + + input.filter( + move |value| { + let string = value.into_string(" ", &config); + re.is_match(string.as_str()) + }, + ctrlc, + ) +} + +fn find_with_predicate( + predicate: CaptureBlock, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let span = call.head; + let ctrlc = engine_state.ctrlc.clone(); + let metadata = input.metadata(); + let redirect_stdout = call.redirect_stdout; + let redirect_stderr = call.redirect_stderr; + let engine_state = engine_state.clone(); + + 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_with_metadata(metadata.clone(), span), + redirect_stdout, + redirect_stderr, + ) + .map_or(false, |pipeline_data| { + pipeline_data.into_value(span).is_true() + }) + }, + ctrlc, + ) +} + +fn find_with_rest( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let span = call.head; + let ctrlc = engine_state.ctrlc.clone(); + let metadata = input.metadata(); + let engine_state = engine_state.clone(); + let config = stack.get_config()?; + + let terms = call.rest::(&engine_state, stack, 0)?; + let lower_terms = terms + .iter() + .map(|v| { + if let Ok(span) = v.span() { + Value::string(v.into_string("", &config).to_lowercase(), span) + } else { + v.clone() + } + }) + .collect::>(); + + let pipe = input.filter( + move |value| { + let lower_value = if let Ok(span) = value.span() { + Value::string(value.into_string("", &config).to_lowercase(), span) + } else { + value.clone() + }; + + lower_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 { .. } => lower_value + .eq(span, term) + .map_or(false, |value| value.is_true()), + Value::String { .. } + | Value::List { .. } + | Value::CellPath { .. } + | Value::CustomValue { .. } => term + .r#in(span, &lower_value) + .map_or(false, |value| value.is_true()), + Value::Record { vals, .. } => vals.iter().any(|val| { + if let Ok(span) = val.span() { + let lower_val = Value::string( + val.into_string("", &config).to_lowercase(), + Span::test_data(), + ); + + term.r#in(span, &lower_val) + .map_or(false, |value| value.is_true()) + } else { + term.r#in(span, val).map_or(false, |value| value.is_true()) + } + }), + Value::Binary { .. } => false, + }) + }, + ctrlc, + )?; + + match metadata { + Some(m) => Ok(pipe.into_pipeline_data_with_metadata(m, engine_state.ctrlc.clone())), + None => Ok(pipe), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 6ac65b0609..b3ecc98b17 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -171,6 +171,7 @@ impl Clone for Value { } impl Value { + /// Converts into string values that can be changed into string natively pub fn as_string(&self) -> Result { match self { Value::Int { val, .. } => Ok(val.to_string()),