Skip to main content

nu_command/strings/str_/
expand.rs

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