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}