Software / code / prosody
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 |