nu_command/strings/str_/
expand.rs

1use nu_engine::command_prelude::*;
2
3#[derive(Clone)]
4pub struct StrExpand;
5
6impl Command for StrExpand {
7    fn name(&self) -> &str {
8        "str expand"
9    }
10
11    fn description(&self) -> &str {
12        "Generates all possible combinations defined in brace expansion syntax."
13    }
14
15    fn extra_description(&self) -> &str {
16        "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."
17    }
18
19    fn signature(&self) -> Signature {
20        Signature::build("str expand")
21            .input_output_types(vec![
22                (Type::String, Type::List(Box::new(Type::String))),
23                (
24                    Type::List(Box::new(Type::String)),
25                    Type::List(Box::new(Type::List(Box::new(Type::String)))),
26                ),
27            ])
28            .switch(
29                "path",
30                "Replaces all backslashes with double backslashes, useful for Path.",
31                None,
32            )
33            .allow_variants_without_examples(true)
34            .category(Category::Strings)
35    }
36
37    fn examples(&self) -> Vec<nu_protocol::Example> {
38        vec![
39            Example {
40                description: "Define a range inside braces to produce a list of string.",
41                example: "\"{3..5}\" | str expand",
42                result: Some(Value::list(
43                    vec![
44                        Value::test_string("3"),
45                        Value::test_string("4"),
46                        Value::test_string("5"),
47                    ],
48                    Span::test_data(),
49                )),
50            },
51            Example {
52                description: "Ignore the next character after the backslash ('\\')",
53                example: "'A{B\\,,C}' | str expand",
54                result: Some(Value::list(
55                    vec![Value::test_string("AB,"), Value::test_string("AC")],
56                    Span::test_data(),
57                )),
58            },
59            Example {
60                description: "Commas that are not inside any braces need to be skipped.",
61                example: "'Welcome\\, {home,mon ami}!' | str expand",
62                result: Some(Value::list(
63                    vec![
64                        Value::test_string("Welcome, home!"),
65                        Value::test_string("Welcome, mon ami!"),
66                    ],
67                    Span::test_data(),
68                )),
69            },
70            Example {
71                description: "Use double backslashes to add a backslash.",
72                example: "'A{B\\\\,C}' | str expand",
73                result: Some(Value::list(
74                    vec![Value::test_string("AB\\"), Value::test_string("AC")],
75                    Span::test_data(),
76                )),
77            },
78            Example {
79                description: "Export comma separated values inside braces (`{}`) to a string list.",
80                example: "\"{apple,banana,cherry}\" | str expand",
81                result: Some(Value::list(
82                    vec![
83                        Value::test_string("apple"),
84                        Value::test_string("banana"),
85                        Value::test_string("cherry"),
86                    ],
87                    Span::test_data(),
88                )),
89            },
90            Example {
91                description: "If the piped data is path, you may want to use --path flag, or else manually replace the backslashes with double backslashes.",
92                example: "'C:\\{Users,Windows}' | str expand --path",
93                result: Some(Value::list(
94                    vec![
95                        Value::test_string("C:\\Users"),
96                        Value::test_string("C:\\Windows"),
97                    ],
98                    Span::test_data(),
99                )),
100            },
101            Example {
102                description: "Brace expressions can be used one after another.",
103                example: "\"A{b,c}D{e,f}G\" | str expand",
104                result: Some(Value::list(
105                    vec![
106                        Value::test_string("AbDeG"),
107                        Value::test_string("AbDfG"),
108                        Value::test_string("AcDeG"),
109                        Value::test_string("AcDfG"),
110                    ],
111                    Span::test_data(),
112                )),
113            },
114            Example {
115                description: "Collection may include an empty item. It can be put at the start of the list.",
116                example: "\"A{,B,C}\" | str expand",
117                result: Some(Value::list(
118                    vec![
119                        Value::test_string("A"),
120                        Value::test_string("AB"),
121                        Value::test_string("AC"),
122                    ],
123                    Span::test_data(),
124                )),
125            },
126            Example {
127                description: "Empty item can be at the end of the collection.",
128                example: "\"A{B,C,}\" | str expand",
129                result: Some(Value::list(
130                    vec![
131                        Value::test_string("AB"),
132                        Value::test_string("AC"),
133                        Value::test_string("A"),
134                    ],
135                    Span::test_data(),
136                )),
137            },
138            Example {
139                description: "Empty item can be in the middle of the collection.",
140                example: "\"A{B,,C}\" | str expand",
141                result: Some(Value::list(
142                    vec![
143                        Value::test_string("AB"),
144                        Value::test_string("A"),
145                        Value::test_string("AC"),
146                    ],
147                    Span::test_data(),
148                )),
149            },
150            Example {
151                description: "Also, it is possible to use one inside another. Here is a real-world example, that creates files:",
152                example: "\"A{B{1,3},C{2,5}}D\" | str expand",
153                result: Some(Value::list(
154                    vec![
155                        Value::test_string("AB1D"),
156                        Value::test_string("AB3D"),
157                        Value::test_string("AC2D"),
158                        Value::test_string("AC5D"),
159                    ],
160                    Span::test_data(),
161                )),
162            },
163            Example {
164                description: "Supports zero padding in numeric ranges.",
165                example: "\"A{08..10}B{11..013}C\" | str expand",
166                result: Some(Value::list(
167                    vec![
168                        Value::test_string("A08B011C"),
169                        Value::test_string("A08B012C"),
170                        Value::test_string("A08B013C"),
171                        Value::test_string("A09B011C"),
172                        Value::test_string("A09B012C"),
173                        Value::test_string("A09B013C"),
174                        Value::test_string("A10B011C"),
175                        Value::test_string("A10B012C"),
176                        Value::test_string("A10B013C"),
177                    ],
178                    Span::test_data(),
179                )),
180            },
181        ]
182    }
183
184    fn is_const(&self) -> bool {
185        true
186    }
187
188    fn run(
189        &self,
190        engine_state: &EngineState,
191        stack: &mut Stack,
192        call: &Call,
193        input: PipelineData,
194    ) -> Result<PipelineData, ShellError> {
195        let is_path = call.has_flag(engine_state, stack, "path")?;
196        run(call, input, is_path, engine_state)
197    }
198
199    fn run_const(
200        &self,
201        working_set: &StateWorkingSet,
202        call: &Call,
203        input: PipelineData,
204    ) -> Result<PipelineData, ShellError> {
205        let is_path = call.has_flag_const(working_set, "path")?;
206        run(call, input, is_path, working_set.permanent())
207    }
208}
209
210fn run(
211    call: &Call,
212    input: PipelineData,
213    is_path: bool,
214    engine_state: &EngineState,
215) -> Result<PipelineData, ShellError> {
216    let span = call.head;
217    if matches!(input, PipelineData::Empty) {
218        return Err(ShellError::PipelineEmpty { dst_span: span });
219    }
220    input.map(
221        move |v| {
222            let value_span = v.span();
223            let type_ = v.get_type();
224            match v.coerce_into_string() {
225                Ok(s) => {
226                    let contents = if is_path { s.replace('\\', "\\\\") } else { s };
227                    str_expand(&contents, span, value_span)
228                }
229                Err(_) => Value::error(
230                    ShellError::OnlySupportsThisInputType {
231                        exp_input_type: "string".into(),
232                        wrong_type: type_.to_string(),
233                        dst_span: span,
234                        src_span: value_span,
235                    },
236                    span,
237                ),
238            }
239        },
240        engine_state.signals(),
241    )
242}
243
244fn str_expand(contents: &str, span: Span, value_span: Span) -> Value {
245    use bracoxide::{
246        expand,
247        parser::{ParsingError, parse},
248        tokenizer::{TokenizationError, tokenize},
249    };
250    match tokenize(contents) {
251        Ok(tokens) => {
252            match parse(&tokens) {
253                Ok(node) => {
254                    match expand(&node) {
255                        Ok(possibilities) => {
256                            Value::list(possibilities.iter().map(|e| Value::string(e,span)).collect::<Vec<Value>>(), span)
257                        },
258                        Err(e) => match e {
259                            bracoxide::ExpansionError::NumConversionFailed(s) => Value::error(
260                                ShellError::GenericError{error: "Number Conversion Failed".into(), msg: format!("Number conversion failed at {s}."), span: Some(value_span), help: Some("Expected number, found text. Range format is `{M..N}`, where M and N are numeric values representing the starting and ending limits.".into()), inner: vec![]},
261                            span,
262                        ),
263                        },
264                    }
265                },
266                Err(e) => Value::error(
267                    match e {
268                        ParsingError::NoTokens => ShellError::PipelineEmpty { dst_span: value_span },
269                        ParsingError::OBraExpected(s) => ShellError::GenericError{ error: "Opening Brace Expected".into(), msg: format!("Opening brace is expected at {s}."), span: Some(value_span), help: Some("In brace syntax, we use equal amount of opening (`{`) and closing (`}`). Please, take a look at the examples.".into()), inner: vec![]},
270                        ParsingError::CBraExpected(s) => ShellError::GenericError{ error: "Closing Brace Expected".into(), msg: format!("Closing brace is expected at {s}."), span: Some(value_span), help: Some("In brace syntax, we use equal amount of opening (`{`) and closing (`}`). Please, see the examples.".into()), inner: vec![]},
271                        ParsingError::RangeStartLimitExpected(s) => ShellError::GenericError{error: "Range Start Expected".into(), msg: format!("Range start limit is missing, expected at {s}."), span: Some(value_span), help: 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.".into()), inner: vec![]},
272                        ParsingError::RangeEndLimitExpected(s) => ShellError::GenericError{ error: "Range Start Expected".into(), msg: format!("Range start limit is missing, expected at {s}."),span:  Some(value_span), help: 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.".into()), inner: vec![]},
273                        ParsingError::ExpectedText(s) => ShellError::GenericError { error: "Expected Text".into(), msg: format!("Expected text at {s}."), span: Some(value_span), help: Some("Texts are only allowed before opening brace (`{`), after closing brace (`}`), or inside `{}`. Please take a look at the examples.".into()), inner: vec![] },
274                        ParsingError::InvalidCommaUsage(s) => ShellError::GenericError { error: "Invalid Comma Usage".into(), msg: format!("Found comma at {s}. Commas are only valid inside collection (`{{X,Y}}`)."),span:  Some(value_span), help: Some("To escape comma use backslash `\\,`.".into()), inner: vec![] },
275                        ParsingError::RangeCantHaveText(s) => ShellError::GenericError { error: "Range Can not Have Text".into(), msg: format!("Expecting, brace, number, or range operator, but found text at {s}."), span: Some(value_span), help: 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.".into()), inner: vec![]},
276                        ParsingError::ExtraRangeOperator(s) => ShellError::GenericError { error: "Extra Range Operator".into(), msg: format!("Found additional, range operator at {s}."), span: Some(value_span), help: Some("Please, use the format `{M..N}` where M and N are numeric values representing the starting and ending limits of the range.".into()), inner: vec![] },
277                        ParsingError::ExtraCBra(s) => ShellError::GenericError { error: "Extra Closing Brace".into(), msg: format!("Used extra closing brace at {s}."), span: Some(value_span), help: Some("To escape closing brace use backslash, e.g. `\\}`".into()), inner: vec![] },
278                        ParsingError::ExtraOBra(s) => ShellError::GenericError { error: "Extra Opening Brace".into(), msg: format!("Used extra opening brace at {s}."), span: Some(value_span), help: Some("To escape opening brace use backslash, e.g. `\\{`".into()), inner: vec![] },
279                        ParsingError::NothingInBraces(s) => ShellError::GenericError { error: "Nothing In Braces".into(), msg: format!("Nothing found inside braces at {s}."), span: Some(value_span), help: Some("Please provide valid content within the braces. Additionally, you can safely remove it, not needed.".into()), inner: vec![] },
280                    }
281                ,
282                span,
283                )
284            }
285        },
286        Err(e) => match e {
287            TokenizationError::EmptyContent => Value::error(
288                ShellError::PipelineEmpty { dst_span: value_span },
289                value_span,
290            ),
291            TokenizationError::FormatNotSupported => Value::error(
292
293                    ShellError::GenericError {
294                        error: "Format Not Supported".into(),
295                        msg: "Usage of only `{` or `}`. Brace Expansion syntax, needs to have equal amount of opening (`{`) and closing (`}`)".into(),
296                        span: Some(value_span),
297                        help: 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.".into()),
298                        inner: vec![]
299                },
300                 value_span,
301            ),
302            TokenizationError::NoBraces => Value::error(
303                ShellError::GenericError { error: "No Braces".into(), msg: "At least one `{}` brace expansion expected.".into(), span: Some(value_span), help: Some("Please, examine the examples.".into()), inner: vec![] },
304                value_span,
305            )
306        },
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn test_outer_single_item() {
316        assert_eq!(
317            str_expand("{W{x,y}}", Span::test_data(), Span::test_data()),
318            Value::list(
319                vec![
320                    Value::string(String::from("Wx"), Span::test_data(),),
321                    Value::string(String::from("Wy"), Span::test_data(),)
322                ],
323                Span::test_data(),
324            )
325        );
326    }
327
328    #[test]
329    fn dots() {
330        assert_eq!(
331            str_expand("{a.b.c,d}", Span::test_data(), Span::test_data()),
332            Value::list(
333                vec![
334                    Value::string(String::from("a.b.c"), Span::test_data(),),
335                    Value::string(String::from("d"), Span::test_data(),)
336                ],
337                Span::test_data(),
338            )
339        );
340        assert_eq!(
341            str_expand("{1.2.3,a}", Span::test_data(), Span::test_data()),
342            Value::list(
343                vec![
344                    Value::string(String::from("1.2.3"), Span::test_data(),),
345                    Value::string(String::from("a"), Span::test_data(),)
346                ],
347                Span::test_data(),
348            )
349        );
350        assert_eq!(
351            str_expand("{a-1.2,b}", Span::test_data(), Span::test_data()),
352            Value::list(
353                vec![
354                    Value::string(String::from("a-1.2"), Span::test_data(),),
355                    Value::string(String::from("b"), Span::test_data(),)
356                ],
357                Span::test_data(),
358            )
359        );
360    }
361
362    #[test]
363    fn test_numbers_proceeding_escape_char_not_ignored() {
364        assert_eq!(
365            str_expand("1\\\\{a,b}", Span::test_data(), Span::test_data()),
366            Value::list(
367                vec![
368                    Value::string(String::from("1\\a"), Span::test_data(),),
369                    Value::string(String::from("1\\b"), Span::test_data(),)
370                ],
371                Span::test_data(),
372            )
373        );
374    }
375
376    #[test]
377    fn test_examples() {
378        use crate::test_examples;
379        test_examples(StrExpand {})
380    }
381}