defmodule ExMinimatch do @moduledoc """ ExMinimatch =========== Globbing paths without walking the tree! Elixir and Erlang provide `wildcard` functions in the stdlib. But these will walk the directory tree. If you simply want to test whether a file path matches a glob, ExMinimatch is for you. Quick examples: iex> import ExMinimatch nil iex> match("**/*{1..2}{a,b}.{png,jpg}", "asdf/pic2a.jpg") true iex> match("*.+(bar|foo)", "bar.foo") true iex> ["me.jpg", "images/me.png", "images/you.svg"] |> filter("**/*.{png,jpg}") ["me.jpg", "images/me.png"] Compiled forms below allows us to cache the %ExMinimatcher{} struct when used against large number of files repeated. iex> compile("**/*{1..2}{a,b}.{png,jpg}") |> match("asdf/pic2a.jpg") true iex> ["me.jpg", "images/me.png", "images/you.svg"] |> filter(compile("**/*.{png,jpg}")) ["me.jpg", "images/me.png"] ExMinimatch is a port of the [minimatch](https://github.com/isaacs/minimatch) javascript project. It is a close port but not exactly the same. See "Comparison to minimatch.js" section below. ## Glob Patterns Supports these glob features: - [Brace Expansion](https://github.com/gniquil/ex_brace_expansion) - Extended glob matching - "Globstar" ** matching See: - man sh - man bash - man 3 match - man 5 gitignore ## Options `compile`, `match`, and `filter` all have forms that take an options argument (as a map %{}). The following are the explanations. By default, all fo these are false. ### log Possible values are `:info`, and `:debug`. If set, will dump information into repl. `:debug` dumps more. ### nobrace Do not expand `{a,b}` and `{1..3}` brace sets. ### noglobstar Disable `**` matching against multiple folder names. ### dot Allow patterns to match filenames starting with a period, even if the pattern does not explicitly have a period in that spot. Note that by default, `a/**/b` will not match `a/.d/b`, unless dot is set, e.g. `match("a/**/b", "a/.d/b", %{dot: true})` ### noext Disable "extglob" style patterns like `+(a|b)`. ### nocase Perform a case-insensitive match. ### match_base If set, then patterns without slashes will be matched against the basename of the path if it contains slashes. For example, `a?b` would match the path `/xyz/123/acb`, but not `/xyz/acb/123`. ### nocomment Suppress the behavior of treating `#` at the start of a pattern as a comment. ### nonegate Suppress the behavior of treating leading `!` character as negation. ## Comparison to minimatch.js `minimatch.js` converts a glob into a list of regular expressions. However, when it comes to matching, one can choose to use `minimatch.match` or use the complete regular expression generated by `minimatch.makeRe` to test it against a file pattern. Unfortunately, the 2 approaches are __inconsistent__. Notably the full regular expression based approach has a few important pieces missing. Therefore, here we implement the first approach. For detail, take a look at `ExMinimatcher.Matcher`. """ @doc """ Return a compiled %ExMinimatcher{} struct, which can be used in conjunction with `match/2`, 'match/3', or `filter/2` to match files. This purpose of this function is to save time by precompiling the glob pattern. For possible glob patterns and available options, please refer to moduledoc. """ def compile(glob), do: compile(glob, %{}) def compile(glob, options) do options = %{ dot: false, nocase: false, match_base: false, nonegate: false, noext: false, noglobstar: false, nocomment: false, nobrace: false, log: nil } |> Dict.merge(options) ExMinimatch.Compiler.compile_matcher(glob, options) end @doc ~S""" Returns true when file matches the compiled %ExMinimatcher{} struct. This is intended to be used with `compile` ## Examples iex> compile("**/*.{png,jpg}") |> match("me.jpg") true iex> compile("**/*.{png,jpg}") |> match("images/me.png") true iex> compile("**/*.{png,jpg}") |> match("images/you.svg") false iex> ["me.jpg", "images/me.png", "images/you.svg"] |> filter(compile("**/*.{png,jpg}")) ["me.jpg", "images/me.png"] """ def match(%ExMinimatcher{pattern: pattern}, file) when pattern == [] and file == "", do: true def match(%ExMinimatcher{pattern: pattern}, _file) when pattern == [], do: false def match(%ExMinimatcher{} = matcher, file), do: ExMinimatch.Matcher.match_file(file, matcher) @doc """ Return true if the file matches the glob. This is a convenience function that is literally `glob |> compile(options) |> match(file)` Use this for one off matching, as the glob is recompiled every time this is called. For possible glob patterns and available options, please refer to moduledoc. ## Examples iex> match("**/*.png", "qwer.png") true iex> match("**/*.png", "qwer/qwer.png") true """ def match(glob, file) when is_binary(glob), do: match(glob, file, %{}) def match(glob, file, options) when is_binary(glob), do: glob |> compile(options) |> match(file) @doc """ Returns a list of files filtered by the compiled %ExMinimatcher{} struct. Note the collection argument comes first, different from `match`. This is more suitable for piping collections. ## Examples iex> ["me.jpg", "images/me.png", "images/you.svg"] |> filter(compile("**/*.{png,jpg}")) ["me.jpg", "images/me.png"] """ def filter(files, %ExMinimatcher{} = matcher), do: files |> Enum.filter(&match(matcher, &1)) @doc """ return a list of files that match the given pattern. This is a convenience function For possible glob patterns and available options, please refer to moduledoc. ## Examples iex> filter(["qwer.png", "asdf/qwer.png"], "**/*.png") ["qwer.png", "asdf/qwer.png"] iex> filter(["qwer/pic1a.png", "qwer/asdf/pic2a.png", "asdf/pic2c.jpg"], "**/*{1..2}{a,b}.{png,jpg}") ["qwer/pic1a.png", "qwer/asdf/pic2a.png"] """ def filter(files, pattern) when is_binary(pattern), do: filter(files, pattern, %{}) def filter(files, pattern, options) when is_binary(pattern), do: files |> filter(compile(pattern, options)) end