mirror of
https://github.com/nushell/nushell
synced 2024-12-27 05:23:11 +00:00
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:
parent
9068093081
commit
d80abb20a4
5 changed files with 199 additions and 0 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -360,6 +360,12 @@ dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bracoxide"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "218d42d1e9cdf071a7badff501a139dd6f598fe21348e5e5c4e2302e43bdcefb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "3.3.4"
|
version = "3.3.4"
|
||||||
|
@ -2820,6 +2826,7 @@ dependencies = [
|
||||||
"alphanumeric-sort",
|
"alphanumeric-sort",
|
||||||
"atty",
|
"atty",
|
||||||
"base64 0.21.2",
|
"base64 0.21.2",
|
||||||
|
"bracoxide",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bytesize",
|
"bytesize",
|
||||||
"calamine",
|
"calamine",
|
||||||
|
|
|
@ -96,6 +96,7 @@ url = "2.2"
|
||||||
uuid = { version = "1.3", features = ["v4"] }
|
uuid = { version = "1.3", features = ["v4"] }
|
||||||
wax = { version = "0.5" }
|
wax = { version = "0.5" }
|
||||||
which = { version = "4.4", optional = true }
|
which = { version = "4.4", optional = true }
|
||||||
|
bracoxide = "0.1.0"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.50"
|
winreg = "0.50"
|
||||||
|
|
|
@ -195,6 +195,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
||||||
StrDistance,
|
StrDistance,
|
||||||
StrDowncase,
|
StrDowncase,
|
||||||
StrEndswith,
|
StrEndswith,
|
||||||
|
StrExpand,
|
||||||
StrJoin,
|
StrJoin,
|
||||||
StrReplace,
|
StrReplace,
|
||||||
StrIndexOf,
|
StrIndexOf,
|
||||||
|
|
188
crates/nu-command/src/strings/str_/expand.rs
Normal file
188
crates/nu-command/src/strings/str_/expand.rs
Normal 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 {})
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ mod case;
|
||||||
mod contains;
|
mod contains;
|
||||||
mod distance;
|
mod distance;
|
||||||
mod ends_with;
|
mod ends_with;
|
||||||
|
mod expand;
|
||||||
mod index_of;
|
mod index_of;
|
||||||
mod join;
|
mod join;
|
||||||
mod length;
|
mod length;
|
||||||
|
@ -15,6 +16,7 @@ pub use case::*;
|
||||||
pub use contains::SubCommand as StrContains;
|
pub use contains::SubCommand as StrContains;
|
||||||
pub use distance::SubCommand as StrDistance;
|
pub use distance::SubCommand as StrDistance;
|
||||||
pub use ends_with::SubCommand as StrEndswith;
|
pub use ends_with::SubCommand as StrEndswith;
|
||||||
|
pub use expand::SubCommand as StrExpand;
|
||||||
pub use index_of::SubCommand as StrIndexOf;
|
pub use index_of::SubCommand as StrIndexOf;
|
||||||
pub use join::*;
|
pub use join::*;
|
||||||
pub use length::SubCommand as StrLength;
|
pub use length::SubCommand as StrLength;
|
||||||
|
|
Loading…
Reference in a new issue