brace_expand/lib.rs
1//! This library performs brace expansion of strings, as in shells like Bash etc.
2//!
3//! Given the input:
4//!
5//! ```text
6//! {hello,goodbye,wonderful} world
7//! ```
8//!
9//! this algorithm produces the following collection of strings:
10//!
11//! ```text
12//! hello world
13//! goodbye world
14//! wonderful world
15//! ```
16//!
17//! Note that unlike shell brace expansion, the result is a collection of separate
18//! strings rather than a single string. Also, whitespace characters are not
19//! treated specially by the algorithm; they are on the same footing as printing
20//! characers.
21//!
22//! Curly braces `{` and `}` are used to mark the start and end of an expansion
23//! list, and commas separate the items in each list. Literal curly braces and
24//! commas must be escaped with single backslashes:
25//!
26//! ```text
27//! this is {\{braced\},[bracketed\, nicely]}
28//! ```
29//!
30//! produces:
31//!
32//! ```text
33//! this is {braced}
34//! this is [bracketed, nicely]
35//! ```
36//!
37//! If you want a literal backslash, that too must be escaped:
38//!
39//! ```text
40//! this is a backslash: \\
41//! ```
42//!
43//! produces:
44//!
45//! ```text
46//! this is a backslash: \
47//! ```
48//!
49//! Note that the escaping backslashes are removed from the output.
50//!
51//! Inputs can contain multiple expansion lists, and these can be nested. For
52//! example:
53//!
54//! ```text
55//! {hello,goodbye} {world,my {friends,colleagues}}
56//! ```
57//!
58//! produces:
59//!
60//! ```text
61//! hello world
62//! goodbye world
63//! hello my friends
64//! hello my colleagues
65//! goodbye my friends
66//! goodbye my colleagues
67//! ```
68//!
69//! # Example
70//!
71//! ```rust
72//! # fn main() {
73//! # use brace_expand::brace_expand;
74//! let output = brace_expand("this {is,is not} a pipe");
75//!
76//! assert_eq!(output, vec!["this is a pipe", "this is not a pipe"]);
77//! # }
78//! ```
79
80#![doc(html_root_url = "https://docs.rs/brace-expand/0.1.0")]
81
82#[cfg(test)]
83mod tests;
84
85use std::str::Chars;
86
87//------------------------------------------------------------------------------
88
89// Iterator which converts a stream of characters into a stream of tokens.
90
91#[derive(Debug, Clone, Copy, PartialEq)]
92enum Token {
93 OpenBrace,
94 CloseBrace,
95 Comma,
96 Char(char),
97}
98
99struct TokenIter<'a> {
100 stream: Chars<'a>,
101}
102
103impl<'a> TokenIter<'a> {
104 fn new(buffer: &'a str) -> Self {
105 Self {
106 stream: buffer.chars(),
107 }
108 }
109}
110
111impl<'a> Iterator for TokenIter<'a> {
112 type Item = Token;
113
114 fn next(&mut self) -> Option<Self::Item> {
115 self.stream.next().and_then(|ch| match ch {
116 '\\' => self.stream.next().map(Token::Char),
117 '{' => Some(Token::OpenBrace),
118 '}' => Some(Token::CloseBrace),
119 ',' => Some(Token::Comma),
120 _ => Some(Token::Char(ch)),
121 })
122 }
123}
124
125//------------------------------------------------------------------------------
126
127fn convert_to_string(tokens: &[Token]) -> String {
128 tokens
129 .iter()
130 .filter_map(|token| match token {
131 Token::Char(ch) => Some(ch),
132 _ => None,
133 })
134 .collect()
135}
136
137//------------------------------------------------------------------------------
138
139enum Expansion {
140 Partial(Vec<Vec<Token>>),
141 Complete(String),
142}
143
144fn expand_one_level(to_expand: Vec<Token>) -> Expansion {
145 let mut level = 0;
146 let mut list_start_pos = 0;
147 let mut list_end_pos = 0;
148 let mut term_start_pos = 0;
149 let mut terms = Vec::new();
150
151 for (pos, token) in to_expand.iter().enumerate() {
152 match token {
153 Token::OpenBrace => {
154 level += 1;
155 if level == 1 {
156 list_start_pos = pos;
157 term_start_pos = pos + 1;
158 }
159 }
160
161 Token::CloseBrace => {
162 level -= 1;
163 if level == 0 {
164 list_end_pos = pos + 1;
165 terms.push(&to_expand[term_start_pos..pos]);
166 break; // This is the last component.
167 }
168 }
169
170 Token::Comma => {
171 if level == 1 {
172 terms.push(&to_expand[term_start_pos..pos]);
173 term_start_pos = pos + 1;
174 }
175 }
176
177 _ => (),
178 }
179 }
180
181 if !terms.is_empty() {
182 let prefix = &to_expand[..list_start_pos];
183 let suffix = &to_expand[list_end_pos..];
184
185 let results: Vec<Vec<Token>> = terms
186 .iter()
187 .map(|term| [prefix, term, suffix].concat())
188 .collect();
189
190 Expansion::Partial(results)
191 } else {
192 Expansion::Complete(convert_to_string(&to_expand))
193 }
194}
195
196//------------------------------------------------------------------------------
197
198/// Expand braces and return the set of results.
199///
200/// # Example
201///
202/// ```rust
203/// # fn main() {
204/// # use brace_expand::brace_expand;
205/// let output = brace_expand("{hello,goodbye} {world,my {friends,colleagues}}");
206///
207/// assert_eq!(
208/// output,
209/// vec![
210/// "hello world",
211/// "hello my friends",
212/// "hello my colleagues",
213/// "goodbye world",
214/// "goodbye my friends",
215/// "goodbye my colleagues",
216/// ]
217/// );
218/// # }
219/// ```
220pub fn brace_expand(input: &str) -> Vec<String> {
221 let mut work_queue: Vec<Vec<_>> = vec![TokenIter::new(input).collect()];
222 let mut results = Vec::new();
223
224 while let Some(to_expand) = work_queue.pop() {
225 match expand_one_level(to_expand) {
226 Expansion::Partial(mut new_work) => work_queue.append(&mut new_work),
227 Expansion::Complete(fully_expanded) => results.push(fully_expanded),
228 }
229 }
230
231 // Reverse to get correct ordering of the expansions (Bash-like).
232 results.reverse();
233
234 results
235}