Changeset

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
parents 13731:d78e0f422464
children 13733:48c056c10e5a
files plugins/mod_admin_shell.lua util/prosodyctl/shell.lua
diffstat 2 files changed, 96 insertions(+), 29 deletions(-) [+]
line wrap: on
line diff
--- a/plugins/mod_admin_shell.lua	Mon Feb 17 16:38:48 2025 +0000
+++ b/plugins/mod_admin_shell.lua	Mon Feb 17 17:02:35 2025 +0000
@@ -19,6 +19,7 @@
 local server = require "prosody.net.server";
 local schema = require "prosody.util.jsonschema";
 local st = require "prosody.util.stanza";
+local parse_args = require "prosody.util.argparse".parse;
 
 local _G = _G;
 
@@ -255,6 +256,65 @@
 	return session;
 end
 
+local function process_cmd_line(arg_line)
+	local chunk = load("return "..arg_line, "=shell", "t", {});
+	local ok, args = pcall(chunk);
+	if not ok then return nil, args; end
+
+	local section_name, command = args[1], args[2];
+
+	local section_mt = getmetatable(def_env[section_name]);
+	local section_help = section_mt and section_mt.help;
+	local command_help = section_help.commands[command];
+
+	local fmt = { "%s"; ":%s("; ")" };
+
+	local flags;
+	if command_help.flags then
+		flags = parse_args(args, command_help.flags);
+
+		table.remove(flags, 2);
+		table.remove(flags, 1);
+
+		local n_fixed_args = #command_help.args;
+
+		local arg_str = {};
+		for i = 1, n_fixed_args do
+			if flags[i] ~= nil then
+				table.insert(arg_str, ("%q"):format(flags[i]));
+			else
+				table.insert(arg_str, "nil");
+			end
+		end
+
+		table.insert(arg_str, "flags");
+
+		for i = n_fixed_args + 1, #flags do
+			if flags[i] ~= nil then
+				table.insert(arg_str, ("%q"):format(flags[i]));
+			else
+				table.insert(arg_str, "nil");
+			end
+		end
+
+		table.insert(fmt, 3, "%s");
+
+		return "local flags = ...; return "..string.format(table.concat(fmt), section_name, command, table.concat(arg_str, ", ")), flags;
+	end
+
+	for i = 3, #args do
+		if args[i]:sub(1, 1) == ":" then
+			table.insert(fmt, i, ")%s(");
+		elseif i > 3 and fmt[i - 1]:match("%%q$") then
+			table.insert(fmt, i, ", %q");
+		else
+			table.insert(fmt, i, "%q");
+		end
+	end
+
+	return "return "..string.format(table.concat(fmt), table.unpack(args));
+end
+
 local function handle_line(event)
 	local session = event.origin.shell_session;
 	if not session then
@@ -295,23 +355,6 @@
 		session.globalenv = redirect_output(_G, session);
 	end
 
-	local chunkname = "=console";
-	local env = (useglobalenv and session.globalenv) or session.env or nil
-	-- luacheck: ignore 311/err
-	local chunk, err = envload("return "..line, chunkname, env);
-	if not chunk then
-		chunk, err = envload(line, chunkname, env);
-		if not chunk then
-			err = err:gsub("^%[string .-%]:%d+: ", "");
-			err = err:gsub("^:%d+: ", "");
-			err = err:gsub("'<eof>'", "the end of the line");
-			result.attr.type = "error";
-			result:text("Sorry, I couldn't understand that... "..err);
-			event.origin.send(result);
-			return;
-		end
-	end
-
 	local function send_result(taskok, message)
 		if not message then
 			if type(taskok) ~= "string" and useglobalenv then
@@ -328,7 +371,36 @@
 		event.origin.send(result);
 	end
 
-	local taskok, message = chunk();
+	local taskok, message;
+	local env = (useglobalenv and session.globalenv) or session.env or nil;
+	local flags;
+
+	local source;
+	if line:match("^{") then
+		-- Input is a serialized array of strings, typically from
+		-- a command-line invocation of 'prosodyctl shell something'
+		source, flags = process_cmd_line(line);
+	end
+
+	local chunkname = "=console";
+	-- luacheck: ignore 311/err
+	local chunk, err = envload(source or ("return "..line), chunkname, env);
+	if not chunk then
+		if not source then
+			chunk, err = envload(line, chunkname, env);
+		end
+		if not chunk then
+			err = err:gsub("^%[string .-%]:%d+: ", "");
+			err = err:gsub("^:%d+: ", "");
+			err = err:gsub("'<eof>'", "the end of the line");
+			result.attr.type = "error";
+			result:text("Sorry, I couldn't understand that... "..err);
+			event.origin.send(result);
+			return;
+		end
+	end
+
+	taskok, message = chunk(flags);
 
 	if promise.is_promise(taskok) then
 		taskok:next(function (resolved_message)
@@ -2641,10 +2713,15 @@
 			section_mt.help = section_help;
 		end
 
+		if command.flags and command.flags.stop_on_positional == nil then
+			command.flags.stop_on_positional = false;
+		end
+
 		section_help.commands[command.name] = {
 			desc = command.desc;
 			full = command.help;
 			args = array(command.args);
+			flags = command.flags;
 			module = command._provided_by;
 		};
 
--- a/util/prosodyctl/shell.lua	Mon Feb 17 16:38:48 2025 +0000
+++ b/util/prosodyctl/shell.lua	Mon Feb 17 17:02:35 2025 +0000
@@ -87,17 +87,7 @@
 
 	if arg[1] then
 		if arg[2] then
-			local fmt = { "%s"; ":%s("; ")" };
-			for i = 3, #arg do
-				if arg[i]:sub(1, 1) == ":" then
-					table.insert(fmt, i, ")%s(");
-				elseif i > 3 and fmt[i - 1]:match("%%q$") then
-					table.insert(fmt, i, ", %q");
-				else
-					table.insert(fmt, i, "%q");
-				end
-			end
-			arg[1] = string.format(table.concat(fmt), table.unpack(arg));
+			arg[1] = ("{"..string.rep("%q", #arg, ", ").."}"):format(table.unpack(arg, 1, #arg));
 		end
 
 		client.events.add_handler("connected", function()