A new subcommand to str, str-expand. (#9290)

# Description

<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->

Description can be found [here:
https://github.com/nushell/nushell/discussions/9277#discussioncomment-5997793](https://github.com/nushell/nushell/discussions/9277#discussioncomment-5997793)

# User-Facing Changes

Again, examples can be found in the discussion #9277

<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

# Tests + Formatting

I've written tests that cover the changes I've made.

<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect -A clippy::result_large_err` to check that
you're using the standard code style
- `cargo test --workspace` to check that all tests pass
- `cargo run -- crates/nu-std/tests/run.nu` to run the tests for the
standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->

---------

Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
A. Taha Baki 2023-06-28 20:57:44 +03:00 committed by GitHub
parent 9068093081
commit d80abb20a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 199 additions and 0 deletions

7
Cargo.lock generated
View File

@ -360,6 +360,12 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bracoxide"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "218d42d1e9cdf071a7badff501a139dd6f598fe21348e5e5c4e2302e43bdcefb"
[[package]]
name = "brotli"
version = "3.3.4"
@ -2820,6 +2826,7 @@ dependencies = [
"alphanumeric-sort",
"atty",
"base64 0.21.2",
"bracoxide",
"byteorder",
"bytesize",
"calamine",

View File

@ -96,6 +96,7 @@ url = "2.2"
uuid = { version = "1.3", features = ["v4"] }
wax = { version = "0.5" }
which = { version = "4.4", optional = true }
bracoxide = "0.1.0"
[target.'cfg(windows)'.dependencies]
winreg = "0.50"

View File

@ -195,6 +195,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
StrDistance,
StrDowncase,
StrEndswith,
StrExpand,
StrJoin,
StrReplace,
StrIndexOf,

View File

@ -0,0 +1,188 @@
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
Category, Example, PipelineData, ShellError, Signature, Span, Type, Value,
};
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"str expand"
}
fn usage(&self) -> &str {
"Generates all possible combinations defined in brace expansion syntax."
}
fn extra_usage(&self) -> &str {
"This syntax may seem familiar with `glob {A,B}.C`. The difference is glob relies on filesystem, but str expand is not. Inside braces, we put variants. Then basically we're creating all possible outcomes."
}
fn signature(&self) -> Signature {
Signature::build("str expand")
.input_output_types(vec![(Type::String, Type::List(Box::new(Type::String)))])
.vectorizes_over_list(true)
.category(Category::Strings)
}
fn examples(&self) -> Vec<nu_protocol::Example> {
vec![
Example {
description: "Define a range inside braces to produce a list of string.",
example: "\"{3..5}\" | str expand",
result: Some(Value::List{
vals: vec![
Value::test_string("3"),
Value::test_string("4"),
Value::test_string("5")
],
span: Span::test_data()
},)
},
Example {
description: "Export comma separated values inside braces (`{}`) to a string list.",
example: "\"{apple,banana,cherry}\" | str expand",
result: Some(Value::List{
vals: vec![
Value::test_string("apple"),
Value::test_string("banana"),
Value::test_string("cherry")
],
span: Span::test_data()
},)
},
Example {
description: "Brace expressions can be used one after another.",
example: "\"A{b,c}D{e,f}G\" | str expand",
result: Some(Value::List{
vals: vec![
Value::test_string("AbDeG"),
Value::test_string("AbDfG"),
Value::test_string("AcDeG"),
Value::test_string("AcDfG"),
],
span: Span::test_data()
},)
},
Example {
description: "Also, it is possible to use one inside another. Here is a real-world example, that creates files:",
example: "\"A{B{1,3},C{2,5}}D\" | str expand",
result: Some(Value::List{
vals: vec![
Value::test_string("AB1D"),
Value::test_string("AB3D"),
Value::test_string("AC2D"),
Value::test_string("AC5D"),
],
span: Span::test_data()
},)
}
]
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let span = call.head;
if matches!(input, PipelineData::Empty) {
return Err(ShellError::PipelineEmpty { dst_span: span });
}
input.map(
move |v| {
let value_span = match v.span() {
Err(v) => return Value::Error { error: Box::new(v) },
Ok(v) => v,
};
match v.as_string() {
Ok(s) => str_expand(&s, span, v.expect_span()),
Err(_) => Value::Error {
error: Box::new(ShellError::PipelineMismatch {
exp_input_type: "string".into(),
dst_span: span,
src_span: value_span,
}),
},
}
},
engine_state.ctrlc.clone(),
)
}
}
fn str_expand(contents: &str, span: Span, value_span: Span) -> Value {
use bracoxide::{
expand,
parser::{parse, ParsingError},
tokenizer::{tokenize, TokenizationError},
};
match tokenize(contents) {
Ok(tokens) => {
match parse(&tokens) {
Ok(node) => {
match expand(&node) {
Ok(possibilities) => {
Value::List { vals: possibilities.iter().map(|e| Value::string(e,span)).collect::<Vec<Value>>(), span }
},
Err(e) => match e {
bracoxide::ExpansionError::NumConversionFailed(s) => Value::Error { error:
Box::new(ShellError::GenericError("Number Conversion Failed".to_owned(), format!("Number conversion failed at {s}."), Some(value_span), Some("Expected number, found text. Range format is `{M..N}`, where M and N are numeric values representing the starting and ending limits.".to_owned()), vec![])) },
},
}
},
Err(e) => Value::Error { error: Box::new(
match e {
ParsingError::NoTokens => ShellError::PipelineEmpty { dst_span: value_span },
ParsingError::OBraExpected(s) => ShellError::GenericError("Opening Brace Expected".to_owned(), format!("Opening brace is expected at {s}."), Some(value_span), Some("In brace syntax, we use equal amount of opening (`{`) and closing (`}`). Please, take a look at the examples.".to_owned()), vec![]),
ParsingError::CBraExpected(s) => ShellError::GenericError("Closing Brace Expected".to_owned(), format!("Closing brace is expected at {s}."), Some(value_span), Some("In brace syntax, we use equal amount of opening (`{`) and closing (`}`). Please, see the examples.".to_owned()), vec![]),
ParsingError::RangeStartLimitExpected(s) => ShellError::GenericError("Range Start Expected".to_owned(), format!("Range start limit is missing, expected at {s}."), Some(value_span), Some("In brace syntax, Range is defined like `{X..Y}`, where X and Y are a number. X is the start, Y is the end. Please, inspect the examples for more information.".to_owned()), vec![]),
ParsingError::RangeEndLimitExpected(s) => ShellError::GenericError("Range Start Expected".to_owned(), format!("Range start limit is missing, expected at {s}."), Some(value_span), Some("In brace syntax, Range is defined like `{X..Y}`, where X and Y are a number. X is the start, Y is the end. Please see the examples, for more information.".to_owned()), vec![]),
ParsingError::ExpectedText(s) => ShellError::GenericError("Expected Text".to_owned(), format!("Expected text at {s}."), Some(value_span), Some("Texts are only allowed before opening brace (`{`), after closing brace (`}`), or inside `{}`. Please take a look at the examples.".to_owned()), vec![]),
ParsingError::InvalidCommaUsage(s) => ShellError::GenericError("Invalid Comma Usage".to_owned(), format!("Found comma at {s}. Commas are only valid inside collection (`{{X,Y}}`)."), Some(value_span), Some("To escape comma use backslash `\\,`.".to_owned()), vec![]),
ParsingError::RangeCantHaveText(s) => ShellError::GenericError("Range Can not Have Text".to_owned(), format!("Expecting, brace, number, or range operator, but found text at {s}."), Some(value_span), Some("Please use the format {M..N} for ranges in brace expansion, where M and N are numeric values representing the starting and ending limits of the sequence, respectively.".to_owned()), vec![]),
ParsingError::ExtraRangeOperator(s) => ShellError::GenericError("Extra Range Operator".to_owned(), format!("Found additional, range operator at {s}."), Some(value_span), Some("Please, use the format `{M..N}` where M and N are numeric values representing the starting and ending limits of the range.".to_owned()), vec![]),
ParsingError::ExtraCBra(s) => ShellError::GenericError("Extra Closing Brace".to_owned(), format!("Used extra closing brace at {s}."), Some(value_span), Some("To escape closing brace use backslash, e.g. `\\}`".to_owned()), vec![]),
ParsingError::ExtraOBra(s) => ShellError::GenericError("Extra Opening Brace".to_owned(), format!("Used extra opening brace at {s}."), Some(value_span), Some("To escape opening brace use backslash, e.g. `\\{`".to_owned()), vec![]),
ParsingError::NothingInBraces(s) => ShellError::GenericError("Nothing In Braces".to_owned(), format!("Nothing found inside braces at {s}."), Some(value_span), Some("Please provide valid content within the braces. Additionally, you can safely remove it, not needed.".to_owned()), vec![]),
}
) }
}
},
Err(e) => match e {
TokenizationError::EmptyContent => Value::Error {
error: Box::new(ShellError::PipelineEmpty { dst_span: value_span }),
},
TokenizationError::FormatNotSupported => Value::Error {
error: Box::new(
ShellError::GenericError(
"Format Not Supported".to_owned(),
"Usage of only `{` or `}`. Brace Expansion syntax, needs to have equal amount of opening (`{`) and closing (`}`)".to_owned(),
Some(value_span),
Some("In brace expansion syntax, it is important to have an equal number of opening (`{`) and closing (`}`) braces. Please ensure that you provide a balanced pair of braces in your brace expansion pattern.".to_owned()),
vec![]
))
},
TokenizationError::NoBraces => Value::Error {
error: Box::new(ShellError::GenericError("No Braces".to_owned(), "At least one `{}` brace expansion expected.".to_owned(), Some(value_span), Some("Please, examine the examples.".to_owned()), vec![]))
}
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View File

@ -2,6 +2,7 @@ mod case;
mod contains;
mod distance;
mod ends_with;
mod expand;
mod index_of;
mod join;
mod length;
@ -15,6 +16,7 @@ pub use case::*;
pub use contains::SubCommand as StrContains;
pub use distance::SubCommand as StrDistance;
pub use ends_with::SubCommand as StrEndswith;
pub use expand::SubCommand as StrExpand;
pub use index_of::SubCommand as StrIndexOf;
pub use join::*;
pub use length::SubCommand as StrLength;