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}