Find with regex flag (#4649)

* split find functions

* find command with regex

* corrected message

* cargo fmt
This commit is contained in:
Fernando Herrera 2022-02-26 09:19:19 +00:00 committed by GitHub
parent 3eca43c0bb
commit 11bc056576
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 199 additions and 112 deletions

View File

@ -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<PipelineData, ShellError> {
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::<CaptureBlock>(engine_state, stack, "predicate")?;
let regex = call.get_flag::<String>(engine_state, stack, "regex")?;
let redirect_stdout = call.redirect_stdout;
let redirect_stderr = call.redirect_stderr;
match call.get_flag::<CaptureBlock>(&engine_state, stack, "predicate")? {
Some(predicate) => {
let capture_block = predicate;
let block_id = capture_block.block_id;
if !call.rest::<Value>(&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::<Value>(&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::<Vec<Value>>();
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<PipelineData, ShellError> {
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<PipelineData, ShellError> {
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::<Value>(&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<PipelineData, ShellError> {
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::<Value>(&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::<Vec<Value>>();
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::*;

View File

@ -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<String, ShellError> {
match self {
Value::Int { val, .. } => Ok(val.to_string()),