diff --git a/Cargo.lock b/Cargo.lock index c7921be54c..b1b936da9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 0d4fb7c04c..92c18988c5 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -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" diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 6937ac121c..bc907bf23f 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -195,6 +195,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { StrDistance, StrDowncase, StrEndswith, + StrExpand, StrJoin, StrReplace, StrIndexOf, diff --git a/crates/nu-command/src/strings/str_/expand.rs b/crates/nu-command/src/strings/str_/expand.rs new file mode 100644 index 0000000000..48dceacfbc --- /dev/null +++ b/crates/nu-command/src/strings/str_/expand.rs @@ -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 { + 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 { + 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::>(), 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 {}) + } +} diff --git a/crates/nu-command/src/strings/str_/mod.rs b/crates/nu-command/src/strings/str_/mod.rs index e244144ac7..f246720c14 100644 --- a/crates/nu-command/src/strings/str_/mod.rs +++ b/crates/nu-command/src/strings/str_/mod.rs @@ -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;