|
| 1 | +-module(rabbit_cli_main). |
| 2 | + |
| 3 | +-export([run/2]). |
| 4 | + |
| 5 | +-spec run(string(), iodata()) -> ok. |
| 6 | + |
| 7 | +run(_ProgName, RawArgs) -> |
| 8 | + %% 1. Argument parsing. |
| 9 | + CommandMods = discover_commands(), |
| 10 | + CommandMap = collect_args_spec(CommandMods, #{}), |
| 11 | + Ret = argparse:parse(RawArgs, CommandMap), |
| 12 | + |
| 13 | + %% 2. Output setup. |
| 14 | + rabbit_cli_output:setup(Ret), |
| 15 | + logger:set_primary_config(level, debug), |
| 16 | + %logger:i(), |
| 17 | + logger:debug("argparse:parse/2 -> ~p~n", [Ret]), |
| 18 | + |
| 19 | + %% 3. Command execution. |
| 20 | + case Ret of |
| 21 | + {ArgMap, PathTo} -> |
| 22 | + run_handler( |
| 23 | + CommandMap, ArgMap, PathTo, undefined); |
| 24 | + ArgMap -> |
| 25 | + %{ maps:find(default, Options), Modules, Options} |
| 26 | + run_handler( |
| 27 | + CommandMap, ArgMap, {[], CommandMap}, {CommandMods, #{}}) |
| 28 | + end, |
| 29 | + |
| 30 | + |
| 31 | + %% 4. Close output and return exit status. |
| 32 | + rabbit_cli_output:close(), |
| 33 | + ok. |
| 34 | + |
| 35 | +discover_commands() -> |
| 36 | + [% TODO: Generate that list. |
| 37 | + rabbit_cli_global_options, |
| 38 | + rabbit_cli_cmd_list_exchanges, |
| 39 | + rabbit_cli_cmd_version |
| 40 | + ]. |
| 41 | + |
| 42 | +%% ------------------------------------------------------------------- |
| 43 | +%% Copied from `cli` module (argparse 1.1.0). |
| 44 | +%% ------------------------------------------------------------------- |
| 45 | + |
| 46 | +collect_args_spec(Modules, Options) when is_list(Modules) -> |
| 47 | + lists:foldl( |
| 48 | + fun (Mod, Cmds) -> |
| 49 | + ModCmd = |
| 50 | + try |
| 51 | + {_, MCmd} = argparse:validate(Mod:cli(), Options), |
| 52 | + MCmd |
| 53 | + catch |
| 54 | + _:_ -> |
| 55 | + %% TODO: Handle error. |
| 56 | + #{} |
| 57 | + end, |
| 58 | + |
| 59 | + %% handlers: use first non-empty handler |
| 60 | + Cmds1 = |
| 61 | + case maps:find(handler, ModCmd) of |
| 62 | + {ok, _Handler} when is_map_key(handler, Cmds) -> |
| 63 | + %% TODO: Warn about duplicate handlers. |
| 64 | + Cmds; |
| 65 | + {ok, Handler} -> |
| 66 | + Cmds#{handler => Handler}; |
| 67 | + error -> |
| 68 | + Cmds |
| 69 | + end, |
| 70 | + |
| 71 | + %% help: concatenate help lines |
| 72 | + Cmds2 = |
| 73 | + case is_map_key(help, ModCmd) of |
| 74 | + true -> |
| 75 | + Cmds1#{ |
| 76 | + help => |
| 77 | + maps:get(help, ModCmd) ++ |
| 78 | + maps:get(help, Cmds1, "") |
| 79 | + }; |
| 80 | + false -> |
| 81 | + Cmds1 |
| 82 | + end, |
| 83 | + |
| 84 | + Cmds3 = merge_arguments( |
| 85 | + maps:get(arguments, ModCmd, []), |
| 86 | + Cmds2), |
| 87 | + merge_commands( |
| 88 | + maps:get(commands, ModCmd, #{}), |
| 89 | + Mod, |
| 90 | + Options, |
| 91 | + Cmds3) |
| 92 | + end, #{}, Modules). |
| 93 | + |
| 94 | +merge_arguments([], Existing) -> |
| 95 | + Existing; |
| 96 | +merge_arguments(Args, Existing) -> |
| 97 | + ExistingArgs = maps:get(arguments, Existing, []), |
| 98 | + Existing#{arguments => ExistingArgs ++ Args}. |
| 99 | + |
| 100 | +%% argparse accepts a map of commands, which means, commands names |
| 101 | +%% can never clash. Yet for cli it is possible when multiple modules |
| 102 | +%% export command with the same name. For this case, skip duplicate |
| 103 | +%% command names, emitting a warning. |
| 104 | +merge_commands(Cmds, Mod, Options, Existing) -> |
| 105 | + MergedCmds = maps:fold( |
| 106 | + fun (Name, Cmd, Acc) -> |
| 107 | + case maps:find(Name, Acc) of |
| 108 | + error -> |
| 109 | + %% merge command with name Name into Acc-umulator |
| 110 | + Acc#{ |
| 111 | + Name => |
| 112 | + create_handlers( |
| 113 | + Mod, Name, Cmd, maps:find(default, Options)) |
| 114 | + }; |
| 115 | + {ok, _Another} -> |
| 116 | + %% TODO: Warn about command conflict. |
| 117 | + Acc |
| 118 | + end |
| 119 | + end, maps:get(commands, Existing, #{}), Cmds), |
| 120 | + Existing#{commands => MergedCmds}. |
| 121 | + |
| 122 | +%% Descends into sub-commands creating handlers where applicable |
| 123 | +create_handlers(Mod, _CmdName, Cmd0, DefaultTerm) -> |
| 124 | + Handler = |
| 125 | + case maps:find(handler, Cmd0) of |
| 126 | + % TODO: |
| 127 | + %error -> |
| 128 | + % make_handler(CmdName, Mod, DefaultTerm); |
| 129 | + %{ok, optional} -> |
| 130 | + % make_handler(CmdName, Mod, DefaultTerm); |
| 131 | + {ok, Existing} -> |
| 132 | + Existing |
| 133 | + end, |
| 134 | + %% |
| 135 | + Cmd = Cmd0#{handler => Handler}, |
| 136 | + case maps:find(commands, Cmd) of |
| 137 | + error -> |
| 138 | + Cmd; |
| 139 | + {ok, Sub} -> |
| 140 | + NewCmds = maps:map( |
| 141 | + fun (CN, CV) -> |
| 142 | + create_handlers(Mod, CN, CV, DefaultTerm) |
| 143 | + end, |
| 144 | + Sub), |
| 145 | + Cmd#{commands => NewCmds} |
| 146 | + end. |
| 147 | + |
| 148 | +run_handler(CmdMap, ArgMap, {Path, #{handler := {Mod, ModFun, Default}}}, _MO) -> |
| 149 | + ArgList = arg_map_to_arg_list(CmdMap, Path, ArgMap, Default), |
| 150 | + %% if argument count may not match, better error can be produced |
| 151 | + erlang:apply(Mod, ModFun, ArgList); |
| 152 | +run_handler(_CmdMap, ArgMap, {_Path, #{handler := {Mod, ModFun}}}, _MO) when is_atom(Mod), is_atom(ModFun) -> |
| 153 | + Mod:ModFun(ArgMap); |
| 154 | +run_handler(CmdMap, ArgMap, {Path, #{handler := {Fun, Default}}}, _MO) when is_function(Fun) -> |
| 155 | + ArgList = arg_map_to_arg_list(CmdMap, Path, ArgMap, Default), |
| 156 | + %% if argument count may not match, better error can be produced |
| 157 | + erlang:apply(Fun, ArgList); |
| 158 | +run_handler(_CmdMap, ArgMap, {_Path, #{handler := Handler}}, _MO) when is_function(Handler, 1) -> |
| 159 | + Handler(ArgMap); |
| 160 | +%% below is compatibility mode: cli/1 behaviour has been removed in 1.1.0, but |
| 161 | +%% is still honoured for existing users |
| 162 | +run_handler(CmdMap, ArgMap, {[], _}, {Modules, Options}) when is_map_key(default, Options) -> |
| 163 | + ArgList = arg_map_to_arg_list(CmdMap, [], ArgMap, maps:get(default, Options)), |
| 164 | + exec_cli(Modules, CmdMap, ArgList, Options); |
| 165 | +run_handler(CmdMap, ArgMap, {[], _}, {Modules, Options}) -> |
| 166 | + % {undefined, {ok, Default}, Modules, Options} |
| 167 | + exec_cli(Modules, CmdMap, [ArgMap], Options). |
| 168 | + |
| 169 | +%% finds first module that exports ctl/1 and execute it |
| 170 | +exec_cli([], CmdMap, _ArgMap, ArgOpts) -> |
| 171 | + %% command not found, let's print usage |
| 172 | + io:format(argparse:help(CmdMap, ArgOpts)); |
| 173 | +exec_cli([Mod|Tail], CmdMap, Args, ArgOpts) -> |
| 174 | + case erlang:function_exported(Mod, cli, length(Args)) of |
| 175 | + true -> |
| 176 | + erlang:apply(Mod, cli, Args); |
| 177 | + false -> |
| 178 | + exec_cli(Tail, CmdMap, Args, ArgOpts) |
| 179 | + end. |
| 180 | + |
| 181 | +%% Given command map, path to reach a specific command, and a parsed argument |
| 182 | +%% map, returns a list of arguments (effectively used to transform map-based |
| 183 | +%% callback handler into positional). |
| 184 | +arg_map_to_arg_list(Command, Path, ArgMap, Default) -> |
| 185 | + AllArgs = collect_arguments(Command, Path, []), |
| 186 | + [maps:get(Arg, ArgMap, Default) || #{name := Arg} <- AllArgs]. |
| 187 | + |
| 188 | +%% recursively descend into Path, ignoring arguments with duplicate names |
| 189 | +collect_arguments(Command, [], Acc) -> |
| 190 | + Acc ++ maps:get(arguments, Command, []); |
| 191 | +collect_arguments(Command, [H|Tail], Acc) -> |
| 192 | + Args = maps:get(arguments, Command, []), |
| 193 | + Next = maps:get(H, maps:get(commands, Command, H)), |
| 194 | + collect_arguments(Next, Tail, Acc ++ Args). |
0 commit comments