Packages

Brace expansion, as known from sh/bash, in Elixir. Quick example: ExBraceExpansion.expand("file-{a,b,c}.jpg") => ["file-a.jpg", "file-b.jpg", "file-c.jpg"]

Current section

Files

Jump to
ex_brace_expansion lib ex_brace_expansion.ex
Raw

lib/ex_brace_expansion.ex

defmodule ExBraceExpansion do
@esc_slash "\0SLASH#{:random.uniform}\0"
@esc_open "\0OPEN#{:random.uniform}\0"
@esc_close "\0CLOSE#{:random.uniform}\0"
@esc_comma "\0COMMA#{:random.uniform}\0"
@esc_period "\0PERIOD#{:random.uniform}\0"
@moduledoc """
[Brace expansion](https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html), as known from sh/bash, in Elixir. This is a port of [brace-expasion](https://github.com/juliangruber/brace-expansion) javascript project.
"""
import ExBraceExpansion.BalancedMatch
import ExBraceExpansion.ConcatMap
@doc ~S"""
expands the `str` into a list of patterns
## Examples
iex> import ExBraceExpansion
nil
iex> expand("file-{a,b,c}.jpg")
["file-a.jpg", "file-b.jpg", "file-c.jpg"]
iex> expand("-v{,,}")
["-v", "-v", "-v"]
iex> expand("file{0..2}.jpg")
["file0.jpg", "file1.jpg", "file2.jpg"]
iex> expand("file-{a..c}.jpg")
["file-a.jpg", "file-b.jpg", "file-c.jpg"]
iex> expand("file{2..0}.jpg")
["file2.jpg", "file1.jpg", "file0.jpg"]
iex> expand("file{0..4..2}.jpg")
["file0.jpg", "file2.jpg", "file4.jpg"]
iex> expand("file-{a..e..2}.jpg")
["file-a.jpg", "file-c.jpg", "file-e.jpg"]
iex> expand("file{00..10..5}.jpg")
["file00.jpg", "file05.jpg", "file10.jpg"]
iex> expand("{{A..C},{a..c}}")
["A", "B", "C", "a", "b", "c"]
iex> expand("ppp{,config,oe{,conf}}")
["ppp", "pppconfig", "pppoe", "pppoeconf"]
"""
def expand(str) do
if str == nil do
[]
else
_expand(escape_braces(str), true)
|> Enum.map(fn val -> unescape_braces(val) end)
end
end
defp _expand(str, is_top) do
m = balanced("{", "}", str)
done = m == nil || m.pre =~ ~r/\$$/
is_numeric_sequence = if m, do: m.body =~ ~r/^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/, else: false
is_alpha_sequence = if m, do: m.body =~ ~r/^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/, else: false
is_sequence = is_numeric_sequence || is_alpha_sequence
is_options = if m, do: m.body =~ ~r/^(.*,)+(.+)?$/, else: false
is_comma_and_brace = if m, do: m.body =~ ~r/,.*}/, else: false
state = %{
str: str,
m: m,
is_top: is_top,
is_numeric_sequence: is_numeric_sequence,
is_alpha_sequence: is_alpha_sequence,
is_sequence: is_sequence,
is_options: is_options,
is_comma_and_brace: is_comma_and_brace,
done: done,
value: (if done, do: [str], else: nil)
}
state
|> expand_step1
|> expand_step2
|> expand_step3
|> expand_step4
|> expand_step5
end
defp expand_step1(%{done: done} = state) when done, do: state
defp expand_step1(%{is_sequence: is_sequence, is_options: is_options, is_comma_and_brace: is_comma_and_brace, m: m} = state) when not is_sequence and not is_options and is_comma_and_brace do
state
|> put_in([:done], true)
|> put_in([:value], _expand(m.pre <> "{" <> m.body <> @esc_close <> m.post, false))
end
defp expand_step1(%{is_sequence: is_sequence, is_options: is_options, str: str} = state) when not is_sequence and not is_options do
state
|> put_in([:done], true)
|> put_in([:value], [str])
end
defp expand_step1(%{is_sequence: is_sequence, m: m} = state) when is_sequence do
state
|> put_in([:n], Regex.split(~r/\.\./, m.body))
end
defp expand_step1(%{m: m} = state) do
state
|> put_in([:n], parse_comma_parts(m.body))
end
defp expand_step2(%{done: done} = state) when done, do: state
defp expand_step2(%{is_sequence: is_sequence, n: n} = state) when not is_sequence and length(n) == 1 do
state
|> put_in([:n], _expand(hd(n), false) |> Enum.map(fn val -> embrace(val) end))
|> expand_step2a
end
defp expand_step2(state), do: state
defp expand_step2a(%{n: n, m: m} = state) when length(n) == 1 do
post = if String.length(m.post) > 0, do: _expand(m.post, false), else: [""]
value = Enum.map post, fn p ->
m.pre <> hd(n) <> p
end
state
|> put_in([:done], true)
|> put_in([:value], value)
end
defp expand_step2a(state), do: state
defp expand_step3(%{done: done} = state) when done, do: state
defp expand_step3(%{m: m} = state) do
state
|> put_in([:pre], m.pre)
|> put_in([:post], (if String.length(m.post) > 0, do: _expand(m.post, false), else: [""]))
end
defp expand_step4(%{done: done} = state) when done, do: state
defp expand_step4(%{is_sequence: is_sequence, n: n, is_alpha_sequence: is_alpha_sequence} = state) when is_sequence do
n_0 = Enum.at(n, 0)
n_1 = Enum.at(n, 1)
n_2 = Enum.at(n, 2)
x = numeric(n_0)
y = numeric(n_1)
incr = if length(n) == 3, do: abs(numeric(n_2)), else: 1
nn = for i <- x..y, rem(i - x, incr) == 0 do
if is_alpha_sequence do
get_alpha_character(i)
else
get_numeric_character(i, n, n_0, n_1)
end
end
state
|> put_in([:nn], nn)
end
defp expand_step4(%{n: n} = state) do
state
|> put_in([:nn], concat_map(n, fn val -> _expand(val, false) end))
end
defp expand_step5(%{done: done, value: value}) when done, do: value
defp expand_step5(%{nn: nn, post: post, pre: pre, is_top: is_top, is_sequence: is_sequence}) do
for x <- nn, y <- post do
expansion = pre <> x <> y
if not is_top or is_sequence or expansion do
expansion
else
nil
end
end
end
# helpers
defp escape_braces(str) do
str
|> String.split("\\\\") |> Enum.join(@esc_slash)
|> String.split("\\{") |> Enum.join(@esc_open)
|> String.split("\\}") |> Enum.join(@esc_close)
|> String.split("\\,") |> Enum.join(@esc_comma)
|> String.split("\\.") |> Enum.join(@esc_period)
end
defp unescape_braces(str) do
str
|> String.split(@esc_slash) |> Enum.join("\\")
|> String.split(@esc_open) |> Enum.join("{")
|> String.split(@esc_close) |> Enum.join("}")
|> String.split(@esc_comma) |> Enum.join(",")
|> String.split(@esc_period) |> Enum.join(".")
end
defp parse_comma_parts(str) when str == nil or str == "", do: [""]
defp parse_comma_parts(str) do
m = balanced("{", "}", str)
if m == nil do
str |> String.split(",")
else
p = m.pre |> String.split(",")
p = List.update_at(p, length(p) - 1, fn val -> val <> "{" <> m.body <> "}" end)
post_parts = parse_comma_parts(m.post)
if m.post != "" do
[post_parts_hd | post_parts_tail] = post_parts
p = List.update_at(p, length(p) - 1, fn val -> val <> post_parts_hd end)
p = p ++ post_parts_tail
end
[] ++ p
end
end
defp numeric(val) do
try do
String.to_integer(val)
rescue
_ -> hd(to_char_list(val))
end
end
defp embrace(str) do
"{" <> str <> "}"
end
defp get_alpha_character(i) do
c = to_string([i])
if c == "\\", do: "", else: c
end
defp get_numeric_character(i, n, n_0, n_1) do
c = to_string(i)
pad = Enum.any? n, fn val -> val =~ ~r/^-?0\d/ end
if pad do
width = Enum.max([String.length(n_0), String.length(n_1)])
need = width - String.length(c)
if need > 0 do
front_padding = Enum.join(Enum.map(0..(need-1), fn _ -> "0" end), "")
if i < 0 do
c = "-" <> front_padding <> String.slice(c, 1, String.length(c))
else
c = front_padding <> c
end
end
end
c
end
end