1#![no_std]
21#![doc(html_root_url = "https://docs.rs/brace-expansion/0.1.0")]
22#![allow(
25 clippy::cast_possible_truncation,
26 clippy::cast_sign_loss,
27 clippy::cast_possible_wrap
28)]
29
30extern crate alloc;
31
32use alloc::format;
33use alloc::string::{String, ToString};
34use alloc::vec;
35use alloc::vec::Vec;
36
37#[cfg(doctest)]
39#[doc = include_str!("../README.md")]
40struct ReadmeDoctests;
41
42pub const EXPANSION_MAX: usize = 100_000;
44
45const MAX_DEPTH: u32 = 256;
49
50const ESC_SLASH: &str = "\u{0}SLASH\u{0}";
54const ESC_OPEN: &str = "\u{0}OPEN\u{0}";
55const ESC_CLOSE: &str = "\u{0}CLOSE\u{0}";
56const ESC_COMMA: &str = "\u{0}COMMA\u{0}";
57const ESC_PERIOD: &str = "\u{0}PERIOD\u{0}";
58
59#[must_use]
68pub fn expand(pattern: &str) -> Vec<String> {
69 expand_with_max(pattern, EXPANSION_MAX)
70}
71
72#[must_use]
74pub fn expand_with_max(pattern: &str, max: usize) -> Vec<String> {
75 if pattern.is_empty() {
76 return Vec::new();
77 }
78 let pattern = match pattern.strip_prefix("{}") {
80 Some(rest) => format!("\\{{\\}}{rest}"),
81 None => pattern.to_string(),
82 };
83
84 expand_inner(&escape_braces(&pattern), max, true, 0)
85 .iter()
86 .map(|s| unescape_braces(s))
87 .collect()
88}
89
90fn escape_braces(s: &str) -> String {
91 s.replace("\\\\", ESC_SLASH)
93 .replace("\\{", ESC_OPEN)
94 .replace("\\}", ESC_CLOSE)
95 .replace("\\,", ESC_COMMA)
96 .replace("\\.", ESC_PERIOD)
97}
98
99fn unescape_braces(s: &str) -> String {
100 s.replace(ESC_SLASH, "\\")
101 .replace(ESC_OPEN, "{")
102 .replace(ESC_CLOSE, "}")
103 .replace(ESC_COMMA, ",")
104 .replace(ESC_PERIOD, ".")
105}
106
107fn embrace(s: &str) -> String {
108 let mut out = String::with_capacity(s.len() + 2);
109 out.push('{');
110 out.push_str(s);
111 out.push('}');
112 out
113}
114
115fn numeric(s: &str) -> i64 {
119 match s.parse::<i64>() {
120 Ok(n) => n,
121 Err(_) if is_int(s) => {
122 if s.starts_with('-') {
123 i64::MIN
124 } else {
125 i64::MAX
126 }
127 }
128 Err(_) => s.chars().next().map_or(0, |c| c as i64),
129 }
130}
131
132fn is_padded(el: &str) -> bool {
134 let body = el.strip_prefix('-').unwrap_or(el).as_bytes();
135 body.len() >= 2 && body[0] == b'0' && body[1].is_ascii_digit()
136}
137
138fn is_int(s: &str) -> bool {
139 let body = s.strip_prefix('-').unwrap_or(s);
140 !body.is_empty() && body.bytes().all(|b| b.is_ascii_digit())
141}
142
143fn is_single_letter(s: &str) -> bool {
144 let mut chars = s.chars();
145 matches!((chars.next(), chars.next()), (Some(c), None) if c.is_ascii_alphabetic())
146}
147
148fn is_numeric_sequence(body: &str) -> bool {
149 let parts: Vec<&str> = body.split("..").collect();
150 matches!(parts.len(), 2 | 3) && parts.iter().all(|p| is_int(p))
151}
152
153fn is_alpha_sequence(body: &str) -> bool {
154 let parts: Vec<&str> = body.split("..").collect();
155 match parts.len() {
156 2 => is_single_letter(parts[0]) && is_single_letter(parts[1]),
157 3 => is_single_letter(parts[0]) && is_single_letter(parts[1]) && is_int(parts[2]),
158 _ => false,
159 }
160}
161
162fn has_dangling_close(post: &str) -> bool {
165 let chars: Vec<char> = post.chars().collect();
166 for i in 0..chars.len() {
167 if chars[i] == ',' && chars.get(i + 1) != Some(&',') && chars[i + 1..].contains(&'}') {
168 return true;
169 }
170 }
171 false
172}
173
174fn balanced(s: &str) -> Option<(String, String, String)> {
180 let chars: Vec<char> = s.chars().collect();
181 let (start, end) = range(&chars)?;
182 Some((
183 chars[..start].iter().collect(),
184 chars[start + 1..end].iter().collect(),
185 chars[end + 1..].iter().collect(),
186 ))
187}
188
189fn index_of(s: &[char], c: char, from: i64) -> i64 {
190 let start = if from < 0 { 0 } else { from as usize };
191 if start >= s.len() {
192 return -1;
193 }
194 match s[start..].iter().position(|&x| x == c) {
195 Some(p) => (start + p) as i64,
196 None => -1,
197 }
198}
199
200#[allow(clippy::many_single_char_names)] fn range(s: &[char]) -> Option<(usize, usize)> {
203 let (a, b) = ('{', '}');
204 let mut ai = index_of(s, a, 0);
205 let mut bi = index_of(s, b, ai + 1);
206 let mut i = ai;
207 let mut result: Option<(i64, i64)> = None;
208
209 if ai >= 0 && bi > 0 {
210 let mut begs: Vec<i64> = Vec::new();
211 let mut left = s.len() as i64;
212 let mut right: i64 = -1;
213 let mut right_set = false;
214
215 while i >= 0 && result.is_none() {
216 if i == ai {
217 begs.push(i);
218 ai = index_of(s, a, i + 1);
219 } else if begs.len() == 1 {
220 if let Some(r) = begs.pop() {
221 result = Some((r, bi));
222 }
223 } else {
224 if let Some(beg) = begs.pop() {
225 if beg < left {
226 left = beg;
227 right = bi;
228 right_set = true;
229 }
230 }
231 bi = index_of(s, b, i + 1);
232 }
233 i = if ai < bi && ai >= 0 { ai } else { bi };
234 }
235
236 if !begs.is_empty() && right_set {
237 result = Some((left, right));
238 }
239 }
240
241 result.map(|(l, r)| (l as usize, r as usize))
242}
243
244fn parse_comma_parts(s: &str, depth: u32) -> Vec<String> {
246 if s.is_empty() {
247 return vec![String::new()];
248 }
249 let split_plain = || s.split(',').map(ToString::to_string).collect();
250 if depth > MAX_DEPTH {
251 return split_plain();
252 }
253 let Some((pre, body, post)) = balanced(s) else {
254 return split_plain();
255 };
256
257 let mut p: Vec<String> = pre.split(',').map(ToString::to_string).collect();
258 let last = p.len() - 1;
259 p[last].push('{');
260 p[last].push_str(&body);
261 p[last].push('}');
262
263 let mut post_parts = parse_comma_parts(&post, depth + 1);
264 if !post.is_empty() && !post_parts.is_empty() {
265 let first = post_parts.remove(0);
266 p[last].push_str(&first);
267 p.extend(post_parts);
268 }
269 p
270}
271
272fn expand_inner(s: &str, max: usize, is_top: bool, depth: u32) -> Vec<String> {
273 if depth > MAX_DEPTH {
274 return vec![s.to_string()];
275 }
276 let Some((pre, body, post_str)) = balanced(s) else {
277 return vec![s.to_string()];
278 };
279
280 let post = if post_str.is_empty() {
281 vec![String::new()]
282 } else {
283 expand_inner(&post_str, max, false, depth + 1)
284 };
285
286 let mut expansions: Vec<String> = Vec::new();
287
288 if pre.ends_with('$') {
289 for p in post.iter().take(max) {
290 expansions.push(format!("{pre}{{{body}}}{p}"));
291 }
292 return expansions;
293 }
294
295 let is_numeric_seq = is_numeric_sequence(&body);
296 let is_alpha_seq = is_alpha_sequence(&body);
297 let is_sequence = is_numeric_seq || is_alpha_seq;
298 let is_options = body.contains(',');
299
300 if !is_sequence && !is_options {
301 if has_dangling_close(&post_str) {
302 let rebuilt = format!("{pre}{{{body}{ESC_CLOSE}{post_str}");
303 return expand_inner(&rebuilt, max, true, depth + 1);
304 }
305 return vec![s.to_string()];
306 }
307
308 let n: Vec<String> = if is_sequence {
309 body.split("..").map(ToString::to_string).collect()
310 } else {
311 let parsed = parse_comma_parts(&body, depth + 1);
312 if parsed.len() == 1 {
313 let embraced: Vec<String> = expand_inner(&parsed[0], max, false, depth + 1)
314 .iter()
315 .map(|x| embrace(x))
316 .collect();
317 if embraced.len() == 1 {
318 return post
319 .iter()
320 .map(|p| format!("{pre}{}{p}", embraced[0]))
321 .collect();
322 }
323 embraced
324 } else {
325 parsed
326 }
327 };
328
329 let big_n: Vec<String> = if is_sequence {
330 sequence(&n, is_alpha_seq, max)
331 } else {
332 let mut v = Vec::new();
333 for part in &n {
334 v.extend(expand_inner(part, max, false, depth + 1));
335 }
336 v
337 };
338
339 for item in &big_n {
340 for p in &post {
341 if expansions.len() >= max {
342 break;
343 }
344 let expansion = format!("{pre}{item}{p}");
345 if !is_top || is_sequence || !expansion.is_empty() {
346 expansions.push(expansion);
347 }
348 }
349 }
350
351 expansions
352}
353
354#[allow(clippy::many_single_char_names)] fn sequence(n: &[String], is_alpha: bool, max: usize) -> Vec<String> {
358 let x = numeric(&n[0]);
359 let y = numeric(&n[1]);
360 let width = n[0].chars().count().max(n[1].chars().count());
361 let mut incr: i64 = if n.len() == 3 {
362 numeric(&n[2]).saturating_abs().max(1)
363 } else {
364 1
365 };
366 let reverse = y < x;
367 if reverse {
368 incr = -incr;
369 }
370 let pad = n.iter().any(|e| is_padded(e));
371
372 let mut out = Vec::new();
373 let mut i = x;
374 loop {
375 let in_range = if reverse { i >= y } else { i <= y };
376 if !in_range || out.len() >= max {
377 break;
378 }
379 if is_alpha {
380 match u32::try_from(i).ok().and_then(char::from_u32) {
381 Some('\\') | None => out.push(String::new()),
382 Some(c) => out.push(c.to_string()),
383 }
384 } else {
385 out.push(pad_number(i, width, pad));
386 }
387 match i.checked_add(incr) {
389 Some(next) => i = next,
390 None => break,
391 }
392 }
393 out
394}
395
396fn pad_number(i: i64, width: usize, pad: bool) -> String {
397 let mut c = i.to_string();
398 if pad {
399 let need = width as i64 - c.chars().count() as i64;
400 if need > 0 {
401 let zeros = "0".repeat(need as usize);
402 c = if i < 0 {
403 let digits = c.strip_prefix('-').unwrap_or("").to_string();
404 format!("-{zeros}{digits}")
405 } else {
406 format!("{zeros}{c}")
407 };
408 }
409 }
410 c
411}