%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 1997-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% -module(filelib). %% File utilities. -export([wildcard/1, wildcard/2, is_dir/1, is_file/1, is_regular/1]). -export([fold_files/5, last_modified/1, file_size/1, ensure_dir/1]). -export([wildcard/3, is_dir/2, is_file/2, is_regular/2]). -export([fold_files/6, last_modified/2, file_size/2]). -export([find_file/2, find_file/3, find_source/1, find_source/2, find_source/3]). %% For debugging/testing. -export([compile_wildcard/1]). -include_lib("kernel/include/file.hrl"). -define(HANDLE_ERROR(Expr), try Expr catch error:{badpattern,_}=UnUsUalVaRiAbLeNaMe -> %% Get the stack backtrace correct. error(UnUsUalVaRiAbLeNaMe) end). -type filename() :: file:name(). -type dirname() :: filename(). -type filename_all() :: file:name_all(). -type dirname_all() :: filename_all(). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec wildcard(Wildcard) -> [file:filename()] when Wildcard :: filename() | dirname(). wildcard(Pattern) when is_list(Pattern) -> ?HANDLE_ERROR(do_wildcard(Pattern, ".", file)). -spec wildcard(Wildcard, Cwd) -> [file:filename()] when Wildcard :: filename() | dirname(), Cwd :: dirname(). wildcard(Pattern, Cwd) when is_list(Pattern), is_list(Cwd) -> ?HANDLE_ERROR(do_wildcard(Pattern, Cwd, file)); wildcard(Pattern, Mod) when is_list(Pattern), is_atom(Mod) -> ?HANDLE_ERROR(do_wildcard(Pattern, ".", Mod)). -spec wildcard(file:name(), file:name(), atom()) -> [file:filename()]. wildcard(Pattern, Cwd, Mod) when is_list(Pattern), is_list(Cwd), is_atom(Mod) -> ?HANDLE_ERROR(do_wildcard(Pattern, Cwd, Mod)). -spec is_dir(Name) -> boolean() when Name :: filename_all() | dirname_all(). is_dir(Dir) -> do_is_dir(Dir, file). -spec is_dir(file:name_all(), atom()) -> boolean(). is_dir(Dir, Mod) when is_atom(Mod) -> do_is_dir(Dir, Mod). -spec is_file(Name) -> boolean() when Name :: filename_all() | dirname_all(). is_file(File) -> do_is_file(File, file). -spec is_file(file:name_all(), atom()) -> boolean(). is_file(File, Mod) when is_atom(Mod) -> do_is_file(File, Mod). -spec is_regular(Name) -> boolean() when Name :: filename_all(). is_regular(File) -> do_is_regular(File, file). -spec is_regular(file:name_all(), atom()) -> boolean(). is_regular(File, Mod) when is_atom(Mod) -> do_is_regular(File, Mod). -spec fold_files(Dir, RegExp, Recursive, Fun, AccIn) -> AccOut when Dir :: dirname(), RegExp :: string(), Recursive :: boolean(), Fun :: fun((F :: file:filename(), AccIn) -> AccOut), AccIn :: term(), AccOut :: term(). fold_files(Dir, RegExp, Recursive, Fun, Acc) -> do_fold_files(Dir, RegExp, Recursive, Fun, Acc, file). -spec fold_files(file:name(), string(), boolean(), fun((_,_) -> _), _, atom()) -> _. fold_files(Dir, RegExp, Recursive, Fun, Acc, Mod) when is_atom(Mod) -> do_fold_files(Dir, RegExp, Recursive, Fun, Acc, Mod). -spec last_modified(Name) -> file:date_time() | 0 when Name :: filename_all() | dirname_all(). last_modified(File) -> do_last_modified(File, file). -spec last_modified(file:name_all(), atom()) -> file:date_time() | 0. last_modified(File, Mod) when is_atom(Mod) -> do_last_modified(File, Mod). -spec file_size(Filename) -> non_neg_integer() when Filename :: filename_all(). file_size(File) -> do_file_size(File, file). -spec file_size(file:name(), atom()) -> non_neg_integer(). file_size(File, Mod) when is_atom(Mod) -> do_file_size(File, Mod). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% do_is_dir(Dir, Mod) -> case eval_read_file_info(Dir, Mod) of {ok, #file_info{type=directory}} -> true; _ -> false end. do_is_file(File, Mod) -> case eval_read_file_info(File, Mod) of {ok, #file_info{type=regular}} -> true; {ok, #file_info{type=directory}} -> true; _ -> false end. do_is_regular(File, Mod) -> case eval_read_file_info(File, Mod) of {ok, #file_info{type=regular}} -> true; _ -> false end. %% fold_files(Dir, RegExp, Recursive, Fun, AccIn). %% folds the function Fun(F, Acc) -> Acc1 over %% all files in that match the regular expression %% If is true all sub-directories to are processed do_fold_files(Dir, RegExp, Recursive, Fun, Acc, Mod) -> {ok, Re1} = re:compile(RegExp,[unicode]), do_fold_files1(Dir, Re1, RegExp, Recursive, Fun, Acc, Mod). do_fold_files1(Dir, RegExp, OrigRE, Recursive, Fun, Acc, Mod) -> case eval_list_dir(Dir, Mod) of {ok, Files} -> do_fold_files2(Files, Dir, RegExp, OrigRE, Recursive, Fun, Acc, Mod); {error, _} -> Acc end. %% OrigRE is not to be compiled as it's for non conforming filenames, %% i.e. for filenames that does not comply to the current encoding, which should %% be very rare. We use it only in those cases and do not want to precompile. do_fold_files2([], _Dir, _RegExp, _OrigRE, _Recursive, _Fun, Acc, _Mod) -> Acc; do_fold_files2([File|T], Dir, RegExp, OrigRE, Recursive, Fun, Acc0, Mod) -> FullName = filename:join(Dir, File), case do_is_regular(FullName, Mod) of true -> case (catch re:run(File, if is_binary(File) -> OrigRE; true -> RegExp end, [{capture,none}])) of match -> Acc = Fun(FullName, Acc0), do_fold_files2(T, Dir, RegExp, OrigRE, Recursive, Fun, Acc, Mod); {'EXIT',_} -> do_fold_files2(T, Dir, RegExp, OrigRE, Recursive, Fun, Acc0, Mod); nomatch -> do_fold_files2(T, Dir, RegExp, OrigRE, Recursive, Fun, Acc0, Mod) end; false -> case Recursive andalso do_is_dir(FullName, Mod) of true -> Acc1 = do_fold_files1(FullName, RegExp, OrigRE, Recursive, Fun, Acc0, Mod), do_fold_files2(T, Dir, RegExp, OrigRE, Recursive, Fun, Acc1, Mod); false -> do_fold_files2(T, Dir, RegExp, OrigRE, Recursive, Fun, Acc0, Mod) end end. do_last_modified(File, Mod) -> case eval_read_file_info(File, Mod) of {ok, Info} -> Info#file_info.mtime; _ -> 0 end. do_file_size(File, Mod) -> case eval_read_file_info(File, Mod) of {ok, Info} -> Info#file_info.size; _ -> 0 end. %%---------------------------------------------------------------------- %% +type ensure_dir(X) -> ok | {error, Reason}. %% +type X = filename() | dirname() %% ensures that the directory name required to create D exists -spec ensure_dir(Name) -> 'ok' | {'error', Reason} when Name :: filename_all() | dirname_all(), Reason :: file:posix(). ensure_dir("/") -> ok; ensure_dir(F) -> Dir = filename:dirname(F), case do_is_dir(Dir, file) of true -> ok; false when Dir =:= F -> %% Protect against infinite loop {error,einval}; false -> _ = ensure_dir(Dir), case file:make_dir(Dir) of {error,eexist}=EExist -> case do_is_dir(Dir, file) of true -> ok; false -> EExist end; Err -> Err end end. %%% %%% Pattern matching using a compiled wildcard. %%% do_wildcard(Pattern, Cwd, Mod) -> {Compiled,PrefixLen} = compile_wildcard(Pattern, Cwd), Files0 = do_wildcard_1(Compiled, Mod), Files = if PrefixLen =:= 0 -> Files0; true -> [lists:nthtail(PrefixLen, File) || File <- Files0] end, lists:sort(Files). do_wildcard_1({exists,File}, Mod) -> case eval_read_link_info(File, Mod) of {ok,_} -> [File]; _ -> [] end; do_wildcard_1([Base|Rest], Mod) -> do_wildcard_2([Base], Rest, [], Mod). do_wildcard_2([File|Rest], Pattern, Result, Mod) -> do_wildcard_2(Rest, Pattern, do_wildcard_3(File, Pattern, Result, Mod), Mod); do_wildcard_2([], _, Result, _Mod) -> Result. do_wildcard_3(Base, [[double_star]|Rest], Result, Mod) -> do_double_star(".", [Base], Rest, Result, Mod, true); do_wildcard_3(Base0, [Pattern|Rest], Result, Mod) -> case do_list_dir(Base0, Mod) of {ok, Files} -> Base = prepare_base(Base0), Matches = do_wildcard_4(Pattern, Base, Files), do_wildcard_2(Matches, Rest, Result, Mod); _ -> Result end; do_wildcard_3(Base, [], Result, _Mod) -> [Base|Result]. do_wildcard_4(Pattern, Base, Files) -> case will_always_match(Pattern) of false -> [Base++F || F <- Files, match_part(Pattern, F)]; true -> [Base++F || F <- Files] end. match_part([question|Rest1], [_|Rest2]) -> match_part(Rest1, Rest2); match_part([accept], _) -> true; match_part([double_star], _) -> true; match_part([star|Rest], File) -> do_star(Rest, File); match_part([{one_of, Ordset}|Rest], [C|File]) -> gb_sets:is_element(C, Ordset) andalso match_part(Rest, File); match_part([{alt, Alts}], File) -> do_alt(Alts, File); match_part([C|Rest1], [C|Rest2]) when is_integer(C) -> match_part(Rest1, Rest2); match_part([X|_], [Y|_]) when is_integer(X), is_integer(Y) -> false; match_part([], []) -> true; match_part([], [_|_]) -> false; match_part([_|_], []) -> false. will_always_match([accept]) -> true; will_always_match(_) -> false. prepare_base(Base0) -> Base1 = filename:join(Base0, "x"), "x"++Base2 = lists:reverse(Base1), lists:reverse(Base2). do_double_star(Base, [H|T], Rest, Result, Mod, Root) -> Full = case Root of false -> filename:join(Base, H); true -> H end, Result1 = case do_list_dir(Full, Mod) of {ok, Files} -> do_double_star(Full, Files, Rest, Result, Mod, false); _ -> Result end, Result2 = case Root andalso Rest == [] of true -> Result1; false -> do_wildcard_3(Full, Rest, Result1, Mod) end, do_double_star(Base, T, Rest, Result2, Mod, Root); do_double_star(_Base, [], _Rest, Result, _Mod, _Root) -> Result. do_star(Pattern, [_|Rest]=File) -> match_part(Pattern, File) orelse do_star(Pattern, Rest); do_star(Pattern, []) -> match_part(Pattern, []). do_alt([Alt|Rest], File) -> match_part(Alt, File) orelse do_alt(Rest, File); do_alt([], _File) -> false. do_list_dir(Dir, Mod) -> eval_list_dir(Dir, Mod). %%% Compiling a wildcard. %% Define characters used for escaping a \. -define(ESCAPE_PREFIX, $@). -define(ESCAPE_CHARACTER, [?ESCAPE_PREFIX,$e]). -define(ESCAPED_ESCAPE_PREFIX, [?ESCAPE_PREFIX,?ESCAPE_PREFIX]). %% Only for debugging. compile_wildcard(Pattern) when is_list(Pattern) -> {compiled_wildcard,?HANDLE_ERROR(compile_wildcard(Pattern, "."))}. compile_wildcard(Pattern0, Cwd0) -> Pattern = convert_escapes(Pattern0), [Root|Rest] = filename:split(Pattern), case filename:pathtype(Root) of relative -> Cwd = prepare_base(Cwd0), compile_wildcard_2([Root|Rest], {cwd,Cwd}); _ -> compile_wildcard_2(Rest, {root,0,Root}) end. compile_wildcard_2([Part|Rest], Root) -> case compile_part(Part) of Part -> compile_wildcard_2(Rest, compile_join(Root, Part)); Pattern -> compile_wildcard_3(Rest, [Pattern,Root]) end; compile_wildcard_2([], {root,PrefixLen,Root}) -> {{exists,Root},PrefixLen}. compile_wildcard_3([Part|Rest], Result) -> compile_wildcard_3(Rest, [compile_part(Part)|Result]); compile_wildcard_3([], Result) -> case lists:reverse(Result) of [{root,PrefixLen,Root}|Compiled] -> {[Root|Compiled],PrefixLen}; [{cwd,Root}|Compiled] -> {[Root|Compiled],length(filename:join(Root, "x"))-1} end. compile_join({cwd,"."}, File) -> {root,0,File}; compile_join({cwd,Cwd}, File0) -> File = filename:join([File0]), Root = filename:join(Cwd, File), PrefixLen = length(Root) - length(File), {root,PrefixLen,Root}; compile_join({root,PrefixLen,Root}, File) -> {root,PrefixLen,filename:join(Root, File)}. compile_part(Part0) -> Part = wrap_escapes(Part0), compile_part(Part, false, []). compile_part_to_sep(Part) -> compile_part(Part, true, []). compile_part([], true, _) -> badpattern(missing_delimiter); compile_part([$,|Rest], true, Result) -> {ok, $,, lists:reverse(Result), Rest}; compile_part([$}|Rest], true, Result) -> {ok, $}, lists:reverse(Result), Rest}; compile_part([$?|Rest], Upto, Result) -> compile_part(Rest, Upto, [question|Result]); compile_part([$*,$*], Upto, Result) -> compile_part([], Upto, [double_star|Result]); compile_part([$*,$*|Rest], Upto, Result) -> compile_part(Rest, Upto, [star|Result]); compile_part([$*], Upto, Result) -> compile_part([], Upto, [accept|Result]); compile_part([$*|Rest], Upto, Result) -> compile_part(Rest, Upto, [star|Result]); compile_part([$[|Rest], Upto, Result) -> case compile_charset(Rest, ordsets:new()) of {ok, Charset, Rest1} -> compile_part(Rest1, Upto, [Charset|Result]); error -> compile_part(Rest, Upto, [$[|Result]) end; compile_part([${|Rest], Upto, Result) -> case compile_alt(Rest) of {ok, Alt} -> lists:reverse(Result, [Alt]); error -> compile_part(Rest, Upto, [${|Result]) end; compile_part([{escaped,X}|Rest], Upto, Result) -> compile_part(Rest, Upto, [X|Result]); compile_part([X|Rest], Upto, Result) -> compile_part(Rest, Upto, [X|Result]); compile_part([], _Upto, Result) -> lists:reverse(Result). compile_charset([$]|Rest], Ordset) -> compile_charset1(Rest, ordsets:add_element($], Ordset)); compile_charset([], _Ordset) -> error; compile_charset(List, Ordset) -> compile_charset1(List, Ordset). compile_charset1([Lower, $-, Upper|Rest], Ordset) when Lower =< Upper -> compile_charset1(Rest, compile_range(Lower, Upper, Ordset)); compile_charset1([$]|Rest], Ordset) -> {ok, {one_of, gb_sets:from_ordset(Ordset)}, Rest}; compile_charset1([{escaped,X}|Rest], Ordset) -> compile_charset1(Rest, ordsets:add_element(X, Ordset)); compile_charset1([X|Rest], Ordset) -> compile_charset1(Rest, ordsets:add_element(X, Ordset)); compile_charset1([], _Ordset) -> error. compile_range(Lower, Current, Ordset) when Lower =< Current -> compile_range(Lower, Current-1, ordsets:add_element(Current, Ordset)); compile_range(_, _, Ordset) -> Ordset. compile_alt(Pattern) -> compile_alt(Pattern, []). compile_alt(Pattern, Result) -> case compile_part_to_sep(Pattern) of {ok, $,, AltPattern, Rest} -> compile_alt(Rest, [AltPattern|Result]); {ok, $}, AltPattern, Rest} -> NewResult = [AltPattern|Result], RestPattern = compile_part(Rest), {ok, {alt, [Alt++RestPattern || Alt <- NewResult]}}; Pattern -> error end. %% Convert backslashes to an illegal Unicode character to %% protect in from filename:split/1. convert_escapes([?ESCAPE_PREFIX|T]) -> ?ESCAPED_ESCAPE_PREFIX ++ convert_escapes(T); convert_escapes([$\\|T]) -> ?ESCAPE_CHARACTER ++ convert_escapes(T); convert_escapes([H|T]) -> [H|convert_escapes(T)]; convert_escapes([]) -> []. %% Wrap each escape in a tuple to remove the special meaning for %% the character that follows. wrap_escapes(?ESCAPED_ESCAPE_PREFIX ++ T) -> [?ESCAPE_PREFIX|wrap_escapes(T)]; wrap_escapes(?ESCAPE_CHARACTER ++ [C|T]) -> [{escaped,C}|wrap_escapes(T)]; wrap_escapes(?ESCAPE_CHARACTER) -> []; wrap_escapes([H|T]) -> [H|wrap_escapes(T)]; wrap_escapes([]) -> []. badpattern(Reason) -> error({badpattern,Reason}). eval_read_file_info(File, file) -> file:read_file_info(File); eval_read_file_info(File, erl_prim_loader) -> case erl_prim_loader:read_file_info(File) of error -> {error, erl_prim_loader}; Res-> Res end; eval_read_file_info(File, Mod) -> Mod:read_file_info(File). eval_read_link_info(File, file) -> file:read_link_info(File); eval_read_link_info(File, erl_prim_loader) -> case erl_prim_loader:read_link_info(File) of error -> {error, erl_prim_loader}; Res-> Res end; eval_read_link_info(File, Mod) -> Mod:read_link_info(File). eval_list_dir(Dir, file) -> file:list_dir(Dir); eval_list_dir(Dir, erl_prim_loader) -> case erl_prim_loader:list_dir(Dir) of error -> {error, erl_prim_loader}; Res-> Res end; eval_list_dir(Dir, Mod) -> Mod:list_dir(Dir). %% Getting the rules to use for file search keep_dir_search_rules(Rules) -> [T || {_,_}=T <- Rules]. keep_suffix_search_rules(Rules) -> [T || {_,_,_}=T <- Rules]. get_search_rules() -> case application:get_env(kernel, source_search_rules) of undefined -> default_search_rules(); {ok, []} -> default_search_rules(); {ok, R} when is_list(R) -> R end. default_search_rules() -> [%% suffix-speficic rules for source search {".beam", ".erl", erl_source_search_rules()}, {".erl", ".yrl", []}, {"", ".src", erl_source_search_rules()}, {".so", ".c", c_source_search_rules()}, {".o", ".c", c_source_search_rules()}, {"", ".c", c_source_search_rules()}, {"", ".in", basic_source_search_rules()}, %% plain old directory rules, backwards compatible {"", ""}] ++ erl_source_search_rules(). basic_source_search_rules() -> (erl_source_search_rules() ++ c_source_search_rules()). erl_source_search_rules() -> [{"ebin","src"}, {"ebin","esrc"}, {"ebin",filename:join("src", "*")}, {"ebin",filename:join("esrc", "*")}]. c_source_search_rules() -> [{"priv","c_src"}, {"priv","src"}, {"bin","c_src"}, {"bin","src"}, {"", "src"}]. %% Looks for a file relative to a given directory -type find_file_rule() :: {ObjDirSuffix::string(), SrcDirSuffix::string()}. -spec find_file(filename(), filename()) -> {ok, filename()} | {error, not_found}. find_file(Filename, Dir) -> find_file(Filename, Dir, []). -spec find_file(filename(), filename(), [find_file_rule()]) -> {ok, filename()} | {error, not_found}. find_file(Filename, Dir, []) -> find_file(Filename, Dir, get_search_rules()); find_file(Filename, Dir, Rules) -> try_dir_rules(keep_dir_search_rules(Rules), Filename, Dir). %% Looks for a source file relative to the object file name and directory -type find_source_rule() :: {ObjExtension::string(), SrcExtension::string(), [find_file_rule()]}. -spec find_source(filename()) -> {ok, filename()} | {error, not_found}. find_source(FilePath) -> find_source(filename:basename(FilePath), filename:dirname(FilePath)). -spec find_source(filename(), filename()) -> {ok, filename()} | {error, not_found}. find_source(Filename, Dir) -> find_source(Filename, Dir, []). -spec find_source(filename(), filename(), [find_source_rule()]) -> {ok, filename()} | {error, not_found}. find_source(Filename, Dir, []) -> find_source(Filename, Dir, get_search_rules()); find_source(Filename, Dir, Rules) -> try_suffix_rules(keep_suffix_search_rules(Rules), Filename, Dir). try_suffix_rules(Rules, Filename, Dir) -> Ext = filename:extension(Filename), try_suffix_rules(Rules, filename:rootname(Filename, Ext), Dir, Ext). try_suffix_rules([{Ext,Src,Rules}|Rest], Root, Dir, Ext) when is_list(Src), is_list(Rules) -> case try_dir_rules(add_local_search(Rules), Root ++ Src, Dir) of {ok, File} -> {ok, File}; _Other -> try_suffix_rules(Rest, Root, Dir, Ext) end; try_suffix_rules([_|Rest], Root, Dir, Ext) -> try_suffix_rules(Rest, Root, Dir, Ext); try_suffix_rules([], _Root, _Dir, _Ext) -> {error, not_found}. %% ensuring we check the directory of the object file before any other directory add_local_search(Rules) -> Local = {"",""}, [Local] ++ lists:filter(fun (X) -> X =/= Local end, Rules). try_dir_rules([{From, To}|Rest], Filename, Dir) when is_list(From), is_list(To) -> case try_dir_rule(Dir, Filename, From, To) of {ok, File} -> {ok, File}; error -> try_dir_rules(Rest, Filename, Dir) end; try_dir_rules([], _Filename, _Dir) -> {error, not_found}. try_dir_rule(Dir, Filename, From, To) -> case lists:suffix(From, Dir) of true -> NewDir = lists:sublist(Dir, 1, length(Dir)-length(From))++To, Src = filename:join(NewDir, Filename), case is_regular(Src) of true -> {ok, Src}; false -> find_regular_file(wildcard(Src)) end; false -> error end. find_regular_file([]) -> error; find_regular_file([File|Files]) -> case is_regular(File) of true -> {ok, File}; false -> find_regular_file(Files) end.