%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2007-2010. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved online at http://www.erlang.org/. %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. %% %% %CopyrightEnd% -module(escript). %% Useful functions that can be called from scripts. -export([script_name/0, foldl/3]). %% Internal API. -export([start/0, start/1]). -record(state, {file, module, forms_or_bin, source, n_errors, mode, exports_main, has_records}). script_name() -> [ScriptName|_] = init:get_plain_arguments(), ScriptName. %% Apply Fun(Name, GetInfo, GetBin, Acc) for each file in the escript. %% %% Fun/2 must return a new accumulator which is passed to the next call. %% The function returns the final value of the accumulator. Acc0 is %% returned if the escript contain an empty archive. %% %% GetInfo/0 is a fun that returns a #file_info{} record for the file. %% GetBin/0 is a fun that returns a the contents of the file as a binary. %% %% An escript may contain erlang code, beam code or an archive: %% %% archive - the Fun/2 will be applied for each file in the archive %% beam - the Fun/2 will be applied once and GetInfo/0 returns the file %% info for the (entire) escript file %% erl - the Fun/2 will be applied once, GetInfo/0 returns the file %% info for the (entire) escript file and the GetBin returns %% the compiled beam code %%-spec foldl(fun((string(), %% fun(() -> #file_info()), %% fun(() -> binary() -> term()), %% term()) -> term()), %% term(), %% string()). foldl(Fun, Acc0, File) when is_function(Fun, 4) -> case parse_file(File, false) of {text, _, Forms, _Mode} when is_list(Forms) -> GetInfo = fun() -> file:read_file_info(File) end, GetBin = fun() -> case compile:forms(Forms, [return_errors, debug_info]) of {ok, _, BeamBin} -> BeamBin; {error, _Errors, _Warnings} -> fatal("There were compilation errors.") end end, try {ok, Fun(".", GetInfo, GetBin, Acc0)} catch throw:Reason -> {error, Reason} end; {beam, _, BeamBin, _Mode} when is_binary(BeamBin) -> GetInfo = fun() -> file:read_file_info(File) end, GetBin = fun() -> BeamBin end, try {ok, Fun(".", GetInfo, GetBin, Acc0)} catch throw:Reason -> {error, Reason} end; {archive, _, ArchiveBin, _Mode} when is_binary(ArchiveBin) -> ZipFun = fun({Name, GetInfo, GetBin}, A) -> A2 = Fun(Name, GetInfo, GetBin, A), {true, false, A2} end, case prim_zip:open(ZipFun, Acc0, {File, ArchiveBin}) of {ok, PrimZip, Res} -> ok = prim_zip:close(PrimZip), {ok, Res}; {error, bad_eocd} -> {error, "Not an archive file"}; {error, Reason} -> {error, Reason} end end. %% %% Internal API. %% start() -> start([]). start(EscriptOptions) -> try %% Commands run using -run or -s are run in a process %% trap_exit set to false. Because this behaviour is %% surprising for users of escript, make sure to reset %% trap_exit to false. process_flag(trap_exit, false), case init:get_plain_arguments() of [File|Args] -> parse_and_run(File, Args, EscriptOptions); [] -> io:format("escript: Missing filename\n", []), my_halt(127) end catch throw:Str -> io:format("escript: ~s\n", [Str]), my_halt(127); _:Reason -> io:format("escript: Internal error: ~p\n", [Reason]), io:format("~p\n", [erlang:get_stacktrace()]), my_halt(127) end. parse_and_run(File, Args, Options) -> CheckOnly = lists:member("s", Options), {Source, Module, FormsOrBin, Mode} = parse_file(File, CheckOnly), Mode2 = case lists:member("d", Options) of true -> debug; false -> case lists:member("c", Options) of true -> compile; false -> case lists:member("i", Options) of true -> interpret; false -> Mode end end end, if is_list(FormsOrBin) -> case Mode2 of interpret -> interpret(FormsOrBin, File, Args); compile -> case compile:forms(FormsOrBin, [report]) of {ok, Module, BeamBin} -> {module, Module} = code:load_binary(Module, File, BeamBin), run(Module, Args); _Other -> fatal("There were compilation errors.") end; debug -> case compile:forms(FormsOrBin, [report, debug_info]) of {ok,Module,BeamBin} -> {module, Module} = code:load_binary(Module, File, BeamBin), debug(Module, {Module, File, File, BeamBin}, Args); _Other -> fatal("There were compilation errors.") end end; is_binary(FormsOrBin) -> case Source of archive -> {ok, FileInfo} = file:read_file_info(File), case code:set_primary_archive(File, FormsOrBin, FileInfo) of ok when CheckOnly -> case code:load_file(Module) of {module, _} -> case erlang:function_exported(Module, main, 1) of true -> my_halt(0); false -> Text = lists:concat(["Function ", Module, ":main/1 is not exported"]), fatal(Text) end; _ -> Text = lists:concat(["Cannot load module ", Module, " from archive"]), fatal(Text) end; ok -> case Mode2 of run -> run(Module, Args); debug -> debug(Module, Module, Args) end; {error, bad_eocd} -> fatal("Not an archive file"); {error, Reason} -> fatal(Reason) end; beam -> case Mode2 of run -> {module, Module} = code:load_binary(Module, File, FormsOrBin), run(Module, Args); debug -> [Base | Rest] = lists:reverse(filename:split(File)), Base2 = filename:basename(Base, code:objfile_extension()), Rest2 = case Rest of ["ebin" | Top] -> ["src" | Top]; _ -> Rest end, SrcFile = filename:join(lists:reverse([Base2 ++ ".erl" | Rest2])), debug(Module, {Module, SrcFile, File, FormsOrBin}, Args) end end end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Parse script %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% parse_file(File, CheckOnly) -> S = #state{file = File, n_errors = 0, mode = interpret, exports_main = false, has_records = false}, {ok, Fd} = case file:open(File, [read]) of {ok, Fd0} -> {ok, Fd0}; {error, R} -> fatal(lists:concat([file:format_error(R), ": '", File, "'"])) end, {HeaderSz, StartLine, ScriptType} = skip_header(Fd, 1), #state{mode = Mode, source = Source, module = Module, forms_or_bin = FormsOrBin} = case ScriptType of archive -> %% Archive file ok = file:close(Fd), parse_archive(S, File, HeaderSz); beam -> %% Beam file ok = file:close(Fd), parse_beam(S, File, HeaderSz, CheckOnly); source -> %% Source code parse_source(S, File, Fd, StartLine, HeaderSz, CheckOnly) end, {Source, Module, FormsOrBin, Mode}. %% Skip header and make a heuristic guess about the script type skip_header(P, LineNo) -> %% Skip shebang on first line {ok, HeaderSz0} = file:position(P, cur), Line1 = get_line(P), case classify_line(Line1) of shebang -> find_first_body_line(P, LineNo); archive -> {HeaderSz0, LineNo, archive}; beam -> {HeaderSz0, LineNo, beam}; _ -> find_first_body_line(P, LineNo) end. find_first_body_line(P, LineNo) -> {ok, HeaderSz1} = file:position(P, cur), %% Look for special comment on second line Line2 = get_line(P), {ok, HeaderSz2} = file:position(P, cur), case classify_line(Line2) of emu_args -> %% Skip special comment on second line Line3 = get_line(P), {HeaderSz2, LineNo + 2, guess_type(Line3)}; _ -> %% Look for special comment on third line Line3 = get_line(P), {ok, HeaderSz3} = file:position(P, cur), case classify_line(Line3) of emu_args -> %% Skip special comment on third line Line4 = get_line(P), {HeaderSz3, LineNo + 3, guess_type(Line4)}; _ -> %% Just skip shebang on first line {HeaderSz1, LineNo + 1, guess_type(Line2)} end end. classify_line(Line) -> case Line of [$\#, $\! | _] -> shebang; [$P, $K | _] -> archive; [$F, $O, $R, $1 | _] -> beam; [$\%, $\%, $\! | _] -> emu_args; _ -> undefined end. guess_type(Line) -> case classify_line(Line) of archive -> archive; beam -> beam; _ -> source end. get_line(P) -> case io:get_line(P, '') of eof -> fatal("Premature end of file reached"); Line -> Line end. parse_archive(S, File, HeaderSz) -> case file:read_file(File) of {ok, <<_FirstLine:HeaderSz/binary, Bin/binary>>} -> Mod = case init:get_argument(escript) of {ok, [["main", M]]} -> %% Use explicit module name list_to_atom(M); _ -> %% Use escript name without extension as module name RevBase = lists:reverse(filename:basename(File)), RevBase2 = case lists:dropwhile(fun(X) -> X =/= $. end, RevBase) of [$. | Rest] -> Rest; [] -> RevBase end, list_to_atom(lists:reverse(RevBase2)) end, S#state{source = archive, mode = run, module = Mod, forms_or_bin = Bin}; {ok, _} -> fatal("Illegal archive format"); {error, Reason} -> fatal(file:format_error(Reason)) end. parse_beam(S, File, HeaderSz, CheckOnly) -> {ok, <<_FirstLine:HeaderSz/binary, Bin/binary>>} = file:read_file(File), case beam_lib:chunks(Bin, [exports]) of {ok, {Module, [{exports, Exports}]}} -> case CheckOnly of true -> case lists:member({main, 1}, Exports) of true -> my_halt(0); false -> Text = lists:concat(["Function ", Module, ":main/1 is not exported"]), fatal(Text) end; false -> S#state{source = beam, mode = run, module = Module, forms_or_bin = Bin} end; {error, beam_lib, Reason} when is_tuple(Reason) -> fatal(element(1, Reason)); {error, beam_lib, Reason} -> fatal(Reason) end. parse_source(S, File, Fd, StartLine, HeaderSz, CheckOnly) -> {PreDefMacros, Module} = pre_def_macros(File), IncludePath = [], {ok, _} = file:position(Fd, {bof, HeaderSz}), case epp:open(File, Fd, StartLine, IncludePath, PreDefMacros) of {ok, Epp} -> {ok, FileForm} = epp:parse_erl_form(Epp), OptModRes = epp:parse_erl_form(Epp), S2 = S#state{source = text, module = Module}, S3 = case OptModRes of {ok, {attribute,_, module, M} = Form} -> epp_parse_file(Epp, S2#state{module = M}, [Form, FileForm]); {ok, _} -> ModForm = {attribute,1,module, Module}, epp_parse_file2(Epp, S2, [ModForm, FileForm], OptModRes); {error, _} -> epp_parse_file2(Epp, S2, [FileForm], OptModRes); {eof,LastLine} -> S#state{forms_or_bin = [FileForm, {eof,LastLine}]} end, ok = epp:close(Epp), ok = file:close(Fd), check_source(S3, CheckOnly); {error, Reason} -> io:format("escript: ~p\n", [Reason]), fatal("Preprocessor error") end. check_source(S, CheckOnly) -> case S of #state{n_errors = Nerrs} when Nerrs =/= 0 -> fatal("There were compilation errors."); #state{exports_main = ExpMain, has_records = HasRecs, forms_or_bin = [FileForm2, ModForm2 | Forms]} -> %% Optionally add export of main/1 Forms2 = case ExpMain of false -> [{attribute,0,export, [{main,1}]} | Forms]; true -> Forms end, Forms3 = [FileForm2, ModForm2 | Forms2], case CheckOnly of true -> %% Optionally expand records Forms4 = case HasRecs of false -> Forms3; true -> erl_expand_records:module(Forms3, []) end, %% Strong validation and halt case compile:forms(Forms4, [report,strong_validation]) of {ok,_} -> my_halt(0); _Other -> fatal("There were compilation errors.") end; false -> %% Basic validation before execution case erl_lint:module(Forms3) of {ok,Ws} -> report_warnings(Ws); {error,Es,Ws} -> report_errors(Es), report_warnings(Ws), fatal("There were compilation errors.") end, %% Optionally expand records Forms4 = case HasRecs of false -> Forms3; true -> erl_expand_records:module(Forms3, []) end, S#state{forms_or_bin = Forms4} end end. pre_def_macros(File) -> {MegaSecs, Secs, MicroSecs} = erlang:now(), Replace = fun(Char) -> case Char of $\. -> $\_; _ -> Char end end, CleanBase = lists:map(Replace, filename:basename(File)), ModuleStr = CleanBase ++ "__" ++ "escript__" ++ integer_to_list(MegaSecs) ++ "__" ++ integer_to_list(Secs) ++ "__" ++ integer_to_list(MicroSecs), Module = list_to_atom(ModuleStr), PreDefMacros = [{'MODULE', Module, redefine}, {'MODULE_STRING', ModuleStr, redefine}], {PreDefMacros, Module}. epp_parse_file(Epp, S, Forms) -> Parsed = epp:parse_erl_form(Epp), epp_parse_file2(Epp, S, Forms, Parsed). epp_parse_file2(Epp, S, Forms, Parsed) -> %% io:format("~p\n", [Parsed]), case Parsed of {ok, Form} -> case Form of {attribute,Ln,record,{Record,Fields}} -> S2 = S#state{has_records = true}, case epp:normalize_typed_record_fields(Fields) of {typed, NewFields} -> epp_parse_file(Epp, S2, [{attribute, Ln, record, {Record, NewFields}}, {attribute, Ln, type, {{record, Record}, Fields, []}} | Forms]); not_typed -> epp_parse_file(Epp, S2, [Form | Forms]) end; {attribute,Ln,mode,NewMode} -> S2 = S#state{mode = NewMode}, if NewMode =:= compile; NewMode =:= interpret; NewMode =:= debug -> epp_parse_file(Epp, S2, [Form | Forms]); true -> Args = lists:flatten(io_lib:format("illegal mode attribute: ~p", [NewMode])), io:format("~s:~w ~s\n", [S#state.file,Ln,Args]), Error = {error,{Ln,erl_parse,Args}}, Nerrs= S#state.n_errors + 1, epp_parse_file(Epp, S2#state{n_errors = Nerrs}, [Error | Forms]) end; {attribute,_,export,Fs} -> case lists:member({main,1}, Fs) of false -> epp_parse_file(Epp, S, [Form | Forms]); true -> epp_parse_file(Epp, S#state{exports_main = true}, [Form | Forms]) end; _ -> epp_parse_file(Epp, S, [Form | Forms]) end; {error,{Ln,Mod,Args}} = Form -> io:format("~s:~w: ~s\n", [S#state.file,Ln,Mod:format_error(Args)]), epp_parse_file(Epp, S#state{n_errors = S#state.n_errors + 1}, [Form | Forms]); {eof,LastLine} -> S#state{forms_or_bin = lists:reverse([{eof, LastLine} | Forms])} end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Evaluate script %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% debug(Module, AbsMod, Args) -> case hidden_apply(debugger, debugger, start, []) of {ok, _} -> case hidden_apply(debugger, int, i, [AbsMod]) of {module, _} -> hidden_apply(debugger, debugger, auto_attach, [[init]]), run(Module, Args); error -> Text = lists:concat(["Cannot load the code for ", Module, " into the debugger"]), fatal(Text) end; _ -> fatal("Cannot start the debugger") end. run(Module, Args) -> try Module:main(Args), my_halt(0) catch Class:Reason -> fatal(format_exception(Class, Reason)) end. interpret(Forms, File, Args) -> Dict = parse_to_dict(Forms), ArgsA = erl_parse:abstract(Args, 0), Call = {call,0,{atom,0,main},[ArgsA]}, try erl_eval:expr(Call, erl_eval:new_bindings(), {value,fun(I, J) -> code_handler(I, J, Dict, File) end}), my_halt(0) catch Class:Reason -> fatal(format_exception(Class, Reason)) end. report_errors(Errors) -> lists:foreach(fun ({{F,_L},Eds}) -> list_errors(F, Eds); ({F,Eds}) -> list_errors(F, Eds) end, Errors). list_errors(F, [{Line,Mod,E}|Es]) -> io:fwrite("~s:~w: ~s\n", [F,Line,Mod:format_error(E)]), list_errors(F, Es); list_errors(F, [{Mod,E}|Es]) -> io:fwrite("~s: ~s\n", [F,Mod:format_error(E)]), list_errors(F, Es); list_errors(_F, []) -> ok. report_warnings(Ws0) -> Ws1 = lists:flatmap(fun({{F,_L},Eds}) -> format_message(F, Eds); ({F,Eds}) -> format_message(F, Eds) end, Ws0), Ws = ordsets:from_list(Ws1), lists:foreach(fun({_,Str}) -> io:put_chars(Str) end, Ws). format_message(F, [{Line,Mod,E}|Es]) -> M = {{F,Line},io_lib:format("~s:~w: Warning: ~s\n", [F,Line,Mod:format_error(E)])}, [M|format_message(F, Es)]; format_message(F, [{Mod,E}|Es]) -> M = {none,io_lib:format("~s: Warning: ~s\n", [F,Mod:format_error(E)])}, [M|format_message(F, Es)]; format_message(_, []) -> []. parse_to_dict(L) -> parse_to_dict(L, dict:new()). parse_to_dict([{function,_,Name,Arity,Clauses}|T], Dict0) -> Dict = dict:store({local, Name,Arity}, Clauses, Dict0), parse_to_dict(T, Dict); parse_to_dict([{attribute,_,import,{Mod,Funcs}}|T], Dict0) -> Dict = lists:foldl(fun(I, D) -> dict:store({remote,I}, Mod, D) end, Dict0, Funcs), parse_to_dict(T, Dict); parse_to_dict([_|T], Dict) -> parse_to_dict(T, Dict); parse_to_dict([], Dict) -> Dict. code_handler(local, [file], _, File) -> File; code_handler(Name, Args, Dict, File) -> %%io:format("code handler=~p~n",[{Name, Args}]), Arity = length(Args), case dict:find({local,Name,Arity}, Dict) of {ok, Cs} -> LF = {value,fun(I, J) -> code_handler(I, J, Dict, File) end}, case erl_eval:match_clause(Cs, Args,erl_eval:new_bindings(),LF) of {Body, Bs} -> eval_exprs(Body, Bs, LF, none, none); nomatch -> erlang:error({function_clause,[{local,Name,Args}]}) end; error -> case dict:find({remote,{Name,Arity}}, Dict) of {ok, Mod} -> %% io:format("Calling:~p~n",[{Mod,Name,Args}]), apply(Mod, Name, Args); error -> io:format("Script does not export ~w/~w\n", [Name,Arity]), my_halt(127) end end. eval_exprs([E], Bs0, Lf, Ef, _RBs) -> RBs1 = value, erl_eval:expr(E, Bs0, Lf, Ef, RBs1); eval_exprs([E|Es], Bs0, Lf, Ef, RBs) -> RBs1 = none, {value,_V,Bs} = erl_eval:expr(E, Bs0, Lf, Ef, RBs1), eval_exprs(Es, Bs, Lf, Ef, RBs). format_exception(Class, Reason) -> PF = fun(Term, I) -> io_lib:format("~." ++ integer_to_list(I) ++ "P", [Term, 50]) end, StackTrace = erlang:get_stacktrace(), StackFun = fun(M, _F, _A) -> (M =:= erl_eval) or (M =:= ?MODULE) end, lib:format_exception(1, Class, Reason, StackTrace, StackFun, PF). fatal(Str) -> throw(Str). my_halt(Reason) -> case process_info(group_leader(), status) of {_,waiting} -> %% Now all output data is down in the driver. %% Give the driver some extra time before halting. receive after 1 -> ok end, halt(Reason); _ -> %% Probably still processing I/O requests. erlang:yield(), my_halt(Reason) end. hidden_apply(App, M, F, Args) -> try apply(fun() -> M end(), F, Args) catch error:undef -> case erlang:get_stacktrace() of [{M,F,Args} | _] -> Arity = length(Args), Text = io_lib:format("Call to ~w:~w/~w in application ~w failed.\n", [M, F, Arity, App]), fatal(Text); Stk -> erlang:raise(error, undef, Stk) end end.