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}