-module(rebar_cmd_prv). -export([ init/1, do/1, format_error/1 ]). -ignore_xref([ do/1, format_error/1, {rebar_api, debug, 2}, {rebar_api, error, 2}, {rebar_api, warn, 2}, {rebar_api, console, 2}, {rebar_state, add_provider, 2}, {providers, create, 1} ]). -define(PROVIDER, cmd). -define(DEPS, [app_discovery]). -define(DEFAULT_OPT_TIMEOUT, 15000). -define(DEFAULT_OPT_VERBOSE, false). -ifdef(TEST). -export([ do_internal/2, find_command_in/2 ]). -ignore_xref([ do_internal/2, find_command_in/2 ]). -endif. %% =================================================================== %% Public API %% =================================================================== -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> Provider = providers:create([ {name, ?PROVIDER}, {module, ?MODULE}, {bare, true}, {deps, ?DEPS}, {example, "rebar3 cmd "}, {opts, []}, {short_desc, "A rebar3 plugin to run custom shell commands 'cmd '."}, {desc, "A rebar3 plugin to run custom shell commands 'cmd '"} ]), {ok, rebar_state:add_provider(State, Provider)}. -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> Config = rebar_state:get(State, commands, []), {ArgsPropList, _} = rebar_state:command_parsed_args(State), CmdName = proplists:get_value(task, ArgsPropList, undefined), do_internal({CmdName, find_command_in(CmdName, Config)}, State). -spec format_error(any()) -> iolist(). format_error(Reason) -> io_lib:format("~p", [Reason]). %% =================================================================== %% Private API %% =================================================================== do_internal({_CmdName, CmdDef}, _State) when is_tuple(CmdDef) andalso (erlang:tuple_size(CmdDef) < 2 orelse erlang:tuple_size(CmdDef) > 3) -> {error, "Invalid configuration: expected [{CmdName, Cmd, Opts}]."}; do_internal({_CmdName, CmdDef}, State) when is_tuple(CmdDef) -> {CmdName, Cmd, Opts} = normalize(CmdDef), handle_cmd(exec(CmdName, Cmd, Opts), Opts, State); do_internal({undefined = _CmdName, false = _CommandFound}, _State) -> {error, "No (as in 'cmd ') given as input."}; do_internal({CmdName, false = _CommandFound}, _State) -> {error, "Command " ++ CmdName ++ " (as atom) is unknown. Check your configuration."}. find_command_in(undefined = _CmdName, _Config) -> false; find_command_in(CmdName, Config) -> lists:keyfind(list_to_atom(CmdName), 1, Config). normalize({CmdName, Cmd}) -> normalize({CmdName, Cmd, []}); normalize({CmdName, Cmd, Opts}) -> {atom_to_list(CmdName), Cmd, Opts}. exec(CmdName, Cmd, Opts) -> Pid = self(), Ref = erlang:make_ref(), Timeout = get_opt(timeout, Opts, CmdName), {MonitorPid, MonitorRef} = erlang:spawn_monitor( fun() -> not is_list(Cmd) andalso exit("call not expressed as a string"), rebar_api:debug("Command ~s executing ~s with options ~p.", [CmdName, Cmd, Opts]), Pid ! {Ref, os:cmd(Cmd)} end ), receive {Ref, Result} -> {ok, {CmdName, Result}}; {'DOWN', MonitorRef, _Type, _Object, Info} -> {ok, {{CmdName, Info}, died}} after Timeout -> exit(MonitorPid, kill), TimeOutS = integer_to_list(Timeout), {error, "Command " ++ CmdName ++ " (" ++ Cmd ++ ") timed out after " ++ TimeOutS ++ " ms."} end. handle_cmd({error, Error}, _Opts, _State) -> {error, Error}; handle_cmd({ok, {{CmdName, Info}, died}}, _Opts, State) -> rebar_api:error("Command ~s died unexpectedly with ~p.", [CmdName, Info]), {ok, State}; handle_cmd({ok, {CmdName, RawResult}}, Opts, State) -> Result = string:trim(RawResult), case get_opt(verbose, Opts, CmdName) of true -> rebar_api:debug("Command ~s finished", [CmdName]), rebar_api:console("~ts", [Result]); false -> rebar_api:debug("Command ~s finished", [CmdName]), rebar_api:debug("Command ~s result: ~ts", [CmdName, Result]) end, {ok, State}. get_opt(timeout, Opts, CmdName) -> Timeout = proplists:get_value(timeout, Opts, ?DEFAULT_OPT_TIMEOUT), case is_integer(Timeout) of false -> rebar_api:error( "Command ~s's timeout not expressed as an integer. " "Defaulting to ~p.", [CmdName, ?DEFAULT_OPT_TIMEOUT] ), ?DEFAULT_OPT_TIMEOUT; true -> Timeout end; get_opt(verbose, Opts, _CmdName) -> proplists:get_value(verbose, Opts, ?DEFAULT_OPT_VERBOSE).