Bring back parse as built-in.

This commit is contained in:
Andrés N. Robalino
2020-06-04 15:01:24 -05:00
parent 05959d6a61
commit 2a8ea88413
12 changed files with 182 additions and 237 deletions

View File

@ -291,6 +291,7 @@ pub fn create_default_context(
whole_stream_command(Lines),
whole_stream_command(Trim),
whole_stream_command(Echo),
whole_stream_command(Parse),
whole_stream_command(Str),
whole_stream_command(StrToDecimal),
whole_stream_command(StrToInteger),

View File

@ -74,6 +74,7 @@ pub(crate) mod mv;
pub(crate) mod next;
pub(crate) mod nth;
pub(crate) mod open;
pub(crate) mod parse;
pub(crate) mod pivot;
pub(crate) mod plugin;
pub(crate) mod prepend;
@ -203,6 +204,7 @@ pub(crate) use mv::Move;
pub(crate) use next::Next;
pub(crate) use nth::Nth;
pub(crate) use open::Open;
pub(crate) use parse::Parse;
pub(crate) use pivot::Pivot;
pub(crate) use prepend::Prepend;
pub(crate) use prev::Previous;

View File

@ -0,0 +1,175 @@
use crate::commands::WholeStreamCommand;
use crate::prelude::*;
use nu_errors::ShellError;
use nu_protocol::{Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value};
use nu_source::Tagged;
use regex::Regex;
#[derive(Deserialize)]
struct Arguments {
pattern: Tagged<String>,
regex: Tagged<bool>,
}
pub struct Command;
#[async_trait]
impl WholeStreamCommand for Command {
fn name(&self) -> &str {
"parse"
}
fn signature(&self) -> Signature {
Signature::build("parse")
.required(
"pattern",
SyntaxShape::String,
"the pattern to match. Eg) \"{foo}: {bar}\"",
)
.switch("regex", "use full regex syntax for patterns", Some('r'))
}
fn usage(&self) -> &str {
"Parse columns from string data using a simple pattern."
}
async fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
operate(args, registry).await
}
}
pub async fn operate(
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
let name_tag = args.call_info.name_tag.clone();
let (Arguments { regex, pattern }, mut input) = args.process(&registry).await?;
let regex_pattern = if let Tagged { item: true, tag } = regex {
Regex::new(&pattern.item)
.map_err(|_| ShellError::labeled_error("Invalid regex", "invalid regex", tag.span))?
} else {
let parse_regex = build_regex(&pattern.item, name_tag.clone())?;
Regex::new(&parse_regex).map_err(|_| {
ShellError::labeled_error("Invalid pattern", "invalid pattern", name_tag.span)
})?
};
let columns = column_names(&regex_pattern);
let mut parsed: VecDeque<Value> = VecDeque::new();
while let Some(v) = input.next().await {
match v.as_string() {
Ok(s) => {
let results = regex_pattern.captures_iter(&s);
for c in results {
let mut dict = TaggedDictBuilder::new(&v.tag);
for (column_name, cap) in columns.iter().zip(c.iter().skip(1)) {
let cap_string = cap.map(|v| v.as_str()).unwrap_or("").to_string();
dict.insert_untagged(column_name, UntaggedValue::string(cap_string));
}
parsed.push_back(dict.into_value());
}
}
Err(_) => {
return Err(ShellError::labeled_error_with_secondary(
"Expected string input",
"expected string input",
&name_tag,
"value originated here",
v.tag,
))
}
}
}
Ok(futures::stream::iter(parsed).to_output_stream())
}
fn build_regex(input: &str, tag: Tag) -> Result<String, ShellError> {
let mut output = "(?s)\\A".to_string();
//let mut loop_input = input;
let mut loop_input = input.chars().peekable();
loop {
let mut before = String::new();
while let Some(c) = loop_input.next() {
if c == '{' {
// If '{{', still creating a plaintext parse command, but just for a single '{' char
if loop_input.peek() == Some(&'{') {
let _ = loop_input.next();
} else {
break;
}
}
before.push(c);
}
if !before.is_empty() {
output.push_str(&regex::escape(&before));
}
// Look for column as we're now at one
let mut column = String::new();
while let Some(c) = loop_input.next() {
if c == '}' {
break;
}
column.push(c);
if loop_input.peek().is_none() {
return Err(ShellError::labeled_error(
"Found opening `{` without an associated closing `}`",
"invalid parse pattern",
tag,
));
}
}
if !column.is_empty() {
output.push_str("(?P<");
output.push_str(&column);
output.push_str(">.*?)");
}
if before.is_empty() && column.is_empty() {
break;
}
}
output.push_str("\\z");
Ok(output)
}
fn column_names(regex: &Regex) -> Vec<String> {
regex
.capture_names()
.enumerate()
.skip(1)
.map(|(i, name)| {
name.map(String::from)
.unwrap_or_else(|| format!("Capture{}", i))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::Command;
#[test]
fn examples_work_as_expected() {
use crate::examples::test as test_examples;
test_examples(Command {})
}
}

View File

@ -0,0 +1,3 @@
mod command;
pub use command::Command as Parse;

View File

@ -1,21 +0,0 @@
[package]
name = "nu_plugin_parse"
version = "0.14.1"
authors = ["The Nu Project Contributors"]
edition = "2018"
description = "A string parsing plugin for Nushell"
license = "MIT"
[lib]
doctest = false
[dependencies]
nu-plugin = { path = "../nu-plugin", version = "0.14.1" }
nu-protocol = { path = "../nu-protocol", version = "0.14.1" }
nu-source = { path = "../nu-source", version = "0.14.1" }
nu-errors = { path = "../nu-errors", version = "0.14.1" }
futures = { version = "0.3", features = ["compat", "io-compat"] }
regex = "1"
[build-dependencies]
nu-build = { version = "0.14.1", path = "../nu-build" }

View File

@ -1,3 +0,0 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
nu_build::build()
}

View File

@ -1,4 +0,0 @@
mod nu;
mod parse;
pub use parse::Parse;

View File

@ -1,7 +0,0 @@
use nu_plugin::serve_plugin;
use nu_plugin_parse::Parse;
fn main() -> Result<(), Box<dyn std::error::Error>> {
serve_plugin(&mut Parse::new()?);
Ok(())
}

View File

@ -1,159 +0,0 @@
use regex::{self, Regex};
use nu_errors::ShellError;
use nu_plugin::Plugin;
use nu_protocol::{
CallInfo, Primitive, ReturnSuccess, ReturnValue, ShellTypeName, Signature, SyntaxShape,
TaggedDictBuilder, UntaggedValue, Value,
};
use nu_source::Tag;
use crate::Parse;
impl Plugin for Parse {
fn config(&mut self) -> Result<Signature, ShellError> {
Ok(Signature::build("parse")
.switch("regex", "use full regex syntax for patterns", Some('r'))
.required(
"pattern",
SyntaxShape::String,
"the pattern to match. Eg) \"{foo}: {bar}\"",
)
.filter())
}
fn begin_filter(&mut self, call_info: CallInfo) -> Result<Vec<ReturnValue>, ShellError> {
if let Some(ref args) = call_info.args.positional {
let value = &args[0];
match value {
Value {
value: UntaggedValue::Primitive(Primitive::String(s)),
tag,
} => {
self.pattern_tag = tag.clone();
self.regex = if call_info.args.has("regex") {
Regex::new(&s).map_err(|_| {
ShellError::labeled_error("Invalid regex", "invalid regex", tag.span)
})?
} else {
let parse_regex = build_regex(&s, tag.clone())?;
Regex::new(&parse_regex).map_err(|_| {
ShellError::labeled_error(
"Invalid pattern",
"invalid pattern",
tag.span,
)
})?
};
self.column_names = column_names(&self.regex);
}
Value { tag, .. } => {
return Err(ShellError::labeled_error(
format!(
"Unexpected type in params (found `{}`, expected `String`)",
value.type_name()
),
"unexpected type",
tag,
));
}
}
}
Ok(vec![])
}
fn filter(&mut self, input: Value) -> Result<Vec<ReturnValue>, ShellError> {
if let Ok(s) = input.as_string() {
Ok(self
.regex
.captures_iter(&s)
.map(|caps| {
let mut dict = TaggedDictBuilder::new(&input.tag);
for (column_name, cap) in self.column_names.iter().zip(caps.iter().skip(1)) {
let cap_string = cap.map(|v| v.as_str()).unwrap_or("").to_string();
dict.insert_untagged(column_name, UntaggedValue::string(cap_string));
}
Ok(ReturnSuccess::Value(dict.into_value()))
})
.collect())
} else {
Err(ShellError::labeled_error_with_secondary(
"Expected string input",
"expected string input",
&self.name,
"value originated here",
input.tag,
))
}
}
}
fn build_regex(input: &str, tag: Tag) -> Result<String, ShellError> {
let mut output = "(?s)\\A".to_string();
//let mut loop_input = input;
let mut loop_input = input.chars().peekable();
loop {
let mut before = String::new();
while let Some(c) = loop_input.next() {
if c == '{' {
// If '{{', still creating a plaintext parse command, but just for a single '{' char
if loop_input.peek() == Some(&'{') {
let _ = loop_input.next();
} else {
break;
}
}
before.push(c);
}
if !before.is_empty() {
output.push_str(&regex::escape(&before));
}
// Look for column as we're now at one
let mut column = String::new();
while let Some(c) = loop_input.next() {
if c == '}' {
break;
}
column.push(c);
if loop_input.peek().is_none() {
return Err(ShellError::labeled_error(
"Found opening `{` without an associated closing `}`",
"invalid parse pattern",
tag,
));
}
}
if !column.is_empty() {
output.push_str("(?P<");
output.push_str(&column);
output.push_str(">.*?)");
}
if before.is_empty() && column.is_empty() {
break;
}
}
output.push_str("\\z");
Ok(output)
}
fn column_names(regex: &Regex) -> Vec<String> {
regex
.capture_names()
.enumerate()
.skip(1)
.map(|(i, name)| {
name.map(String::from)
.unwrap_or_else(|| format!("Capture{}", i))
})
.collect()
}

View File

@ -1,21 +0,0 @@
use nu_source::Tag;
use regex::Regex;
pub struct Parse {
pub regex: Regex,
pub name: Tag,
pub pattern_tag: Tag,
pub column_names: Vec<String>,
}
impl Parse {
#[allow(clippy::trivial_regex)]
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
Ok(Parse {
regex: Regex::new("")?,
name: Tag::unknown(),
pattern_tag: Tag::unknown(),
column_names: vec![],
})
}
}