]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
machinectl: convert to OPTION and VERB macros 42117/head
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Fri, 15 May 2026 17:19:15 +0000 (19:19 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Sat, 16 May 2026 13:59:15 +0000 (15:59 +0200)
For the shell verb we want switches specified after the program name to
be passed to the program to execute, not processed by us. Mirror the
approach in 'userdbctl ssh-authorized-keys': start with
OPTION_PARSER_RETURN_POSITIONAL_ARGS, then lates switch to
STOP_AT_FIRST_NONOPTION for "shell" or NORMAL otherwise.

VERB declarations are placed directly above each function; functions
that dispatch multiple verb names get stacked VERB() declarations.
chainload_importctl() now takes the args strv instead of relying on the
global optind.

--help output is mostly the same.
--no-pager/--no-legend/--no-ask-password/-q/--quiet are now at the end.
bind-volume/unbind-volume are documented.

Fixes c9f461a8067996c6b0c3ac3bf6f9097aedbf4734.

Co-developed-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
src/machine/machinectl.c
test/units/TEST-13-NSPAWN.machined.sh

index 657c69e2a17d68b12c17ec391d0f11971b688b5d..c0f9fd51e07c512e9896c67f57b567f7edd8d6c0 100644 (file)
@@ -1,6 +1,5 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
-#include <getopt.h>
 #include <locale.h>
 #include <net/if.h>
 #include <sys/socket.h>
@@ -33,6 +32,7 @@
 #include "format-ifname.h"
 #include "format-table.h"
 #include "format-util.h"
+#include "help-util.h"
 #include "hostname-util.h"
 #include "import-util.h"
 #include "in-addr-util.h"
@@ -44,6 +44,7 @@
 #include "machine-util.h"
 #include "main-func.h"
 #include "nulstr-util.h"
+#include "options.h"
 #include "osc-context.h"
 #include "output-mode.h"
 #include "pager.h"
@@ -284,6 +285,9 @@ static int show_table(Table *table, const char *word) {
         return 0;
 }
 
+VERB_GROUP("Machine Commands");
+
+VERB_DEFAULT_NOARG(verb_list_machines, "list", "List running VMs and containers");
 static int verb_list_machines(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
@@ -691,6 +695,10 @@ static int show_machine_properties(sd_bus *bus, const char *path, bool *new_line
         return r;
 }
 
+VERB(verb_show_machine, "status", "NAME…", 2, VERB_ANY, 0,
+     "Show VM/container details");
+VERB(verb_show_machine, "show", "[NAME…]", VERB_ANY, VERB_ANY, 0,
+     "Show properties of one or more VMs/containers");
 static int verb_show_machine(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         bool properties, new_line = false;
@@ -767,6 +775,8 @@ static int image_exists(sd_bus *bus, const char *name) {
         return 1;
 }
 
+VERB(verb_start_machine, "start", "NAME…", 2, VERB_ANY, 0,
+     "Start container as a service");
 static int verb_start_machine(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL;
@@ -934,6 +944,8 @@ static int on_machine_removed(sd_bus_message *m, void *userdata, sd_bus_error *r
         return 0;
 }
 
+VERB(verb_login_machine, "login", "[NAME]", VERB_ANY, 2, 0,
+     "Get a login prompt in a container or on the local host");
 static int verb_login_machine(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
@@ -985,6 +997,8 @@ static int verb_login_machine(int argc, char *argv[], uintptr_t _data, void *use
         return process_forward(event, slot, master, PTY_FORWARD_IGNORE_VHANGUP, machine);
 }
 
+VERB(verb_shell_machine, "shell", "[[USER@]NAME [COMMAND…]]", VERB_ANY, VERB_ANY, 0,
+     "Invoke a shell (or other command) in a container or on the local host");
 static int verb_shell_machine(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL, *m = NULL;
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
@@ -1075,6 +1089,10 @@ static int verb_shell_machine(int argc, char *argv[], uintptr_t _data, void *use
 
 static int verb_poweroff_machine(int argc, char *argv[], uintptr_t data, void *userdata);
 
+VERB(verb_enable_machine, "enable", "NAME…", 2, VERB_ANY, 0,
+     "Enable automatic container start at boot");
+VERB(verb_enable_machine, "disable", "NAME…", 2, VERB_ANY, 0,
+     "Disable automatic container start at boot");
 static int verb_enable_machine(int argc, char *argv[], uintptr_t data, void *userdata) {
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
@@ -1242,6 +1260,10 @@ static int verb_machine_control_one(const char *machine_name, const char *method
         return 0;
 }
 
+VERB(verb_poweroff_machine, "poweroff", "NAME…", 2, VERB_ANY, 0,
+     "Power off one or more machines");
+VERB(verb_poweroff_machine, "stop", "NAME…", 2, VERB_ANY, 0,
+     /* help= */ NULL); /* Convenience alias */
 static int verb_poweroff_machine(int argc, char *argv[], uintptr_t data, void *userdata) {
         sd_bus *bus = ASSERT_PTR(userdata);
         int r;
@@ -1267,6 +1289,10 @@ static int verb_poweroff_machine(int argc, char *argv[], uintptr_t data, void *u
         return 0;
 }
 
+VERB(verb_reboot_machine, "reboot", "NAME…", 2, VERB_ANY, 0,
+     "Reboot one or more machines");
+VERB(verb_reboot_machine, "restart", "NAME…", 2, VERB_ANY, 0,
+     /* help= */ NULL); /* Convenience alias */
 static int verb_reboot_machine(int argc, char *argv[], uintptr_t data, void *userdata) {
         sd_bus *bus = ASSERT_PTR(userdata);
         int r;
@@ -1305,14 +1331,20 @@ static int verb_vm_control(int argc, char *argv[], const char *method) {
         return 0;
 }
 
+VERB(verb_pause, "pause", "NAME…", 2, VERB_ANY, 0,
+     "Pause one or more machines");
 static int verb_pause(int argc, char *argv[], uintptr_t _data, void *userdata) {
         return verb_vm_control(argc, argv, "io.systemd.MachineInstance.Pause");
 }
 
+VERB(verb_resume, "resume", "NAME…", 2, VERB_ANY, 0,
+     "Resume one or more paused machines");
 static int verb_resume(int argc, char *argv[], uintptr_t _data, void *userdata) {
         return verb_vm_control(argc, argv, "io.systemd.MachineInstance.Resume");
 }
 
+VERB(verb_terminate_machine, "terminate", "NAME…", 2, VERB_ANY, 0,
+     "Terminate one or more machines");
 static int verb_terminate_machine(int argc, char *argv[], uintptr_t _data, void *userdata) {
         sd_bus *bus = ASSERT_PTR(userdata);
         int r;
@@ -1337,6 +1369,8 @@ static int verb_terminate_machine(int argc, char *argv[], uintptr_t _data, void
         return 0;
 }
 
+VERB(verb_kill_machine, "kill", "NAME…", 2, VERB_ANY, 0,
+     "Send signal to processes of a machine");
 static int verb_kill_machine(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         sd_bus *bus = ASSERT_PTR(userdata);
@@ -1369,6 +1403,10 @@ static const char* select_copy_method(bool copy_from, bool force) {
                 return copy_from ? "CopyFromMachine" : "CopyToMachine";
 }
 
+VERB(verb_copy_files, "copy-to", "NAME PATH [PATH]", 3, 4, 0,
+     "Copy files from the host to a container");
+VERB(verb_copy_files, "copy-from", "NAME PATH [PATH]", 3, 4, 0,
+     "Copy files from a container to the host");
 static int verb_copy_files(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
@@ -1424,6 +1462,8 @@ static int verb_copy_files(int argc, char *argv[], uintptr_t _data, void *userda
         return 0;
 }
 
+VERB(verb_bind_mount, "bind", "NAME PATH [PATH]", 3, 4, 0,
+     "Bind mount a path from the host into a container");
 static int verb_bind_mount(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         sd_bus *bus = ASSERT_PTR(userdata);
@@ -1449,6 +1489,10 @@ static int verb_bind_mount(int argc, char *argv[], uintptr_t _data, void *userda
         return 0;
 }
 
+VERB_GROUP("Image Commands");
+
+VERB(verb_list_images, "list-images", /* argspec= */ NULL, VERB_ANY, 1, 0,
+     "Show available container and VM images");
 static int verb_list_images(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
@@ -1757,6 +1801,10 @@ static int show_image_properties(sd_bus *bus, const char *path, bool *new_line)
         return r;
 }
 
+VERB(verb_show_image, "image-status", "[NAME…]", VERB_ANY, VERB_ANY, 0,
+     "Show image details");
+VERB(verb_show_image, "show-image", "[NAME…]", VERB_ANY, VERB_ANY, 0,
+     "Show properties of image");
 static int verb_show_image(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         bool properties, new_line = false;
@@ -1841,6 +1889,8 @@ static int get_settings_path(const char *name, char **ret_path) {
         return -ENOENT;
 }
 
+VERB(verb_edit_settings, "edit", "NAME|FILE…", 2, VERB_ANY, 0,
+     "Edit settings of one or more VMs/containers");
 static int verb_edit_settings(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(edit_file_context_done) EditFileContext context = {};
         int r;
@@ -1911,6 +1961,8 @@ static int verb_edit_settings(int argc, char *argv[], uintptr_t _data, void *use
         return do_edit_files_and_install(&context);
 }
 
+VERB(verb_cat_settings, "cat", "NAME|FILE…", 2, VERB_ANY, 0,
+     "Show settings of one or more VMs/containers");
 static int verb_cat_settings(int argc, char *argv[], uintptr_t _data, void *userdata) {
         int r = 0;
 
@@ -1962,6 +2014,8 @@ static int verb_cat_settings(int argc, char *argv[], uintptr_t _data, void *user
         return r;
 }
 
+VERB(verb_clone_image, "clone", "NAME NAME", 3, 3, 0,
+     "Clone an image");
 static int verb_clone_image(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
@@ -1986,6 +2040,8 @@ static int verb_clone_image(int argc, char *argv[], uintptr_t _data, void *userd
         return 0;
 }
 
+VERB(verb_rename_image, "rename", "NAME NAME", 3, 3, 0,
+     "Rename an image");
 static int verb_rename_image(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         sd_bus *bus = ASSERT_PTR(userdata);
@@ -2006,6 +2062,8 @@ static int verb_rename_image(int argc, char *argv[], uintptr_t _data, void *user
         return 0;
 }
 
+VERB(verb_read_only_image, "read-only", "NAME [BOOL]", 2, 3, 0,
+     "Mark or unmark image read-only");
 static int verb_read_only_image(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         sd_bus *bus = ASSERT_PTR(userdata);
@@ -2028,6 +2086,8 @@ static int verb_read_only_image(int argc, char *argv[], uintptr_t _data, void *u
         return 0;
 }
 
+VERB(verb_remove_image, "remove", "NAME…", 2, VERB_ANY, 0,
+     "Remove an image");
 static int verb_remove_image(int argc, char *argv[], uintptr_t _data, void *userdata) {
         sd_bus *bus = ASSERT_PTR(userdata);
         int r;
@@ -2055,6 +2115,8 @@ static int verb_remove_image(int argc, char *argv[], uintptr_t _data, void *user
         return 0;
 }
 
+VERB(verb_set_limit, "set-limit", "[NAME] BYTES", 2, 3, 0,
+     "Set image or pool size limit (disk quota)");
 static int verb_set_limit(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         sd_bus *bus = userdata;
@@ -2085,6 +2147,8 @@ static int verb_set_limit(int argc, char *argv[], uintptr_t _data, void *userdat
         return 0;
 }
 
+VERB(verb_clean_images, "clean", /* argspec= */ NULL, VERB_ANY, 1, 0,
+     "Remove hidden (or all) images");
 static int verb_clean_images(int argc, char *argv[], uintptr_t _data, void *userdata) {
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
@@ -2139,6 +2203,8 @@ static int verb_clean_images(int argc, char *argv[], uintptr_t _data, void *user
         return 0;
 }
 
+VERB(verb_bind_volume, "bind-volume", "MACHINE PROVIDER:VOLUME[:…]", 3, 3, 0,
+     "Attach a volume to a running machine");
 static int verb_bind_volume(int argc, char *argv[], uintptr_t _data, void *userdata) {
         int r;
 
@@ -2209,6 +2275,8 @@ static int verb_bind_volume(int argc, char *argv[], uintptr_t _data, void *userd
         return 0;
 }
 
+VERB(verb_unbind_volume, "unbind-volume", "MACHINE PROVIDER:VOLUME", 3, 3, 0,
+     "Detach a volume from a running machine");
 static int verb_unbind_volume(int argc, char *argv[], uintptr_t _data, void *userdata) {
         int r;
 
@@ -2253,11 +2321,12 @@ static int verb_unbind_volume(int argc, char *argv[], uintptr_t _data, void *use
         return 0;
 }
 
-static int chainload_importctl(int argc, char *argv[]) {
+static int chainload_importctl(char **args) {
         int r;
 
         if (!arg_quiet)
-                log_notice("The 'machinectl %1$s' command has been replaced by 'importctl -m %1$s'. Redirecting invocation.", argv[optind]);
+                log_notice("The 'machinectl %1$s' command has been replaced by 'importctl -m %1$s'. "
+                           "Redirecting invocation.", args[0]);
 
         _cleanup_strv_free_ char **c =
                 strv_new("importctl", "--class=machine");
@@ -2288,7 +2357,7 @@ static int chainload_importctl(int argc, char *argv[]) {
                 if (strv_extend_many(&c, "--format", arg_format) < 0)
                         return log_oom();
 
-        if (strv_extend_strv(&c, argv + optind, /* filter_duplicates= */ false) < 0)
+        if (strv_extend_strv(&c, args, /* filter_duplicates= */ false) < 0)
                 return log_oom();
 
         if (DEBUG_LOGGING) {
@@ -2301,465 +2370,285 @@ static int chainload_importctl(int argc, char *argv[]) {
 }
 
 static int help(void) {
-        _cleanup_free_ char *link = NULL;
+        static const char* const vgroups[] = {
+                "Machine Commands",
+                "Image Commands",
+        };
+
+        Table *vtables[ELEMENTSOF(vgroups)] = {};
+        CLEANUP_ELEMENTS(vtables, table_unref_array_clear);
+        _cleanup_(table_unrefp) Table *options = NULL;
         int r;
 
-        pager_open(arg_pager_flags);
+        for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) {
+                r = verbs_get_help_table_group(vgroups[i], &vtables[i]);
+                if (r < 0)
+                        return r;
+        }
 
-        r = terminal_urlify_man("machinectl", "1", &link);
+        r = option_parser_get_help_table(&options);
         if (r < 0)
-                return log_oom();
+                return r;
 
-        printf("%1$s [OPTIONS...] COMMAND ...\n\n"
-               "%5$sSend control commands to or query the virtual machine and container%6$s\n"
-               "%5$sregistration manager.%6$s\n"
-               "\n%3$sMachine Commands:%4$s\n"
-               "  list                        List running VMs and containers\n"
-               "  status NAME...              Show VM/container details\n"
-               "  show [NAME...]              Show properties of one or more VMs/containers\n"
-               "  start NAME...               Start container as a service\n"
-               "  login [NAME]                Get a login prompt in a container or on the\n"
-               "                              local host\n"
-               "  shell [[USER@]NAME [COMMAND...]]\n"
-               "                              Invoke a shell (or other command) in a container\n"
-               "                              or on the local host\n"
-               "  enable NAME...              Enable automatic container start at boot\n"
-               "  disable NAME...             Disable automatic container start at boot\n"
-               "  poweroff NAME...            Power off one or more machines\n"
-               "  reboot NAME...              Reboot one or more machines\n"
-               "  pause NAME...               Pause one or more machines\n"
-               "  resume NAME...              Resume one or more paused machines\n"
-               "  terminate NAME...           Terminate one or more machines\n"
-               "  kill NAME...                Send signal to processes of a machine\n"
-               "  copy-to NAME PATH [PATH]    Copy files from the host to a container\n"
-               "  copy-from NAME PATH [PATH]  Copy files from a container to the host\n"
-               "  bind NAME PATH [PATH]       Bind mount a path from the host into a container\n"
-               "\n%3$sImage Commands:%4$s\n"
-               "  list-images                 Show available container and VM images\n"
-               "  image-status [NAME...]      Show image details\n"
-               "  show-image [NAME...]        Show properties of image\n"
-               "  edit NAME|FILE...           Edit settings of one or more VMs/containers\n"
-               "  cat NAME|FILE...            Show settings of one or more VMs/containers\n"
-               "  clone NAME NAME             Clone an image\n"
-               "  rename NAME NAME            Rename an image\n"
-               "  read-only NAME [BOOL]       Mark or unmark image read-only\n"
-               "  remove NAME...              Remove an image\n"
-               "  set-limit [NAME] BYTES      Set image or pool size limit (disk quota)\n"
-               "  clean                       Remove hidden (or all) images\n"
-               "\n%3$sOptions:%4$s\n"
-               "  -h --help                   Show this help\n"
-               "     --version                Show package version\n"
-               "     --no-pager               Do not pipe output into a pager\n"
-               "     --no-legend              Do not show the headers and footers\n"
-               "     --no-ask-password        Do not ask for system passwords\n"
-               "  -H --host=[USER@]HOST       Operate on remote host\n"
-               "  -M --machine=CONTAINER      Operate on local container\n"
-               "     --system                 Connect to system machine manager\n"
-               "     --user                   Connect to user machine manager\n"
-               "  -p --property=NAME          Show only properties by this name\n"
-               "     --value                  When showing properties, only print the value\n"
-               "  -P NAME                     Equivalent to --value --property=NAME\n"
-               "  -q --quiet                  Suppress output\n"
-               "  -a --all                    Show all properties, including empty ones\n"
-               "  -l --full                   Do not ellipsize output\n"
-               "     --kill-whom=WHOM         Whom to send signal to\n"
-               "  -s --signal=SIGNAL          Which signal to send\n"
-               "     --uid=USER               Specify user ID to invoke shell as\n"
-               "  -E --setenv=VAR[=VALUE]     Add an environment variable for shell\n"
-               "     --read-only              Create read-only bind mount or clone\n"
-               "     --mkdir                  Create directory before bind mounting, if missing\n"
-               "  -n --lines=INTEGER          Number of journal entries to show\n"
-               "     --max-addresses=INTEGER  Number of internet addresses to show at most\n"
-               "  -o --output=STRING          Change journal output mode (short, short-precise,\n"
-               "                               short-iso, short-iso-precise, short-full,\n"
-               "                               short-monotonic, short-unix, short-delta,\n"
-               "                               json, json-pretty, json-sse, json-seq, cat,\n"
-               "                               verbose, export, with-unit)\n"
-               "     --force                  Replace target file when copying, if necessary\n"
-               "     --now                    Start or power off container after enabling or\n"
-               "                              disabling it\n"
-               "     --runner=RUNNER          Select between nspawn and vmspawn as the runner\n"
-               "  -V                          Short for --runner=vmspawn\n"
-               "\nSee the %2$s for details.\n",
-               program_invocation_short_name,
-               link,
-               ansi_underline(),
-               ansi_normal(),
+        assert_cc(ELEMENTSOF(vtables) == 2);
+        (void) table_sync_column_widths(0, options, vtables[0], vtables[1]);
+
+        pager_open(arg_pager_flags);
+
+        help_cmdline("[OPTIONS…] COMMAND …");
+
+        printf("\n%1$s%2$sSend control commands to or query the virtual machine and container%3$s\n"
+               "%1$s%2$sregistration manager.%3$s\n",
                ansi_highlight(),
+               ansi_add_italics(),
                ansi_normal());
 
-        return 0;
-}
+        for (size_t i = 0; i < ELEMENTSOF(vgroups); i++) {
+                help_section(vgroups[i]);
+                r = table_print_or_warn(vtables[i]);
+                if (r < 0)
+                        return r;
+        }
 
-static int verb_help(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        return help();
-}
+        help_section("Options");
+        r = table_print_or_warn(options);
+        if (r < 0)
+                return r;
 
-static int parse_argv(int argc, char *argv[]) {
-
-        enum {
-                ARG_VERSION = 0x100,
-                ARG_NO_PAGER,
-                ARG_NO_LEGEND,
-                ARG_VALUE,
-                ARG_KILL_WHOM,
-                ARG_READ_ONLY,
-                ARG_MKDIR,
-                ARG_NO_ASK_PASSWORD,
-                ARG_VERIFY,
-                ARG_RUNNER,
-                ARG_NOW,
-                ARG_FORCE,
-                ARG_FORMAT,
-                ARG_UID,
-                ARG_MAX_ADDRESSES,
-                ARG_SYSTEM,
-                ARG_USER,
-        };
+        help_man_page_reference("machinectl", "1");
 
-        static const struct option options[] = {
-                { "help",            no_argument,       NULL, 'h'                 },
-                { "version",         no_argument,       NULL, ARG_VERSION         },
-                { "property",        required_argument, NULL, 'p'                 },
-                { "value",           no_argument,       NULL, ARG_VALUE           },
-                { "all",             no_argument,       NULL, 'a'                 },
-                { "full",            no_argument,       NULL, 'l'                 },
-                { "no-pager",        no_argument,       NULL, ARG_NO_PAGER        },
-                { "no-legend",       no_argument,       NULL, ARG_NO_LEGEND       },
-                { "kill-whom",       required_argument, NULL, ARG_KILL_WHOM       },
-                { "signal",          required_argument, NULL, 's'                 },
-                { "host",            required_argument, NULL, 'H'                 },
-                { "machine",         required_argument, NULL, 'M'                 },
-                { "read-only",       no_argument,       NULL, ARG_READ_ONLY       },
-                { "mkdir",           no_argument,       NULL, ARG_MKDIR           },
-                { "quiet",           no_argument,       NULL, 'q'                 },
-                { "lines",           required_argument, NULL, 'n'                 },
-                { "output",          required_argument, NULL, 'o'                 },
-                { "no-ask-password", no_argument,       NULL, ARG_NO_ASK_PASSWORD },
-                { "verify",          required_argument, NULL, ARG_VERIFY          },
-                { "runner",          required_argument, NULL, ARG_RUNNER          },
-                { "now",             no_argument,       NULL, ARG_NOW             },
-                { "force",           no_argument,       NULL, ARG_FORCE           },
-                { "format",          required_argument, NULL, ARG_FORMAT          },
-                { "uid",             required_argument, NULL, ARG_UID             },
-                { "setenv",          required_argument, NULL, 'E'                 },
-                { "max-addresses",   required_argument, NULL, ARG_MAX_ADDRESSES   },
-                { "user",            no_argument,       NULL, ARG_USER            },
-                { "system",          no_argument,       NULL, ARG_SYSTEM          },
-                {}
-        };
+        return 0;
+}
 
-        bool reorder = false;
-        int c, r, shell = -1;
+VERB_COMMON_HELP_HIDDEN(help);
 
+static int parse_argv(int argc, char *argv[], char ***ret_args) {
         assert(argc >= 0);
         assert(argv);
+        assert(ret_args);
+
+        /* For "shell" we don't want option reordering; options specified after the command should be passed
+         * to the program to execute, and not processed by us. For other verbs, we consume all options as
+         * usual. To make this work, start with OPTION_PARSER_RETURN_POSITIONAL_ARGS and switch to either
+         * OPTION_PARSER_STOP_AT_FIRST_NONOPTION or OPTION_PARSER_NORMAL after we've seen the verb and one
+         * more argument after that. */
+        OptionParser opts = { argc, argv, OPTION_PARSER_RETURN_POSITIONAL_ARGS };
+        _cleanup_strv_free_ char **args = NULL;
+        int r;
 
-        /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
-         * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
-        optind = 0;
-
-        for (;;) {
-                static const char option_string[] = "-hp:P:als:H:M:qn:o:E:V";
-
-                c = getopt_long(argc, argv, option_string + reorder, options, NULL);
-                if (c < 0)
-                        break;
-
+        FOREACH_OPTION_OR_RETURN(c, &opts)
                 switch (c) {
 
-                case 1: /* getopt_long() returns 1 if "-" was the first character of the option string, and a
-                         * non-option argument was discovered. */
-
-                        assert(!reorder);
-
-                        /* We generally are fine with the fact that getopt_long() reorders the command line, and looks
-                         * for switches after the main verb. However, for "shell" we really don't want that, since we
-                         * want that switches specified after the machine name are passed to the program to execute,
-                         * and not processed by us. To make this possible, we'll first invoke getopt_long() with
-                         * reordering disabled (i.e. with the "-" prefix in the option string), looking for the first
-                         * non-option parameter. If it's the verb "shell" we remember its position and continue
-                         * processing options. In this case, as soon as we hit the next non-option argument we found
-                         * the machine name, and stop further processing. If the first non-option argument is any other
-                         * verb than "shell" we switch to normal reordering mode and continue processing arguments
-                         * normally. */
-
-                        if (shell >= 0) {
-                                /* If we already found the "shell" verb on the command line, and now found the next
-                                 * non-option argument, then this is the machine name and we should stop processing
-                                 * further arguments.  */
-                                optind--; /* don't process this argument, go one step back */
-                                goto done;
-                        }
-                        if (streq(optarg, "shell"))
-                                /* Remember the position of the "shell" verb, and continue processing normally. */
-                                shell = optind - 1;
-                        else {
-                                int saved_optind;
-
-                                /* OK, this is some other verb. In this case, turn on reordering again, and continue
-                                 * processing normally. */
-                                reorder = true;
-
-                                /* We changed the option string. getopt_long() only looks at it again if we invoke it
-                                 * at least once with a reset option index. Hence, let's reset the option index here,
-                                 * then invoke getopt_long() again (ignoring what it has to say, after all we most
-                                 * likely already processed it), and the bump the option index so that we read the
-                                 * intended argument again. */
-                                saved_optind = optind;
-                                optind = 0;
-                                (void) getopt_long(argc, argv, option_string + reorder, options, NULL);
-                                optind = saved_optind - 1; /* go one step back, process this argument again */
-                        }
+                OPTION_POSITIONAL:
+                        assert(opts.mode == OPTION_PARSER_RETURN_POSITIONAL_ARGS);
+
+                        if (!args && !streq(opts.arg, "shell"))
+                                opts.mode = OPTION_PARSER_NORMAL;
+                        else if (args)
+                                opts.mode = OPTION_PARSER_STOP_AT_FIRST_NONOPTION;
 
+                        r = strv_extend(&args, opts.arg);
+                        if (r < 0)
+                                return log_oom();
                         break;
 
-                case 'h':
+                OPTION_COMMON_HELP:
                         return help();
 
-                case ARG_VERSION:
+                OPTION_COMMON_VERSION:
                         return version();
 
-                case 'H':
+                OPTION_COMMON_HOST:
                         arg_transport = BUS_TRANSPORT_REMOTE;
-                        arg_host = optarg;
+                        arg_host = opts.arg;
                         break;
 
-                case 'M':
+                OPTION_COMMON_MACHINE:
                         arg_transport = BUS_TRANSPORT_MACHINE;
-                        arg_host = optarg;
+                        arg_host = opts.arg;
                         break;
 
-                case ARG_SYSTEM:
+                OPTION_LONG("system", NULL, "Connect to system machine manager"):
                         arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
                         break;
 
-                case ARG_USER:
+                OPTION_LONG("user", NULL, "Connect to user machine manager"):
                         arg_runtime_scope = RUNTIME_SCOPE_USER;
                         break;
 
-                case 'p':
-                case 'P':
-                        r = strv_extend(&arg_property, optarg);
+                OPTION_LONG("value", NULL, "When showing properties, only print the value"):
+                        SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_ONLY_VALUE, true);
+                        break;
+
+                OPTION('p', "property", "NAME", "Show only properties by this name"): {}
+                OPTION_SHORT('P', "NAME", "Equivalent to --value --property=NAME"):
+                        r = strv_extend(&arg_property, opts.arg);
                         if (r < 0)
                                 return log_oom();
 
                         /* If the user asked for a particular property, show it to them, even if empty. */
                         SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_SHOW_EMPTY, true);
 
-                        if (c == 'p')
-                                break;
-                        _fallthrough_;
+                        if (opts.opt->short_code == 'P')
+                                SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_ONLY_VALUE, true);
 
-                case ARG_VALUE:
-                        SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_ONLY_VALUE, true);
                         break;
 
-                case 'a':
+                OPTION('a', "all", NULL, "Show all properties, including empty ones"):
                         SET_FLAG(arg_print_flags, BUS_PRINT_PROPERTY_SHOW_EMPTY, true);
                         arg_all = true;
                         break;
 
-                case 'l':
+                OPTION('l', "full", NULL, "Do not ellipsize output"):
                         arg_full = true;
                         break;
 
-                case ARG_KILL_WHOM:
-                        arg_kill_whom = optarg;
+                OPTION_LONG("kill-whom", "WHOM", "Whom to send signal to"):
+                        arg_kill_whom = opts.arg;
                         break;
 
-                case 's':
-                        r = parse_signal_argument(optarg, &arg_signal);
+                OPTION('s', "signal", "SIGNAL", "Which signal to send"):
+                        r = parse_signal_argument(opts.arg, &arg_signal);
                         if (r <= 0)
                                 return r;
                         break;
 
-                case ARG_UID:
-                        arg_uid = optarg;
+                OPTION_LONG("uid", "USER", "Specify user ID to invoke shell as"):
+                        arg_uid = opts.arg;
                         break;
 
-                case 'E':
-                        r = strv_env_replace_strdup_passthrough(&arg_setenv, optarg);
+                OPTION('E', "setenv", "VAR[=VALUE]", "Add an environment variable for shell"):
+                        r = strv_env_replace_strdup_passthrough(&arg_setenv, opts.arg);
                         if (r < 0)
-                                return log_error_errno(r, "Cannot assign environment variable %s: %m", optarg);
+                                return log_error_errno(r, "Cannot assign environment variable %s: %m", opts.arg);
                         break;
 
-                case ARG_READ_ONLY:
+                OPTION_LONG("read-only", NULL, "Create read-only bind mount or clone"):
                         arg_read_only = true;
                         break;
 
-                case ARG_MKDIR:
+                OPTION_LONG("mkdir", NULL, "Create directory before bind mounting, if missing"):
                         arg_mkdir = true;
                         break;
 
-                case 'n':
-                        if (safe_atou(optarg, &arg_lines) < 0)
+                OPTION('n', "lines", "INTEGER", "Number of journal entries to show"):
+                        if (safe_atou(opts.arg, &arg_lines) < 0)
                                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                                       "Failed to parse lines '%s'", optarg);
+                                                       "Failed to parse lines '%s'", opts.arg);
                         break;
 
-                case ARG_MAX_ADDRESSES:
-                        if (streq(optarg, "all"))
+                OPTION_LONG("max-addresses", "INTEGER",
+                            "Number of internet addresses to show at most"):
+                        if (streq(opts.arg, "all"))
                                 arg_max_addresses = UINT_MAX;
-                        else if (safe_atou(optarg, &arg_max_addresses) < 0)
+                        else if (safe_atou(opts.arg, &arg_max_addresses) < 0)
                                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                                       "Invalid number of addresses: %s", optarg);
+                                                       "Invalid number of addresses: %s", opts.arg);
                         break;
 
-                case 'o':
-                        if (streq(optarg, "help"))
+                OPTION('o', "output", "STRING",
+                       "Change journal output mode (short, short-precise, short-iso, "
+                       "short-iso-precise, short-full, short-monotonic, short-unix, short-delta, "
+                       "json, json-pretty, json-sse, json-seq, cat, verbose, export, with-unit)"):
+                        if (streq(opts.arg, "help"))
                                 return DUMP_STRING_TABLE(output_mode, OutputMode, _OUTPUT_MODE_MAX);
 
-                        r = output_mode_from_string(optarg);
+                        r = output_mode_from_string(opts.arg);
                         if (r < 0)
-                                return log_error_errno(r, "Unknown output '%s'.", optarg);
+                                return log_error_errno(r, "Unknown output '%s'.", opts.arg);
                         arg_output = r;
 
                         if (OUTPUT_MODE_IS_JSON(arg_output))
                                 arg_legend = false;
                         break;
 
-                case ARG_FORCE:
+                OPTION_LONG("force", NULL, "Replace target file when copying, if necessary"):
                         arg_force = true;
                         break;
 
-                case ARG_NOW:
+                OPTION_LONG("now", NULL,
+                            "Start or power off container after enabling or disabling it"):
                         arg_now = true;
                         break;
 
-                case ARG_RUNNER:
-                        r = machine_runner_from_string(optarg);
+                OPTION_LONG("runner", "RUNNER",
+                            "Select between nspawn and vmspawn as the runner"):
+                        r = machine_runner_from_string(opts.arg);
                         if (r < 0)
-                                return log_error_errno(r, "Failed to parse --runner= setting: %s", optarg);
+                                return log_error_errno(r, "Failed to parse --runner= setting: %s", opts.arg);
 
                         arg_runner = r;
                         break;
 
-                case 'V':
+                OPTION_SHORT('V', NULL, "Short for --runner=vmspawn"):
                         arg_runner = RUNNER_VMSPAWN;
                         break;
 
-                case ARG_VERIFY:
-                        if (streq(optarg, "help"))
+                /* Hidden options below */
+
+                OPTION_LONG("verify", "MODE", /* help= */ NULL):
+                        if (streq(opts.arg, "help"))
                                 return DUMP_STRING_TABLE(import_verify, ImportVerify, _IMPORT_VERIFY_MAX);
 
-                        r = import_verify_from_string(optarg);
+                        r = import_verify_from_string(opts.arg);
                         if (r < 0)
-                                return log_error_errno(r, "Failed to parse --verify= setting: %s", optarg);
+                                return log_error_errno(r, "Failed to parse --verify= setting: %s", opts.arg);
                         arg_verify = r;
                         break;
 
-                case ARG_FORMAT:
-                        if (!STR_IN_SET(optarg, "uncompressed", "xz", "gzip", "bzip2", "zstd"))
+                OPTION_LONG("format", "FORMAT", /* help= */ NULL):
+                        if (!STR_IN_SET(opts.arg, "uncompressed", "xz", "gzip", "bzip2", "zstd"))
                                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
-                                                       "Unknown format: %s", optarg);
+                                                       "Unknown format: %s", opts.arg);
 
-                        arg_format = optarg;
+                        arg_format = opts.arg;
                         break;
 
-                case ARG_NO_PAGER:
+                OPTION_COMMON_NO_PAGER:
                         arg_pager_flags |= PAGER_DISABLE;
                         break;
 
-                case ARG_NO_LEGEND:
+                OPTION_COMMON_NO_LEGEND:
                         arg_legend = false;
                         break;
 
-                case ARG_NO_ASK_PASSWORD:
+                OPTION_COMMON_NO_ASK_PASSWORD:
                         arg_ask_password = false;
                         break;
 
-                case 'q':
+                OPTION('q', "quiet", NULL, "Suppress output"):
                         arg_quiet = true;
                         break;
-
-                case '?':
-                        return -EINVAL;
-
-                default:
-                        assert_not_reached();
                 }
-        }
-
-done:
-        if (shell >= 0) {
-                char *t;
-
-                /* We found the "shell" verb while processing the argument list. Since we turned off reordering of the
-                 * argument list initially let's readjust it now, and move the "shell" verb to the back. */
-
-                optind -= 1; /* place the option index where the "shell" verb will be placed */
 
-                t = argv[shell];
-                for (int i = shell; i < optind; i++)
-                        argv[i] = argv[i+1];
-                argv[optind] = t;
-        }
+        /* We gathered some positional args in 'args' ourselves. Append the remaining ones. */
+        if (strv_extend_strv(&args, option_parser_get_args(&opts), /* filter_duplicates= */ false) < 0)
+                return log_oom();
 
+        *ret_args = TAKE_PTR(args);
         return 1;
 }
 
-static int machinectl_main(int argc, char *argv[], sd_bus *bus) {
-
-        static const Verb verbs[] = {
-                { "help",            VERB_ANY, VERB_ANY, 0,            verb_help              },
-                { "list",            VERB_ANY, 1,        VERB_DEFAULT, verb_list_machines     },
-                { "list-images",     VERB_ANY, 1,        0,            verb_list_images       },
-                { "status",          2,        VERB_ANY, 0,            verb_show_machine      },
-                { "image-status",    VERB_ANY, VERB_ANY, 0,            verb_show_image        },
-                { "show",            VERB_ANY, VERB_ANY, 0,            verb_show_machine      },
-                { "show-image",      VERB_ANY, VERB_ANY, 0,            verb_show_image        },
-                { "terminate",       2,        VERB_ANY, 0,            verb_terminate_machine },
-                { "reboot",          2,        VERB_ANY, 0,            verb_reboot_machine    },
-                { "restart",         2,        VERB_ANY, 0,            verb_reboot_machine    }, /* Convenience alias */
-                { "poweroff",        2,        VERB_ANY, 0,            verb_poweroff_machine  },
-                { "stop",            2,        VERB_ANY, 0,            verb_poweroff_machine  }, /* Convenience alias */
-                { "kill",            2,        VERB_ANY, 0,            verb_kill_machine      },
-                { "pause",           2,        VERB_ANY, 0,            verb_pause             },
-                { "resume",          2,        VERB_ANY, 0,            verb_resume            },
-                { "login",           VERB_ANY, 2,        0,            verb_login_machine     },
-                { "shell",           VERB_ANY, VERB_ANY, 0,            verb_shell_machine     },
-                { "bind",            3,        4,        0,            verb_bind_mount        },
-                { "bind-volume",     3,        3,        0,            verb_bind_volume       },
-                { "unbind-volume",   3,        3,        0,            verb_unbind_volume     },
-                { "edit",            2,        VERB_ANY, 0,            verb_edit_settings     },
-                { "cat",             2,        VERB_ANY, 0,            verb_cat_settings      },
-                { "copy-to",         3,        4,        0,            verb_copy_files        },
-                { "copy-from",       3,        4,        0,            verb_copy_files        },
-                { "remove",          2,        VERB_ANY, 0,            verb_remove_image      },
-                { "rename",          3,        3,        0,            verb_rename_image      },
-                { "clone",           3,        3,        0,            verb_clone_image       },
-                { "read-only",       2,        3,        0,            verb_read_only_image   },
-                { "start",           2,        VERB_ANY, 0,            verb_start_machine     },
-                { "enable",          2,        VERB_ANY, 0,            verb_enable_machine    },
-                { "disable",         2,        VERB_ANY, 0,            verb_enable_machine    },
-                { "set-limit",       2,        3,        0,            verb_set_limit         },
-                { "clean",           VERB_ANY, 1,        0,            verb_clean_images      },
-                {}
-        };
-
-        return dispatch_verb(argc, argv, verbs, bus);
-}
-
 static int run(int argc, char *argv[]) {
         _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+        _cleanup_strv_free_ char **args = NULL;
         int r;
 
         setlocale(LC_ALL, "");
         log_setup();
 
-        r = parse_argv(argc, argv);
+        r = parse_argv(argc, argv, &args);
         if (r <= 0)
                 return r;
 
         journal_browse_prepare();
 
-        if (STRPTR_IN_SET(argv[optind],
-                          "import-tar", "import-raw", "import-fs",
-                          "export-tar", "export-raw",
-                          "pull-tar", "pull-raw",
-                          "list-transfers", "cancel-transfer"))
-                return chainload_importctl(argc, argv);
+        if (args && STR_IN_SET(args[0],
+                               "import-tar", "import-raw", "import-fs",
+                               "export-tar", "export-raw",
+                               "pull-tar", "pull-raw",
+                               "list-transfers", "cancel-transfer"))
+                return chainload_importctl(args);
 
         r = bus_connect_transport(arg_transport, arg_host, arg_runtime_scope, &bus);
         if (r < 0)
@@ -2767,7 +2656,7 @@ static int run(int argc, char *argv[]) {
 
         (void) sd_bus_set_allow_interactive_authorization(bus, arg_ask_password);
 
-        return machinectl_main(argc, argv, bus);
+        return dispatch_verb_with_args(args, bus);
 }
 
 DEFINE_MAIN_FUNCTION(run);
index de51daa24c73de049105f923146a7a2936beb0da..6b07137ef811f00b1cdaf9a4fc111298e27953b9 100755 (executable)
@@ -120,6 +120,7 @@ machinectl disable long-running long-running long-running container1
 
 [[ "$(TERM=dumb machinectl shell testuser@ /usr/bin/bash -c 'echo -ne $FOO')" == "" ]]
 [[ "$(TERM=dumb machinectl shell --setenv=FOO=bar testuser@ /usr/bin/bash -c 'echo -ne $FOO')" == "bar" ]]
+[[ "$(TERM=dumb machinectl shell testuser@ --setenv=FOO=bar /usr/bin/bash -c 'echo -ne $FOO')" == "bar" ]]
 
 [[ "$(machinectl show --property=State --value long-running)" == "running" ]]
 # Equivalent to machinectl kill --signal=SIGRTMIN+4 --kill-whom=leader