forked from extern/nushell
str substring additions. (#2140)
This commit is contained in:
parent
28be39494c
commit
0fdb9ac5e2
@ -5,14 +5,16 @@ use nu_protocol::ShellTypeName;
|
||||
use nu_protocol::{
|
||||
ColumnPath, Primitive, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value,
|
||||
};
|
||||
use nu_source::{Tag, Tagged};
|
||||
use nu_value_ext::ValueExt;
|
||||
use nu_source::Tag;
|
||||
use nu_value_ext::{as_string, ValueExt};
|
||||
|
||||
use std::cmp;
|
||||
use std::cmp::Ordering;
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Arguments {
|
||||
range: Tagged<String>,
|
||||
range: Value,
|
||||
rest: Vec<ColumnPath>,
|
||||
}
|
||||
|
||||
@ -28,8 +30,8 @@ impl WholeStreamCommand for SubCommand {
|
||||
Signature::build("str substring")
|
||||
.required(
|
||||
"range",
|
||||
SyntaxShape::String,
|
||||
"the indexes to substring \"start, end\"",
|
||||
SyntaxShape::Any,
|
||||
"the indexes to substring [start end]",
|
||||
)
|
||||
.rest(
|
||||
SyntaxShape::ColumnPath,
|
||||
@ -53,9 +55,19 @@ impl WholeStreamCommand for SubCommand {
|
||||
vec![
|
||||
Example {
|
||||
description: "Get a substring from the text",
|
||||
example: "echo 'good nushell' | str substring [5 12]",
|
||||
result: Some(vec![Value::from("nushell")]),
|
||||
},
|
||||
Example {
|
||||
description: "Alternatively, you can use the form",
|
||||
example: "echo 'good nushell' | str substring '5,12'",
|
||||
result: Some(vec![Value::from("nushell")]),
|
||||
},
|
||||
Example {
|
||||
description: "Get the last characters from the string",
|
||||
example: "echo 'good nushell' | str substring ',-5'",
|
||||
result: Some(vec![Value::from("shell")]),
|
||||
},
|
||||
Example {
|
||||
description: "Get the remaining characters from a starting index",
|
||||
example: "echo 'good nushell' | str substring '5,'",
|
||||
@ -71,7 +83,15 @@ impl WholeStreamCommand for SubCommand {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Substring(usize, usize);
|
||||
struct Substring(isize, isize);
|
||||
|
||||
impl From<(isize, isize)> for Substring {
|
||||
fn from(input: (isize, isize)) -> Substring {
|
||||
Substring(input.0, input.1)
|
||||
}
|
||||
}
|
||||
|
||||
struct SubstringText(String, String);
|
||||
|
||||
async fn operate(
|
||||
args: CommandArgs,
|
||||
@ -82,41 +102,8 @@ async fn operate(
|
||||
|
||||
let (Arguments { range, rest }, input) = args.process(®istry).await?;
|
||||
|
||||
let v: Vec<&str> = range.item.split(',').collect();
|
||||
|
||||
let start = match v[0] {
|
||||
"" => 0,
|
||||
_ => v[0].trim().parse().map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
name.span,
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
let end = match v[1] {
|
||||
"" => usize::max_value(),
|
||||
_ => v[1].trim().parse().map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
name.span,
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
if start > end {
|
||||
return Err(ShellError::labeled_error(
|
||||
"End must be greater than or equal to Start",
|
||||
"End must be greater than or equal to Start",
|
||||
name.span,
|
||||
));
|
||||
}
|
||||
|
||||
let options = Substring(start, end);
|
||||
|
||||
let column_paths: Vec<_> = rest;
|
||||
let options = process_arguments(range, name)?.into();
|
||||
|
||||
Ok(input
|
||||
.map(move |v| {
|
||||
@ -153,35 +140,187 @@ async fn operate(
|
||||
}
|
||||
|
||||
fn action(input: &Value, options: &Substring, tag: impl Into<Tag>) -> Result<Value, ShellError> {
|
||||
let tag = tag.into();
|
||||
|
||||
match &input.value {
|
||||
UntaggedValue::Primitive(Primitive::Line(s))
|
||||
| UntaggedValue::Primitive(Primitive::String(s)) => {
|
||||
let start = options.0;
|
||||
let end: usize = cmp::min(options.1, s.len());
|
||||
let len: isize = s.len().try_into().map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
tag.span,
|
||||
)
|
||||
})?;
|
||||
|
||||
let out = {
|
||||
if start > s.len() - 1 {
|
||||
UntaggedValue::string("")
|
||||
} else {
|
||||
UntaggedValue::string(
|
||||
s.chars().skip(start).take(end - start).collect::<String>(),
|
||||
)
|
||||
let start: isize = options.0.try_into().map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
tag.span,
|
||||
)
|
||||
})?;
|
||||
|
||||
let end = options.1;
|
||||
|
||||
if start < len && end >= 0 {
|
||||
match start.cmp(&end) {
|
||||
Ordering::Equal => Ok(UntaggedValue::string("").into_value(tag)),
|
||||
Ordering::Greater => Err(ShellError::labeled_error(
|
||||
"End must be greater than or equal to Start",
|
||||
"End must be greater than or equal to Start",
|
||||
tag.span,
|
||||
)),
|
||||
Ordering::Less => {
|
||||
let end: isize = cmp::min(options.1, len);
|
||||
|
||||
Ok(UntaggedValue::string(
|
||||
s.chars()
|
||||
.skip(start as usize)
|
||||
.take((end - start) as usize)
|
||||
.collect::<String>(),
|
||||
)
|
||||
.into_value(tag))
|
||||
}
|
||||
}
|
||||
};
|
||||
} else if start >= 0 && end <= 0 {
|
||||
let end = options.1.abs();
|
||||
let reversed = s
|
||||
.chars()
|
||||
.skip(start as usize)
|
||||
.take((len - start) as usize)
|
||||
.collect::<String>();
|
||||
|
||||
Ok(out.into_value(tag))
|
||||
let reversed = if start == 0 {
|
||||
reversed
|
||||
} else {
|
||||
s.chars().take(start as usize).collect::<String>()
|
||||
};
|
||||
|
||||
let reversed = reversed
|
||||
.chars()
|
||||
.rev()
|
||||
.take(end as usize)
|
||||
.collect::<String>();
|
||||
|
||||
Ok(
|
||||
UntaggedValue::string(reversed.chars().rev().collect::<String>())
|
||||
.into_value(tag),
|
||||
)
|
||||
} else {
|
||||
Ok(UntaggedValue::string("").into_value(tag))
|
||||
}
|
||||
}
|
||||
other => {
|
||||
let got = format!("got {}", other.type_name());
|
||||
Err(ShellError::labeled_error(
|
||||
"value is not string",
|
||||
got,
|
||||
tag.into().span,
|
||||
tag.span,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_arguments(range: Value, name: impl Into<Tag>) -> Result<(isize, isize), ShellError> {
|
||||
let name = name.into();
|
||||
|
||||
let search = match &range.value {
|
||||
UntaggedValue::Table(indexes) => {
|
||||
if indexes.len() > 2 {
|
||||
Err(ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
name.span,
|
||||
))
|
||||
} else {
|
||||
let idx: Vec<String> = indexes
|
||||
.iter()
|
||||
.map(|v| as_string(v).unwrap_or_else(|_| String::from("")))
|
||||
.collect();
|
||||
|
||||
let start = idx
|
||||
.get(0)
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
name.span,
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
let end = idx
|
||||
.get(1)
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
name.span,
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
Ok(SubstringText(start, end))
|
||||
}
|
||||
}
|
||||
UntaggedValue::Primitive(Primitive::String(indexes)) => {
|
||||
let idx: Vec<&str> = indexes.split(',').collect();
|
||||
|
||||
let start = idx
|
||||
.get(0)
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
name.span,
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
let end = idx
|
||||
.get(1)
|
||||
.ok_or_else(|| {
|
||||
ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
name.span,
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
Ok(SubstringText(start, end))
|
||||
}
|
||||
_ => Err(ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
name.span,
|
||||
)),
|
||||
}?;
|
||||
|
||||
let start = match &search {
|
||||
SubstringText(start, _) if start == "" || start == "_" => 0,
|
||||
SubstringText(start, _) => start.trim().parse().map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
name.span,
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
let end = match &search {
|
||||
SubstringText(_, end) if end == "" || end == "_" => isize::max_value(),
|
||||
SubstringText(_, end) => end.trim().parse().map_err(|_| {
|
||||
ShellError::labeled_error(
|
||||
"could not perform substring",
|
||||
"could not perform substring",
|
||||
name.span,
|
||||
)
|
||||
})?,
|
||||
};
|
||||
|
||||
Ok((start, end))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{action, SubCommand, Substring};
|
||||
@ -195,14 +334,55 @@ mod tests {
|
||||
test_examples(SubCommand {})
|
||||
}
|
||||
|
||||
struct Expectation<'a> {
|
||||
options: (isize, isize),
|
||||
expected: &'a str,
|
||||
}
|
||||
|
||||
impl Expectation<'_> {
|
||||
fn options(&self) -> Substring {
|
||||
Substring(self.options.0, self.options.1)
|
||||
}
|
||||
}
|
||||
|
||||
fn expectation(word: &str, indexes: (isize, isize)) -> Expectation {
|
||||
Expectation {
|
||||
options: indexes,
|
||||
expected: word,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn given_start_and_end_indexes() {
|
||||
let word = string("andresS");
|
||||
let expected = string("andres");
|
||||
fn substrings_indexes() {
|
||||
let word = string("andres");
|
||||
|
||||
let substring_options = Substring(0, 6);
|
||||
let cases = vec![
|
||||
expectation("a", (0, 1)),
|
||||
expectation("an", (0, 2)),
|
||||
expectation("and", (0, 3)),
|
||||
expectation("andr", (0, 4)),
|
||||
expectation("andre", (0, 5)),
|
||||
expectation("andres", (0, 6)),
|
||||
expectation("andres", (0, -6)),
|
||||
expectation("ndres", (0, -5)),
|
||||
expectation("dres", (0, -4)),
|
||||
expectation("res", (0, -3)),
|
||||
expectation("es", (0, -2)),
|
||||
expectation("s", (0, -1)),
|
||||
expectation("", (6, 0)),
|
||||
expectation("s", (6, -1)),
|
||||
expectation("es", (6, -2)),
|
||||
expectation("res", (6, -3)),
|
||||
expectation("dres", (6, -4)),
|
||||
expectation("ndres", (6, -5)),
|
||||
expectation("andres", (6, -6)),
|
||||
];
|
||||
|
||||
let actual = action(&word, &substring_options, Tag::unknown()).unwrap();
|
||||
assert_eq!(actual, expected);
|
||||
for expectation in cases.iter() {
|
||||
let expected = expectation.expected;
|
||||
let actual = action(&word, &expectation.options(), Tag::unknown()).unwrap();
|
||||
|
||||
assert_eq!(actual, string(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user