Comparison

plugins/mod_admin_shell.lua @ 13732:1465b1e305df 13.0

mod_admin_shell, util.prosodyctl.shell: Process command-line args on server-side, with argparse support This allow a shell-command to provide a 'flags' field, which will automatically cause the parameters to be fed through argparse. The rationale is to make it easier for more complex commands to be invoked from the command line (`prosodyctl shell foo bar ...`). Until now they were limited to accepting a list of strings, and any complex argument processing was non-standard and awkward to implement.
author Matthew Wild <mwild1@gmail.com>
date Mon, 17 Feb 2025 17:02:35 +0000
parent 13687:a00d0e2dc33a
child 13734:c133635d0bc6
comparison
equal deleted inserted replaced
13731:d78e0f422464 13732:1465b1e305df
17 local helpers = require "prosody.util.helpers"; 17 local helpers = require "prosody.util.helpers";
18 local it = require "prosody.util.iterators"; 18 local it = require "prosody.util.iterators";
19 local server = require "prosody.net.server"; 19 local server = require "prosody.net.server";
20 local schema = require "prosody.util.jsonschema"; 20 local schema = require "prosody.util.jsonschema";
21 local st = require "prosody.util.stanza"; 21 local st = require "prosody.util.stanza";
22 local parse_args = require "prosody.util.argparse".parse;
22 23
23 local _G = _G; 24 local _G = _G;
24 25
25 local prosody = _G.prosody; 26 local prosody = _G.prosody;
26 27
253 session.env.output:configure(); 254 session.env.output:configure();
254 255
255 return session; 256 return session;
256 end 257 end
257 258
259 local function process_cmd_line(arg_line)
260 local chunk = load("return "..arg_line, "=shell", "t", {});
261 local ok, args = pcall(chunk);
262 if not ok then return nil, args; end
263
264 local section_name, command = args[1], args[2];
265
266 local section_mt = getmetatable(def_env[section_name]);
267 local section_help = section_mt and section_mt.help;
268 local command_help = section_help.commands[command];
269
270 local fmt = { "%s"; ":%s("; ")" };
271
272 local flags;
273 if command_help.flags then
274 flags = parse_args(args, command_help.flags);
275
276 table.remove(flags, 2);
277 table.remove(flags, 1);
278
279 local n_fixed_args = #command_help.args;
280
281 local arg_str = {};
282 for i = 1, n_fixed_args do
283 if flags[i] ~= nil then
284 table.insert(arg_str, ("%q"):format(flags[i]));
285 else
286 table.insert(arg_str, "nil");
287 end
288 end
289
290 table.insert(arg_str, "flags");
291
292 for i = n_fixed_args + 1, #flags do
293 if flags[i] ~= nil then
294 table.insert(arg_str, ("%q"):format(flags[i]));
295 else
296 table.insert(arg_str, "nil");
297 end
298 end
299
300 table.insert(fmt, 3, "%s");
301
302 return "local flags = ...; return "..string.format(table.concat(fmt), section_name, command, table.concat(arg_str, ", ")), flags;
303 end
304
305 for i = 3, #args do
306 if args[i]:sub(1, 1) == ":" then
307 table.insert(fmt, i, ")%s(");
308 elseif i > 3 and fmt[i - 1]:match("%%q$") then
309 table.insert(fmt, i, ", %q");
310 else
311 table.insert(fmt, i, "%q");
312 end
313 end
314
315 return "return "..string.format(table.concat(fmt), table.unpack(args));
316 end
317
258 local function handle_line(event) 318 local function handle_line(event)
259 local session = event.origin.shell_session; 319 local session = event.origin.shell_session;
260 if not session then 320 if not session then
261 session = console:new_session(event.origin); 321 session = console:new_session(event.origin);
262 event.origin.shell_session = session; 322 event.origin.shell_session = session;
293 353
294 if useglobalenv and not session.globalenv then 354 if useglobalenv and not session.globalenv then
295 session.globalenv = redirect_output(_G, session); 355 session.globalenv = redirect_output(_G, session);
296 end 356 end
297 357
358 local function send_result(taskok, message)
359 if not message then
360 if type(taskok) ~= "string" and useglobalenv then
361 taskok = session.serialize(taskok);
362 end
363 result:text("Result: "..tostring(taskok));
364 elseif (not taskok) and message then
365 result.attr.type = "error";
366 result:text("Error: "..tostring(message));
367 else
368 result:text("OK: "..tostring(message));
369 end
370
371 event.origin.send(result);
372 end
373
374 local taskok, message;
375 local env = (useglobalenv and session.globalenv) or session.env or nil;
376 local flags;
377
378 local source;
379 if line:match("^{") then
380 -- Input is a serialized array of strings, typically from
381 -- a command-line invocation of 'prosodyctl shell something'
382 source, flags = process_cmd_line(line);
383 end
384
298 local chunkname = "=console"; 385 local chunkname = "=console";
299 local env = (useglobalenv and session.globalenv) or session.env or nil
300 -- luacheck: ignore 311/err 386 -- luacheck: ignore 311/err
301 local chunk, err = envload("return "..line, chunkname, env); 387 local chunk, err = envload(source or ("return "..line), chunkname, env);
302 if not chunk then 388 if not chunk then
303 chunk, err = envload(line, chunkname, env); 389 if not source then
390 chunk, err = envload(line, chunkname, env);
391 end
304 if not chunk then 392 if not chunk then
305 err = err:gsub("^%[string .-%]:%d+: ", ""); 393 err = err:gsub("^%[string .-%]:%d+: ", "");
306 err = err:gsub("^:%d+: ", ""); 394 err = err:gsub("^:%d+: ", "");
307 err = err:gsub("'<eof>'", "the end of the line"); 395 err = err:gsub("'<eof>'", "the end of the line");
308 result.attr.type = "error"; 396 result.attr.type = "error";
310 event.origin.send(result); 398 event.origin.send(result);
311 return; 399 return;
312 end 400 end
313 end 401 end
314 402
315 local function send_result(taskok, message) 403 taskok, message = chunk(flags);
316 if not message then
317 if type(taskok) ~= "string" and useglobalenv then
318 taskok = session.serialize(taskok);
319 end
320 result:text("Result: "..tostring(taskok));
321 elseif (not taskok) and message then
322 result.attr.type = "error";
323 result:text("Error: "..tostring(message));
324 else
325 result:text("OK: "..tostring(message));
326 end
327
328 event.origin.send(result);
329 end
330
331 local taskok, message = chunk();
332 404
333 if promise.is_promise(taskok) then 405 if promise.is_promise(taskok) then
334 taskok:next(function (resolved_message) 406 taskok:next(function (resolved_message)
335 send_result(true, resolved_message); 407 send_result(true, resolved_message);
336 end, function (rejected_message) 408 end, function (rejected_message)
2639 commands = {}; 2711 commands = {};
2640 }; 2712 };
2641 section_mt.help = section_help; 2713 section_mt.help = section_help;
2642 end 2714 end
2643 2715
2716 if command.flags and command.flags.stop_on_positional == nil then
2717 command.flags.stop_on_positional = false;
2718 end
2719
2644 section_help.commands[command.name] = { 2720 section_help.commands[command.name] = {
2645 desc = command.desc; 2721 desc = command.desc;
2646 full = command.help; 2722 full = command.help;
2647 args = array(command.args); 2723 args = array(command.args);
2724 flags = command.flags;
2648 module = command._provided_by; 2725 module = command._provided_by;
2649 }; 2726 };
2650 2727
2651 module:log("debug", "Shell command added by %s: %s:%s()", mod_name, command.section, command.name); 2728 module:log("debug", "Shell command added by %s: %s:%s()", mod_name, command.section, command.name);
2652 end 2729 end