Packages

A library to match glob expressions

Current section

Files

Jump to
glob src glob.erl
Raw

src/glob.erl

%%%=============================================================================
%%%
%%% | o __ _| _ __ |_ _ _ _ (TM)
%%% |_ | | | (_| (/_ | | |_) (_| |_| | | |
%%%
%%% @copyright (C) 2015, Lindenbaum GmbH
%%%
%%% Permission to use, copy, modify, and/or distribute this software for any
%%% purpose with or without fee is hereby granted, provided that the above
%%% copyright notice and this permission notice appear in all copies.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%%%
%%% @doc
%%% A library application to work with `glob' patterns.
%%%
%%% This implementation provides the same `glob' pattern facilities as `bash'.
%%% Syntax (partly from [https://en.wikipedia.org/wiki/Glob_%28programming%29)]:
%%% `?': Match exactly one unknown character.
%%% `*': Match any number of unknown characters from the position in which it
%%% appears to the end of the subject also match any number of unknown
%%% characters (regardless of the position where it appears, including at
%%% the start and/or multiple times.
%%% `[characters]': Match a character as part of a group of characters.
%%% `[!characters]': Match any character but the ones specified.
%%% `[character-charcter]': Match a character as part of a character range.
%%% `[!character-charcter]': Match any character but the range specified.
%%%
%%% The underlying implementation utilizes the `re' module which means that a
%%% given `glob' pattern will be converted into a regular expression. For
%%% convenience this module features an API that is quite similar to the `re'
%%% module. Although, the shorthand {@link matches/1} enables a more intuitive
%%% and simple experience.
%%%
%%% As `glob' relies on `re' it offers support for `unicode' input. However,
%%% this support comes with the same restrictions known from `re'.
%%% @end
%%%=============================================================================
-module(glob).
%% API
-export([compile/1, compile/2, run/2, matches/2]).
-opaque mp() :: {?MODULE, tuple()}. %% in fact tuple() referes to an re:mp()
-export_type([mp/0]).
-define(COMPILE_OPTS, [{newline, anycrlf}, dotall, dollar_endonly]).
%%%=============================================================================
%%% API
%%%=============================================================================
%%------------------------------------------------------------------------------
%% @doc
%% Pre-compiles a `glob' expression/pattern. Same as `compile(Expr, false)'.
%% @end
%%------------------------------------------------------------------------------
-spec compile(iodata()) -> {ok, mp()} | {error, term()}.
compile(Expr) -> compile(Expr, false).
%%------------------------------------------------------------------------------
%% @doc
%% Pre-compiles a `glob' expression/pattern. Pre-compiling an expression/pattern
%% improves performance when using the result in consecutive matches.
%%
%% When using `unicode'/`UTF-8' as input for the subject or the
%% expression/pattern itself, pre-compilation is mandatory!
%% @end
%%------------------------------------------------------------------------------
-spec compile(iodata() | unicode:charlist(), boolean()) ->
{ok, mp()} | {error, term()}.
compile(Expr, IsUnicode) ->
Opts = if IsUnicode -> [unicode]; true -> [] end,
case convert(to_list(Expr, IsUnicode), [], Opts) of
{ok, MP} -> {ok, {?MODULE, MP}};
Error -> Error
end.
%%------------------------------------------------------------------------------
%% @doc
%% Executes an expression/pattern matching, returning `match' or `nomatch'. The
%% expression/pattern can be given either as `iodata()' in which case it is
%% automatically compiled (as by {@link glob:compile/1,2}) and executed, or as a
%% pre-compiled `mp()' in which case it is executed against the subject
%% directly.
%%
%% If the expression/pattern was previously compiled with the `IsUnicode=true',
%% `Subject' should be provided as a valid Unicode `charlist()', otherwise any
%% `iodata()' will do.
%%
%% When compilation is involved, the exception `badarg' is thrown if a
%% compilation error occurs. Call {@link glob:compile/1,2} to get information
%% about the error in the expression/pattern.
%% @end
%%------------------------------------------------------------------------------
-spec run(iodata() | unicode:charlist(), iodata() | mp()) -> match | nomatch.
run(Subject, Expr) ->
try
match(Subject, Expr)
catch
error:{badmatch, _} -> error(badarg)
end.
%%------------------------------------------------------------------------------
%% @doc
%% Similar to {@link run/2} but returns a `boolean' indicating whether `Subject'
%% matches `Expr'.
%%
%% When compilation is involved, the exception `badmatch' is thrown if a
%% compilation error occurs. Call {@link glob:compile/1,2} to get information
%% about the error in the expression/pattern.
%% @end
%%------------------------------------------------------------------------------
-spec matches(iodata() | unicode:charlist(), iodata() | mp()) -> boolean().
matches(Subject, Expr) -> match(Subject, Expr) =:= match.
%%%=============================================================================
%%% Internal functions
%%%=============================================================================
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
match(Subject, {?MODULE, MP}) ->
re:run(Subject, MP, [{capture, none}]);
match(Subject, Expr) ->
{ok, Compiled} = compile(Expr),
match(Subject, Compiled).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
convert([], [], Opts) ->
re:compile([$^, $$], Opts ++ ?COMPILE_OPTS);
convert([], Acc, Opts) ->
re:compile(lists:reverse([$$ | Acc]), Opts ++ ?COMPILE_OPTS);
convert([$* | Rest], Acc, Opts) ->
convert(Rest, [$* | [$. | Acc]], Opts);
convert([$? | Rest], Acc, Opts) ->
convert(Rest, [$. | Acc], Opts);
convert([$\\], _Acc, _Opts) ->
{error, escape_sequence_at_end_of_pattern};
convert([$\\ | [C | Rest]], Acc, Opts) ->
convert(Rest, [C | [$\\ | Acc]], Opts);
convert([$[ | [$! | Rest]], Acc, Opts) ->
convert_character_class(Rest, [$^ | [$[ | Acc]], Opts);
convert([$[ | Rest], Acc, Opts) ->
convert_character_class(Rest, [$[ | Acc], Opts);
convert([C | Rest], Acc, Opts) ->
convert(Rest, escape(C, Acc), Opts).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
convert_character_class([], _Acc, _Opts) ->
{error, non_terminated_character_class};
convert_character_class([$\\], _Acc, _Opts) ->
{error, escape_sequence_at_end_of_pattern};
convert_character_class([$\\ | [C | Rest]], Acc, Opts) ->
convert_character_class(Rest, [C | [$\\ | Acc]], Opts);
convert_character_class([$] | Rest], Acc, Opts) ->
convert(Rest, [$] | Acc], Opts);
convert_character_class([C | Rest], Acc, Opts) ->
convert_character_class(Rest, [C | Acc], Opts).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
escape($^, Acc) -> [$^ | [$\\ | Acc]];
escape($$, Acc) -> [$$ | [$\\ | Acc]];
escape($., Acc) -> [$. | [$\\ | Acc]];
escape($|, Acc) -> [$| | [$\\ | Acc]];
escape($(, Acc) -> [$( | [$\\ | Acc]];
escape($), Acc) -> [$) | [$\\ | Acc]];
escape($+, Acc) -> [$+ | [$\\ | Acc]];
escape(${, Acc) -> [${ | [$\\ | Acc]];
escape($}, Acc) -> [$} | [$\\ | Acc]];
escape(C, Acc) -> [C | Acc].
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
to_list(Data, true) ->
unicode:characters_to_list(Data);
to_list(Bin, false) when is_binary(Bin) ->
binary_to_list(Bin);
to_list(IoData, false) ->
to_list(iolist_to_binary(IoData), false).