def expand_brackets_core(str, depth) finished_parts = [] active_parts = [+''] while true if str.empty? raise AnticipatedError, "Unmatched opening brace" if depth > 0 break elsif str.start_with?('}') str.slice!(0) raise AnticipatedError, "Unmatched closing brace" if depth == 0 break elsif str.start_with?('{') # Recurse, which removes everything up to and # including the matching closing brace. str.slice!(0) options = expand_brackets_core(str, depth + 1) raise if options.empty? active_parts = active_parts.flat_map { |p1| options.map { |p2| p1 + p2 } } elsif str.start_with?(',') raise AnticipatedError, "Comma at top level" if depth == 0 # Remove the comma, mark the parts we are working # on as finished, and start a new part. str.slice!(0) finished_parts += active_parts active_parts = [''] else part_length = str.index(/[{},]|$/) raise if part_length < 1 part = str.slice!(0, part_length) active_parts.each do |s| s.insert(-1, part) end end end finished_parts + active_parts end # Expands something like "{a,b}{,.x}" to ["a", "a.x", "b", "b.x"] def expand_brackets(str) expand_brackets_core(str.dup, 0) end