mirror of
https://github.com/processone/ejabberd.git
synced 2024-11-24 16:23:40 +01:00
648c83ea03
ECMA-48 SGR sequence ESC [21m is actually 'set double underline' but was incorrectly implemented as 'set normal intensity' in Linux prior to 4.17. The correct sequence for 'set normal intensity' is ESC [22m, which fixes output formatting of 'ejabberdctl' and 'ejabberdctl help' on macos.
922 lines
31 KiB
Erlang
922 lines
31 KiB
Erlang
%%%----------------------------------------------------------------------
|
|
%%% File : ejabberd_ctl.erl
|
|
%%% Author : Alexey Shchepin <alexey@process-one.net>
|
|
%%% Purpose : ejabberd command line admin tool
|
|
%%% Created : 11 Jan 2004 by Alexey Shchepin <alexey@process-one.net>
|
|
%%%
|
|
%%%
|
|
%%% ejabberd, Copyright (C) 2002-2023 ProcessOne
|
|
%%%
|
|
%%% This program is free software; you can redistribute it and/or
|
|
%%% modify it under the terms of the GNU General Public License as
|
|
%%% published by the Free Software Foundation; either version 2 of the
|
|
%%% License, or (at your option) any later version.
|
|
%%%
|
|
%%% This program is distributed in the hope that it will be useful,
|
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
%%% General Public License for more details.
|
|
%%%
|
|
%%% You should have received a copy of the GNU General Public License along
|
|
%%% with this program; if not, write to the Free Software Foundation, Inc.,
|
|
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
%%%
|
|
%%%----------------------------------------------------------------------
|
|
|
|
%%% Does not support commands that have arguments with ctypes: list, tuple
|
|
|
|
-module(ejabberd_ctl).
|
|
|
|
-behaviour(gen_server).
|
|
-author('alexey@process-one.net').
|
|
|
|
-export([start/0, start_link/0, process/1, process2/2]).
|
|
%% gen_server callbacks
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
|
terminate/2, code_change/3]).
|
|
|
|
-include("ejabberd_ctl.hrl").
|
|
-include("ejabberd_commands.hrl").
|
|
-include("logger.hrl").
|
|
-include("ejabberd_stacktrace.hrl").
|
|
|
|
-define(DEFAULT_VERSION, 1000000).
|
|
|
|
-record(state, {}).
|
|
|
|
%%-----------------------------
|
|
%% Module
|
|
%%-----------------------------
|
|
|
|
start() ->
|
|
disable_logging(),
|
|
[SNode, Timeout, Args] = case init:get_plain_arguments() of
|
|
[SNode2, "--no-timeout" | Args2] ->
|
|
[SNode2, infinity, Args2];
|
|
[SNode3 | Args3] ->
|
|
[SNode3, 60000, Args3];
|
|
_ ->
|
|
print_usage(?DEFAULT_VERSION),
|
|
halt(?STATUS_USAGE)
|
|
end,
|
|
SNode1 = case string:tokens(SNode, "@") of
|
|
[_Node, _Server] ->
|
|
SNode;
|
|
_ ->
|
|
case net_kernel:longnames() of
|
|
true ->
|
|
lists:flatten([SNode, "@", inet_db:gethostname(),
|
|
".", inet_db:res_option(domain)]);
|
|
false ->
|
|
lists:flatten([SNode, "@", inet_db:gethostname()]);
|
|
_ ->
|
|
SNode
|
|
end
|
|
end,
|
|
Node = list_to_atom(SNode1),
|
|
Status = case ejabberd_cluster:call(Node, ?MODULE, process, [Args], Timeout) of
|
|
{badrpc, Reason} ->
|
|
print("Failed RPC connection to the node ~p: ~p~n",
|
|
[Node, Reason]),
|
|
%% TODO: show minimal start help
|
|
?STATUS_BADRPC;
|
|
{invalid_version, V} ->
|
|
print("Invalid API version number: ~p~n", [V]),
|
|
?STATUS_ERROR;
|
|
S ->
|
|
S
|
|
end,
|
|
halt(Status).
|
|
|
|
start_link() ->
|
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
|
|
|
init([]) ->
|
|
{ok, #state{}}.
|
|
|
|
handle_call(Request, From, State) ->
|
|
?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]),
|
|
{noreply, State}.
|
|
|
|
handle_cast(Msg, State) ->
|
|
?WARNING_MSG("Unexpected cast: ~p", [Msg]),
|
|
{noreply, State}.
|
|
|
|
handle_info(Info, State) ->
|
|
?WARNING_MSG("Unexpected info: ~p", [Info]),
|
|
{noreply, State}.
|
|
|
|
terminate(_Reason, _State) ->
|
|
ok.
|
|
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
{ok, State}.
|
|
|
|
%%-----------------------------
|
|
%% Process
|
|
%%-----------------------------
|
|
|
|
-spec process([string()]) -> non_neg_integer().
|
|
process(Args) ->
|
|
process(Args, ?DEFAULT_VERSION).
|
|
|
|
|
|
-spec process([string()], non_neg_integer()) -> non_neg_integer().
|
|
|
|
%% The commands status, stop and restart are defined here to ensure
|
|
%% they are usable even if ejabberd is completely stopped.
|
|
process(["status"], _Version) ->
|
|
{InternalStatus, ProvidedStatus} = init:get_status(),
|
|
print("The node ~p is ~p with status: ~p~n",
|
|
[node(), InternalStatus, ProvidedStatus]),
|
|
case lists:keymember(ejabberd, 1, application:which_applications()) of
|
|
false ->
|
|
EjabberdLogPath = ejabberd_logger:get_log_path(),
|
|
print("ejabberd is not running in that node~n"
|
|
"Check for error messages: ~ts~n"
|
|
"or other files in that directory.~n", [EjabberdLogPath]),
|
|
?STATUS_ERROR;
|
|
true ->
|
|
print("ejabberd ~ts is running in that node~n", [ejabberd_option:version()]),
|
|
?STATUS_SUCCESS
|
|
end;
|
|
|
|
%% TODO: Mnesia operations should not be hardcoded in ejabberd_ctl module.
|
|
%% For now, I leave them there to avoid breaking those commands for people that
|
|
%% may be using it (as format of response is going to change).
|
|
process(["mnesia"], _Version) ->
|
|
print("~p~n", [mnesia:system_info(all)]),
|
|
?STATUS_SUCCESS;
|
|
|
|
process(["mnesia", "info"], _Version) ->
|
|
mnesia:info(),
|
|
?STATUS_SUCCESS;
|
|
|
|
process(["mnesia", Arg], _Version) ->
|
|
case catch mnesia:system_info(list_to_atom(Arg)) of
|
|
{'EXIT', Error} -> print("Error: ~p~n", [Error]);
|
|
Return -> print("~p~n", [Return])
|
|
end,
|
|
?STATUS_SUCCESS;
|
|
|
|
%% The arguments --long and --dual are not documented because they are
|
|
%% automatically selected depending in the number of columns of the shell
|
|
process(["help" | Mode], Version) ->
|
|
{MaxC, ShCode} = get_shell_info(),
|
|
case Mode of
|
|
[] ->
|
|
print_usage_help(MaxC, ShCode),
|
|
?STATUS_SUCCESS;
|
|
["--dual"] ->
|
|
print_usage(dual, MaxC, ShCode, Version),
|
|
?STATUS_USAGE;
|
|
["--long"] ->
|
|
print_usage(long, MaxC, ShCode, Version),
|
|
?STATUS_USAGE;
|
|
["tags"] ->
|
|
print_usage_tags(MaxC, ShCode, Version),
|
|
?STATUS_SUCCESS;
|
|
["--tags"] -> % deprecated in favor of "tags"
|
|
print_usage_tags(MaxC, ShCode, Version),
|
|
?STATUS_SUCCESS;
|
|
["commands"] ->
|
|
print_usage_tags_long(MaxC, ShCode, Version),
|
|
?STATUS_SUCCESS;
|
|
["--tags", Tag] -> % deprecated in favor of simply "Tag"
|
|
print_usage_tags(Tag, MaxC, ShCode, Version),
|
|
?STATUS_SUCCESS;
|
|
[String | _] ->
|
|
case determine_string_type(String, Version) of
|
|
no_idea ->
|
|
io:format("No tag or command matches '~ts'~n", [String]);
|
|
both ->
|
|
print_usage_tags(String, MaxC, ShCode, Version),
|
|
print_usage_commands2(String, MaxC, ShCode, Version);
|
|
tag ->
|
|
print_usage_tags(String, MaxC, ShCode, Version);
|
|
command ->
|
|
print_usage_commands2(String, MaxC, ShCode, Version)
|
|
end,
|
|
?STATUS_SUCCESS
|
|
end;
|
|
|
|
process(["--version", Arg | Args], _) ->
|
|
Version =
|
|
try
|
|
list_to_integer(Arg)
|
|
catch _:_ ->
|
|
throw({invalid_version, Arg})
|
|
end,
|
|
process(Args, Version);
|
|
|
|
process(Args, Version) ->
|
|
{String, Code} = process2(Args, [], Version),
|
|
case String of
|
|
[] -> ok;
|
|
_ ->
|
|
io:format("~ts~n", [String])
|
|
end,
|
|
Code.
|
|
|
|
-spec process2(Args::[string()], AccessCommands::any()) ->
|
|
{String::string(), Code::integer()}.
|
|
process2(Args, AccessCommands) ->
|
|
process2(Args, AccessCommands, ?DEFAULT_VERSION).
|
|
|
|
process2(["--auth", User, Server, Pass | Args], AccessCommands, Version) ->
|
|
process2(Args, AccessCommands, {list_to_binary(User), list_to_binary(Server),
|
|
list_to_binary(Pass), true}, Version);
|
|
process2(Args, AccessCommands, Version) ->
|
|
process2(Args, AccessCommands, noauth, Version).
|
|
|
|
|
|
|
|
process2(Args, AccessCommands, Auth, Version) ->
|
|
case try_run_ctp(Args, Auth, AccessCommands, Version) of
|
|
{String, wrong_command_arguments}
|
|
when is_list(String) ->
|
|
io:format(lists:flatten(["\n" | String]++["\n"])),
|
|
[CommandString | _] = Args,
|
|
process(["help" | [CommandString]], Version),
|
|
{lists:flatten(String), ?STATUS_ERROR};
|
|
{String, Code}
|
|
when is_list(String) and is_integer(Code) ->
|
|
{lists:flatten(String), Code};
|
|
String
|
|
when is_list(String) ->
|
|
{lists:flatten(String), ?STATUS_SUCCESS};
|
|
Code
|
|
when is_integer(Code) ->
|
|
{"", Code};
|
|
Other ->
|
|
{"Erroneous result: " ++ io_lib:format("~p", [Other]), ?STATUS_ERROR}
|
|
end.
|
|
|
|
determine_string_type(String, Version) ->
|
|
TagsCommands = ejabberd_commands:get_tags_commands(Version),
|
|
CommandsNames = case lists:keysearch(String, 1, TagsCommands) of
|
|
{value, {String, CNs}} -> CNs;
|
|
false -> []
|
|
end,
|
|
AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)],
|
|
Cmds = filter_commands(AllCommandsNames, String),
|
|
case {CommandsNames, Cmds} of
|
|
{[], []} -> no_idea;
|
|
{[], _} -> command;
|
|
{_, []} -> tag;
|
|
{_, _} -> both
|
|
end.
|
|
|
|
%%-----------------------------
|
|
%% Command calling
|
|
%%-----------------------------
|
|
|
|
try_run_ctp(Args, Auth, AccessCommands, Version) ->
|
|
try ejabberd_hooks:run_fold(ejabberd_ctl_process, false, [Args]) of
|
|
false when Args /= [] ->
|
|
try_call_command(Args, Auth, AccessCommands, Version);
|
|
false ->
|
|
print_usage(Version),
|
|
{"", ?STATUS_USAGE};
|
|
Status ->
|
|
{"", Status}
|
|
catch
|
|
exit:Why ->
|
|
print_usage(Version),
|
|
{io_lib:format("Error in ejabberd ctl process: ~p", [Why]), ?STATUS_USAGE};
|
|
Error:Why ->
|
|
%% In this case probably ejabberd is not started, so let's show Status
|
|
process(["status"], Version),
|
|
print("~n", []),
|
|
{io_lib:format("Error in ejabberd ctl process: '~p' ~p", [Error, Why]), ?STATUS_USAGE}
|
|
end.
|
|
|
|
try_call_command(Args, Auth, AccessCommands, Version) ->
|
|
try call_command(Args, Auth, AccessCommands, Version) of
|
|
{Reason, wrong_command_arguments} ->
|
|
{Reason, ?STATUS_ERROR};
|
|
Res ->
|
|
Res
|
|
catch
|
|
throw:{error, unknown_command} ->
|
|
KnownCommands = [Cmd || {Cmd, _, _} <- ejabberd_commands:list_commands(Version)],
|
|
UnknownCommand = list_to_atom(hd(Args)),
|
|
{io_lib:format(
|
|
"Error: unknown command '~ts'. Did you mean '~ts'?",
|
|
[hd(Args), misc:best_match(UnknownCommand, KnownCommands)]),
|
|
?STATUS_ERROR};
|
|
throw:Error ->
|
|
{io_lib:format("~p", [Error]), ?STATUS_ERROR};
|
|
?EX_RULE(A, Why, Stack) ->
|
|
StackTrace = ?EX_STACK(Stack),
|
|
{io_lib:format("Unhandled exception occurred executing the command:~n** ~ts",
|
|
[misc:format_exception(2, A, Why, StackTrace)]),
|
|
?STATUS_ERROR}
|
|
end.
|
|
|
|
-spec call_command(Args::[string()],
|
|
Auth::noauth | {binary(), binary(), binary(), true},
|
|
AccessCommands::[any()],
|
|
Version::integer()) ->
|
|
string() | integer() | {string(), integer()} | {error, ErrorType::any()}.
|
|
call_command([CmdString | Args], Auth, _AccessCommands, Version) ->
|
|
CmdStringU = ejabberd_regexp:greplace(
|
|
list_to_binary(CmdString), <<"-">>, <<"_">>),
|
|
Command = list_to_atom(binary_to_list(CmdStringU)),
|
|
{ArgsFormat, _, ResultFormat} = ejabberd_commands:get_command_format(Command, Auth, Version),
|
|
case (catch format_args(Args, ArgsFormat)) of
|
|
ArgsFormatted when is_list(ArgsFormatted) ->
|
|
CI = case Auth of
|
|
{U, S, _, _} -> #{usr => {U, S, <<"">>}, caller_host => S};
|
|
_ -> #{}
|
|
end,
|
|
CI2 = CI#{caller_module => ?MODULE},
|
|
Result = ejabberd_commands:execute_command2(Command,
|
|
ArgsFormatted,
|
|
CI2,
|
|
Version),
|
|
format_result(Result, ResultFormat);
|
|
{'EXIT', {function_clause,[{lists,zip,[A1, A2], _} | _]}} ->
|
|
{NumCompa, TextCompa} =
|
|
case {length(A1), length(A2)} of
|
|
{L1, L2} when L1 < L2 -> {L2-L1, "less argument"};
|
|
{L1, L2} when L1 > L2 -> {L1-L2, "more argument"}
|
|
end,
|
|
process(["help" | [CmdString]]),
|
|
{io_lib:format("Error: the command '~ts' requires ~p ~ts.",
|
|
[CmdString, NumCompa, TextCompa]),
|
|
wrong_command_arguments}
|
|
end.
|
|
|
|
|
|
%%-----------------------------
|
|
%% Format arguments
|
|
%%-----------------------------
|
|
|
|
format_args(Args, ArgsFormat) ->
|
|
lists:foldl(
|
|
fun({{_ArgName, ArgFormat}, Arg}, Res) ->
|
|
Formatted = format_arg(Arg, ArgFormat),
|
|
Res ++ [Formatted]
|
|
end,
|
|
[],
|
|
lists:zip(ArgsFormat, Args)).
|
|
|
|
format_arg(Arg, integer) ->
|
|
format_arg2(Arg, "~d");
|
|
format_arg(Arg, binary) ->
|
|
unicode:characters_to_binary(Arg, utf8);
|
|
format_arg("", string) ->
|
|
"";
|
|
format_arg(Arg, string) ->
|
|
NumChars = integer_to_list(length(Arg)),
|
|
Parse = "~" ++ NumChars ++ "c",
|
|
format_arg2(Arg, Parse);
|
|
format_arg(Arg, Format) ->
|
|
S = unicode:characters_to_binary(Arg, utf8),
|
|
JSON = jiffy:decode(S),
|
|
mod_http_api:format_arg(JSON, Format).
|
|
|
|
format_arg2(Arg, Parse)->
|
|
{ok, [Arg2], _RemainingArguments} = io_lib:fread(Parse, Arg),
|
|
Arg2.
|
|
|
|
%%-----------------------------
|
|
%% Format result
|
|
%%-----------------------------
|
|
|
|
format_result({error, ErrorAtom}, _) ->
|
|
{io_lib:format("Error: ~p", [ErrorAtom]), make_status(error)};
|
|
|
|
%% An error should always be allowed to return extended error to help with API.
|
|
%% Extended error is of the form:
|
|
%% {error, type :: atom(), code :: int(), Desc :: string()}
|
|
format_result({error, ErrorAtom, Code, Msg}, _) ->
|
|
{io_lib:format("Error: ~p: ~s", [ErrorAtom, Msg]), make_status(Code)};
|
|
|
|
format_result(Atom, {_Name, atom}) ->
|
|
io_lib:format("~p", [Atom]);
|
|
|
|
format_result(Int, {_Name, integer}) ->
|
|
io_lib:format("~p", [Int]);
|
|
|
|
format_result([A|_]=String, {_Name, string}) when is_list(String) and is_integer(A) ->
|
|
io_lib:format("~ts", [String]);
|
|
|
|
format_result(Binary, {_Name, string}) when is_binary(Binary) ->
|
|
io_lib:format("~ts", [binary_to_list(Binary)]);
|
|
|
|
format_result(Atom, {_Name, string}) when is_atom(Atom) ->
|
|
io_lib:format("~ts", [atom_to_list(Atom)]);
|
|
|
|
format_result(Integer, {_Name, string}) when is_integer(Integer) ->
|
|
io_lib:format("~ts", [integer_to_list(Integer)]);
|
|
|
|
format_result(Other, {_Name, string}) ->
|
|
io_lib:format("~p", [Other]);
|
|
|
|
format_result(Code, {_Name, rescode}) ->
|
|
make_status(Code);
|
|
|
|
format_result({Code, Text}, {_Name, restuple}) ->
|
|
{io_lib:format("~ts", [Text]), make_status(Code)};
|
|
|
|
%% The result is a list of something: [something()]
|
|
format_result([], {_Name, {list, _ElementsDef}}) ->
|
|
"";
|
|
format_result([FirstElement | Elements], {_Name, {list, ElementsDef}}) ->
|
|
%% Start formatting the first element
|
|
[format_result(FirstElement, ElementsDef) |
|
|
%% If there are more elements, put always first a newline character
|
|
lists:map(
|
|
fun(Element) ->
|
|
["\n" | format_result(Element, ElementsDef)]
|
|
end,
|
|
Elements)];
|
|
|
|
%% The result is a tuple with several elements: {something1(), something2(),...}
|
|
%% NOTE: the elements in the tuple are separated with tabular characters,
|
|
%% if a string is empty, it will be difficult to notice in the shell,
|
|
%% maybe a different separation character should be used, like ;;?
|
|
format_result(ElementsTuple, {_Name, {tuple, ElementsDef}}) ->
|
|
ElementsList = tuple_to_list(ElementsTuple),
|
|
[{FirstE, FirstD} | ElementsAndDef] = lists:zip(ElementsList, ElementsDef),
|
|
[format_result(FirstE, FirstD) |
|
|
lists:map(
|
|
fun({Element, ElementDef}) ->
|
|
["\t" | format_result(Element, ElementDef)]
|
|
end,
|
|
ElementsAndDef)];
|
|
|
|
format_result(404, {_Name, _}) ->
|
|
make_status(not_found).
|
|
|
|
make_status(ok) -> ?STATUS_SUCCESS;
|
|
make_status(true) -> ?STATUS_SUCCESS;
|
|
make_status(Code) when is_integer(Code), Code > 255 -> ?STATUS_ERROR;
|
|
make_status(Code) when is_integer(Code), Code > 0 -> Code;
|
|
make_status(Error) ->
|
|
io:format("Error: ~p~n", [Error]),
|
|
?STATUS_ERROR.
|
|
|
|
get_list_commands(Version) ->
|
|
try ejabberd_commands:list_commands(Version) of
|
|
Commands ->
|
|
[tuple_command_help(Command)
|
|
|| {N,_,_}=Command <- Commands,
|
|
%% Don't show again those commands, because they are already
|
|
%% announced by ejabberd_ctl itself
|
|
N /= status, N /= stop, N /= restart]
|
|
catch
|
|
exit:_ ->
|
|
[]
|
|
end.
|
|
|
|
%% Return: {string(), [string()], string()}
|
|
tuple_command_help({Name, _Args, Desc}) ->
|
|
{Args, _, _} = ejabberd_commands:get_command_format(Name, admin),
|
|
Arguments = [atom_to_list(ArgN) || {ArgN, _ArgF} <- Args],
|
|
Prepend = case is_supported_args(Args) of
|
|
true -> "";
|
|
false -> "*"
|
|
end,
|
|
CallString = atom_to_list(Name),
|
|
{CallString, Arguments, Prepend ++ Desc}.
|
|
|
|
is_supported_args(Args) ->
|
|
lists:all(
|
|
fun({_Name, Format}) ->
|
|
(Format == integer)
|
|
or (Format == string)
|
|
or (Format == binary)
|
|
end,
|
|
Args).
|
|
|
|
%%-----------------------------
|
|
%% Print help
|
|
%%-----------------------------
|
|
|
|
%% Commands are Bold
|
|
-define(B1, "\e[1m").
|
|
-define(B2, "\e[22m").
|
|
-define(C(S), case ShCode of true -> [?B1, S, ?B2]; false -> S end).
|
|
|
|
%% Arguments are Dim
|
|
-define(D1, "\e[2m").
|
|
-define(D2, "\e[22m").
|
|
-define(A(S), case ShCode of true -> [?D1, S, ?D2]; false -> S end).
|
|
|
|
%% Tags are Underline
|
|
-define(U1, "\e[4m").
|
|
-define(U2, "\e[24m").
|
|
-define(G(S), case ShCode of true -> [?U1, S, ?U2]; false -> S end).
|
|
|
|
%% B are Nothing
|
|
-define(N1, "\e[0m").
|
|
-define(N2, "\e[0m").
|
|
-define(B(S), case ShCode of true -> [?N1, S, ?N2]; false -> S end).
|
|
|
|
print_usage(Version) ->
|
|
{MaxC, ShCode} = get_shell_info(),
|
|
print_usage(dual, MaxC, ShCode, Version).
|
|
print_usage(HelpMode, MaxC, ShCode, Version) ->
|
|
AllCommands =
|
|
[
|
|
{"help", ["[arguments]"], "Get help"},
|
|
{"status", [], "Get ejabberd status"},
|
|
{"stop", [], "Stop ejabberd"},
|
|
{"restart", [], "Restart ejabberd"},
|
|
{"mnesia", ["[info]"], "show information of Mnesia system"}] ++
|
|
get_list_commands(Version),
|
|
|
|
print(
|
|
["Usage: ", "ejabberdctl", " [--no-timeout] [--node ", ?A("nodename"), "] [--version ", ?A("api_version"), "] ",
|
|
?C("command"), " [", ?A("arguments"), "]\n"
|
|
"\n"
|
|
"Available commands in this ejabberd node:\n"], []),
|
|
print_usage_commands(HelpMode, MaxC, ShCode, AllCommands).
|
|
|
|
print_usage_commands(HelpMode, MaxC, ShCode, Commands) ->
|
|
CmdDescsSorted = lists:keysort(1, Commands),
|
|
|
|
%% What is the length of the largest command?
|
|
{CmdArgsLenDescsSorted, Lens} =
|
|
lists:mapfoldl(
|
|
fun({Cmd, Args, Desc}, Lengths) ->
|
|
Len =
|
|
length(Cmd) +
|
|
lists:foldl(fun(Arg, R) ->
|
|
R + 1 + length(Arg)
|
|
end,
|
|
0,
|
|
Args),
|
|
{{Cmd, Args, Len, Desc}, [Len | Lengths]}
|
|
end,
|
|
[],
|
|
CmdDescsSorted),
|
|
MaxCmdLen = case Lens of
|
|
[] -> 80;
|
|
_ -> lists:max(Lens)
|
|
end,
|
|
|
|
%% For each command in the list of commands
|
|
%% Convert its definition to a line
|
|
FmtCmdDescs = format_command_lines(CmdArgsLenDescsSorted, MaxCmdLen, MaxC, ShCode, HelpMode),
|
|
|
|
print([FmtCmdDescs], []).
|
|
|
|
|
|
%% Get some info about the shell:
|
|
%% how many columns of width
|
|
%% and guess if it supports text formatting codes.
|
|
get_shell_info() ->
|
|
%% This function was introduced in OTP R12B-0
|
|
try io:columns() of
|
|
{ok, C} -> {C-2, true};
|
|
{error, enotsup} -> {78, false}
|
|
catch
|
|
_:_ -> {78, false}
|
|
end.
|
|
|
|
%% Erlang/OTP 20.0 introduced string:find/2, but we must support old 19.3
|
|
string_find([], _SearchPattern) ->
|
|
nomatch;
|
|
string_find([A | String], [A]) ->
|
|
String;
|
|
string_find([_ | String], SearchPattern) ->
|
|
string_find(String, SearchPattern).
|
|
|
|
%% Split this command description in several lines of proper length
|
|
prepare_description(DescInit, MaxC, Desc) ->
|
|
case string_find(Desc, "\n") of
|
|
nomatch ->
|
|
prepare_description2(DescInit, MaxC, Desc);
|
|
_ ->
|
|
Desc
|
|
end.
|
|
|
|
prepare_description2(DescInit, MaxC, Desc) ->
|
|
Words = string:tokens(Desc, " "),
|
|
prepare_long_line(DescInit, MaxC, Words).
|
|
|
|
prepare_long_line(DescInit, MaxC, Words) ->
|
|
MaxSegmentLen = MaxC - DescInit,
|
|
MarginString = lists:duplicate(DescInit, $\s), % Put spaces
|
|
[FirstSegment | MoreSegments] = split_desc_segments(MaxSegmentLen, Words),
|
|
MoreSegmentsMixed = mix_desc_segments(MarginString, MoreSegments),
|
|
[FirstSegment | MoreSegmentsMixed].
|
|
|
|
mix_desc_segments(MarginString, Segments) ->
|
|
[["\n", MarginString, Segment] || Segment <- Segments].
|
|
|
|
split_desc_segments(MaxL, Words) ->
|
|
join(MaxL, Words).
|
|
|
|
%% Join words in a segment,
|
|
%% but stop adding to a segment if adding this word would pass L
|
|
join(L, Words) ->
|
|
join(L, Words, 0, [], []).
|
|
|
|
join(_Len, [], _CurSegLen, CurSeg, AllSegs) ->
|
|
lists:reverse([CurSeg | AllSegs]);
|
|
join(Len, [Word | Tail], CurSegLen, CurSeg, AllSegs) ->
|
|
WordLen = length(Word),
|
|
SegSize = WordLen + CurSegLen + 1,
|
|
{NewCurSeg, NewAllSegs, NewCurSegLen} =
|
|
if SegSize < Len ->
|
|
{[CurSeg, " ", Word], AllSegs, SegSize};
|
|
true ->
|
|
{Word, [CurSeg | AllSegs], WordLen}
|
|
end,
|
|
NewLen = case string:str(Word, "\n") of
|
|
0 ->
|
|
NewCurSegLen;
|
|
_ ->
|
|
0
|
|
end,
|
|
join(Len, Tail, NewLen, NewCurSeg, NewAllSegs).
|
|
|
|
|
|
format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual)
|
|
when MaxC - MaxCmdLen < 40 ->
|
|
%% If the space available for descriptions is too narrow, enforce long help mode
|
|
format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, long);
|
|
|
|
format_command_lines(CALD, _MaxCmdLen, _MaxC, ShCode, short) ->
|
|
lists:map(
|
|
fun({Cmd, Args, _CmdArgsL, _Desc}) ->
|
|
[" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args], "\n"]
|
|
end, CALD);
|
|
|
|
format_command_lines(CALD, MaxCmdLen, MaxC, ShCode, dual) ->
|
|
lists:map(
|
|
fun({Cmd, Args, CmdArgsL, Desc}) ->
|
|
DescFmt = prepare_description(MaxCmdLen+4, MaxC, Desc),
|
|
[" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args],
|
|
lists:duplicate(MaxCmdLen - CmdArgsL + 1, $\s),
|
|
DescFmt, "\n"]
|
|
end, CALD);
|
|
|
|
format_command_lines(CALD, _MaxCmdLen, MaxC, ShCode, long) ->
|
|
lists:map(
|
|
fun({Cmd, Args, _CmdArgsL, Desc}) ->
|
|
DescFmt = prepare_description(13, MaxC, Desc),
|
|
[" ", ?C(Cmd), [[" ", ?A(Arg)] || Arg <- Args], "\n",
|
|
" ", DescFmt, "\n"]
|
|
end, CALD).
|
|
|
|
|
|
%%-----------------------------
|
|
%% Print Tags
|
|
%%-----------------------------
|
|
|
|
print_usage_tags(MaxC, ShCode, Version) ->
|
|
print("Available tags and list of commands:", []),
|
|
TagsCommands = ejabberd_commands:get_tags_commands(Version),
|
|
lists:foreach(
|
|
fun({Tag, Commands} = _TagCommands) ->
|
|
print(["\n\n ", ?G(Tag), "\n "], []),
|
|
Words = lists:sort(Commands),
|
|
Desc = prepare_long_line(5, MaxC, Words),
|
|
print(?C(Desc), [])
|
|
end,
|
|
TagsCommands),
|
|
print("\n\n", []).
|
|
|
|
print_usage_tags_long(MaxC, ShCode, Version) ->
|
|
print("Available tags and commands details:", []),
|
|
TagsCommands = ejabberd_commands:get_tags_commands(Version),
|
|
print("\n", []),
|
|
lists:foreach(
|
|
fun({Tag, CommandsNames} = _TagCommands) ->
|
|
print(["\n ", ?G(Tag), "\n"], []),
|
|
CommandsList = lists:map(
|
|
fun(NameString) ->
|
|
C = ejabberd_commands:get_command_definition(
|
|
list_to_atom(NameString), Version),
|
|
#ejabberd_commands{name = Name,
|
|
args = Args,
|
|
desc = Desc} = C,
|
|
tuple_command_help({Name, Args, Desc})
|
|
end,
|
|
CommandsNames),
|
|
print_usage_commands(short, MaxC, ShCode, CommandsList)
|
|
end,
|
|
TagsCommands),
|
|
print("\n", []).
|
|
|
|
print_usage_tags(Tag, MaxC, ShCode, Version) ->
|
|
print(["Available commands with tag ", ?G(Tag), ":", "\n", "\n"], []),
|
|
HelpMode = long,
|
|
TagsCommands = ejabberd_commands:get_tags_commands(Version),
|
|
CommandsNames = case lists:keysearch(Tag, 1, TagsCommands) of
|
|
{value, {Tag, CNs}} -> CNs;
|
|
false -> []
|
|
end,
|
|
CommandsList = lists:map(
|
|
fun(NameString) ->
|
|
C = ejabberd_commands:get_command_definition(
|
|
list_to_atom(NameString), Version),
|
|
#ejabberd_commands{name = Name,
|
|
args = Args,
|
|
desc = Desc} = C,
|
|
tuple_command_help({Name, Args, Desc})
|
|
end,
|
|
CommandsNames),
|
|
print_usage_commands(HelpMode, MaxC, ShCode, CommandsList),
|
|
print("\n", []).
|
|
|
|
|
|
%%-----------------------------
|
|
%% Print usage of 'help' command
|
|
%%-----------------------------
|
|
|
|
print_usage_help(MaxC, ShCode) ->
|
|
LongDesc =
|
|
["This special ", ?C("help"), " command provides help of ejabberd commands.\n\n"
|
|
"The format is:\n ", ?B("ejabberdctl"), " ", ?C("help"),
|
|
" [", ?A("tags"), " | ", ?A("commands"), " | ", ?G("tag"), " | ", ?C("command"), " | ", ?C("com?*"), "]\n\n"
|
|
"The optional arguments:\n"
|
|
" ",?A("tags")," Show all tags and commands names in each tag\n"
|
|
" ",?A("commands")," Show all tags and commands details in each tag\n"
|
|
" ",?G("tag")," Show commands related to this tag\n"
|
|
" ",?C("command")," Show detailed description of this command\n"
|
|
" ",?C("com?*")," Show commands that match this glob.\n"
|
|
" (? will match a simple character, and\n"
|
|
" * will match several characters)\n"
|
|
"\n",
|
|
"Some example usages:\n",
|
|
" ejabberdctl ", ?C("help"), "\n",
|
|
" ejabberdctl ", ?C("help"), " ", ?A("tags"), "\n",
|
|
" ejabberdctl ", ?C("help"), " ", ?A("commands"), "\n",
|
|
" ejabberdctl ", ?C("help"), " ", ?G("accounts"), "\n",
|
|
" ejabberdctl ", ?C("help"), " ", ?C("register"), "\n",
|
|
" ejabberdctl ", ?C("help"), " ", ?C("regist*"), "\n",
|
|
"\n",
|
|
"Please note that 'ejabberdctl' shows all ejabberd commands,\n",
|
|
"even those that cannot be used in the shell with ejabberdctl.\n",
|
|
"Those commands can be identified because their description starts with: *"],
|
|
ArgsDef = [],
|
|
C = #ejabberd_commands{
|
|
name = help,
|
|
desc = "Show help of ejabberd commands",
|
|
longdesc = lists:flatten(LongDesc),
|
|
args = ArgsDef,
|
|
result = {help, string}},
|
|
print_usage_command2("help", C, MaxC, ShCode).
|
|
|
|
|
|
%%-----------------------------
|
|
%% Print usage command
|
|
%%-----------------------------
|
|
|
|
-spec print_usage_commands2(CmdSubString::string(), MaxC::integer(),
|
|
ShCode::boolean(), Version::integer()) -> ok.
|
|
print_usage_commands2(CmdSubString, MaxC, ShCode, Version) ->
|
|
%% Get which command names match this substring
|
|
AllCommandsNames = [atom_to_list(Name) || {Name, _, _} <- ejabberd_commands:list_commands(Version)],
|
|
Cmds = filter_commands(AllCommandsNames, CmdSubString),
|
|
case Cmds of
|
|
[] -> io:format("Error: no command found that match '~ts'~n", [CmdSubString]);
|
|
_ -> print_usage_commands3(lists:sort(Cmds), MaxC, ShCode, Version)
|
|
end.
|
|
|
|
print_usage_commands3([Cmd], MaxC, ShCode, Version) ->
|
|
print_usage_command(Cmd, MaxC, ShCode, Version);
|
|
print_usage_commands3(Cmds, MaxC, ShCode, Version) ->
|
|
CommandsList = lists:map(
|
|
fun(NameString) ->
|
|
C = ejabberd_commands:get_command_definition(
|
|
list_to_atom(NameString), Version),
|
|
#ejabberd_commands{name = Name,
|
|
args = Args,
|
|
desc = Desc} = C,
|
|
tuple_command_help({Name, Args, Desc})
|
|
end,
|
|
Cmds),
|
|
|
|
print_usage_commands(long, MaxC, ShCode, CommandsList), %% que aqui solo muestre un par de lineas
|
|
ok.
|
|
|
|
filter_commands(All, SubString) ->
|
|
case lists:member(SubString, All) of
|
|
true -> [SubString];
|
|
false -> filter_commands_regexp(All, SubString)
|
|
end.
|
|
|
|
filter_commands_regexp(All, Glob) ->
|
|
RegExp = ejabberd_regexp:sh_to_awk(list_to_binary(Glob)),
|
|
lists:filter(
|
|
fun(Command) ->
|
|
case ejabberd_regexp:run(list_to_binary(Command), RegExp) of
|
|
match ->
|
|
true;
|
|
nomatch ->
|
|
false
|
|
end
|
|
end,
|
|
All).
|
|
|
|
-spec print_usage_command(Cmd::string(), MaxC::integer(),
|
|
ShCode::boolean(), Version::integer()) -> ok.
|
|
print_usage_command(Cmd, MaxC, ShCode, Version) ->
|
|
Name = list_to_atom(Cmd),
|
|
C = ejabberd_commands:get_command_definition(Name, Version),
|
|
print_usage_command2(Cmd, C, MaxC, ShCode).
|
|
|
|
print_usage_command2(Cmd, C, MaxC, ShCode) ->
|
|
#ejabberd_commands{
|
|
tags = TagsAtoms,
|
|
definer = Definer,
|
|
desc = Desc,
|
|
args = ArgsDef,
|
|
longdesc = LongDesc,
|
|
result = ResultDef} = C,
|
|
|
|
NameFmt = [" ", ?B("Command Name"), ": ", ?C(Cmd), "\n"],
|
|
|
|
%% Initial indentation of result is 13 = length(" Arguments: ")
|
|
Args = [format_usage_ctype(ArgDef, 13) || ArgDef <- ArgsDef],
|
|
ArgsMargin = lists:duplicate(13, $\s),
|
|
ArgsListFmt = case Args of
|
|
[] -> "\n";
|
|
_ -> [ [Arg, "\n", ArgsMargin] || Arg <- Args]
|
|
end,
|
|
ArgsFmt = [" ", ?B("Arguments"), ": ", ArgsListFmt],
|
|
|
|
%% Initial indentation of result is 11 = length(" Returns: ")
|
|
ResultFmt = format_usage_ctype(ResultDef, 11),
|
|
ReturnsFmt = [" ",?B("Returns"),": ", ResultFmt],
|
|
|
|
XmlrpcFmt = "", %%+++ [" ",?B("XML-RPC"),": ", format_usage_xmlrpc(ArgsDef, ResultDef), "\n\n"],
|
|
|
|
TagsFmt = [" ",?B("Tags"),":", prepare_long_line(8, MaxC, [?G(atom_to_list(TagA)) || TagA <- TagsAtoms])],
|
|
|
|
IsDefinerMod = case Definer of
|
|
unknown -> true;
|
|
_ -> lists:member(gen_mod, proplists:get_value(behaviour, Definer:module_info(attributes)))
|
|
end,
|
|
ModuleFmt = case IsDefinerMod of
|
|
true -> [" ",?B("Module"),": ", atom_to_list(Definer), "\n\n"];
|
|
false -> []
|
|
end,
|
|
|
|
DescFmt = [" ",?B("Description"),":", prepare_description(15, MaxC, Desc)],
|
|
|
|
LongDescFmt = case LongDesc of
|
|
"" -> "";
|
|
_ -> ["", prepare_description(0, MaxC, LongDesc), "\n\n"]
|
|
end,
|
|
|
|
NoteEjabberdctl = case is_supported_args(ArgsDef) of
|
|
true -> "";
|
|
false -> [" ", ?B("Note:"), " This command cannot be executed using ejabberdctl. Try ejabberd_xmlrpc.\n\n"]
|
|
end,
|
|
|
|
case Cmd of
|
|
"help" -> ok;
|
|
_ -> print([NameFmt, "\n", ArgsFmt, "\n", ReturnsFmt,
|
|
"\n\n", XmlrpcFmt, TagsFmt, "\n\n", ModuleFmt, DescFmt, "\n\n"], [])
|
|
end,
|
|
print([LongDescFmt, NoteEjabberdctl], []).
|
|
|
|
format_usage_ctype(Type, _Indentation)
|
|
when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary) or (Type==rescode) or (Type==restuple)->
|
|
io_lib:format("~p", [Type]);
|
|
|
|
format_usage_ctype({Name, Type}, _Indentation)
|
|
when (Type==atom) or (Type==integer) or (Type==string) or (Type==binary) or (Type==rescode) or (Type==restuple)->
|
|
io_lib:format("~p::~p", [Name, Type]);
|
|
|
|
format_usage_ctype({Name, {list, ElementDef}}, Indentation) ->
|
|
NameFmt = atom_to_list(Name),
|
|
Indentation2 = Indentation + length(NameFmt) + 4,
|
|
ElementFmt = format_usage_ctype(ElementDef, Indentation2),
|
|
[NameFmt, "::[ ", ElementFmt, " ]"];
|
|
|
|
format_usage_ctype({Name, {tuple, ElementsDef}}, Indentation) ->
|
|
NameFmt = atom_to_list(Name),
|
|
Indentation2 = Indentation + length(NameFmt) + 4,
|
|
ElementsFmt = format_usage_tuple(ElementsDef, Indentation2),
|
|
[NameFmt, "::{ " | ElementsFmt].
|
|
|
|
format_usage_tuple([], _Indentation) ->
|
|
[];
|
|
format_usage_tuple([ElementDef], Indentation) ->
|
|
[format_usage_ctype(ElementDef, Indentation) , " }"];
|
|
format_usage_tuple([ElementDef | ElementsDef], Indentation) ->
|
|
ElementFmt = format_usage_ctype(ElementDef, Indentation),
|
|
MarginString = lists:duplicate(Indentation, $\s), % Put spaces
|
|
[ElementFmt, ",\n", MarginString, format_usage_tuple(ElementsDef, Indentation)].
|
|
|
|
print(Format, Args) ->
|
|
io:format(lists:flatten(Format), Args).
|
|
|
|
-ifdef(LAGER).
|
|
disable_logging() ->
|
|
ok.
|
|
-else.
|
|
disable_logging() ->
|
|
logger:set_primary_config(level, none).
|
|
-endif.
|