[systemd-devel] [PATCH] systemctl: add edit verb

Ronny Chevalier chevalier.ronny at gmail.com
Wed Oct 29 06:33:09 PDT 2014


It helps editing units by either creating a drop-in file, like
/etc/systemd/system/my.service.d/override.conf, or by copying the
original unit from /usr/lib/systemd/ to /etc/systemd/ if the --full
option is specified.

It invokes an editor on temporary files related to the unit files and
if the editor exited successfully, then it renames the temporary files
to their original names (e.g. my.service or override.conf) and
daemon-reload is invoked.

If the temporary file is empty the modification is canceled.

See https://bugzilla.redhat.com/show_bug.cgi?id=906824
---
changes:
  * --runtime is handled
  * changes are made atomically by creating temporary files
  * man page improved
  * no heap allocation for execlp editor
  * arg_root is handled properly

 TODO                      |   4 +-
 man/less-variables.xml    |   4 +-
 man/systemctl.xml         |  64 +++++-
 src/systemctl/systemctl.c | 525 +++++++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 587 insertions(+), 10 deletions(-)

diff --git a/TODO b/TODO
index abe89b7..1cbedd4 100644
--- a/TODO
+++ b/TODO
@@ -84,7 +84,7 @@ Features:
 
 * systemctl: if it fails, show log output?
 
-* maybe add "systemctl edit" that copies unit files from /usr/lib/systemd/system to /etc/systemd/system and invokes vim on them
+* systemctl edit: add commented help text to the end, like git commit
 
 * maybe introduce AssertXYZ= similar to ConditionXYZ= that causes a unit to fail (instead of skipping it) if some condition is not true...
 
@@ -776,7 +776,7 @@ External:
 
 * zsh shell completion:
   - <command> <verb> -<TAB> should complete options, but currently does not
-  - systemctl add-wants,add-requires
+  - systemctl add-wants,add-requires, edit
 
 
 Regularly:
diff --git a/man/less-variables.xml b/man/less-variables.xml
index 09cbd42..0fb4d7f 100644
--- a/man/less-variables.xml
+++ b/man/less-variables.xml
@@ -6,7 +6,7 @@
         <title>Environment</title>
 
         <variablelist class='environment-variables'>
-                <varlistentry>
+                <varlistentry id='pager'>
                         <term><varname>$SYSTEMD_PAGER</varname></term>
 
                         <listitem><para>Pager to use when
@@ -17,7 +17,7 @@
                         <option>--no-pager</option>.</para></listitem>
                 </varlistentry>
 
-                <varlistentry>
+                <varlistentry id='less'>
                         <term><varname>$SYSTEMD_LESS</varname></term>
 
                         <listitem><para>Override the default
diff --git a/man/systemctl.xml b/man/systemctl.xml
index 7cbaa6c..26f5235 100644
--- a/man/systemctl.xml
+++ b/man/systemctl.xml
@@ -465,7 +465,7 @@ along with systemd; If not, see <http://www.gnu.org/licenses/>.
 
         <listitem>
           <para>When used with <command>enable</command>,
-          <command>disable</command>,
+          <command>disable</command>, <command>edit</command>,
           (and related commands), make changes only temporarily, so
           that they are lost on the next reboot. This will have the
           effect that changes are not made in subdirectories of
@@ -1150,6 +1150,43 @@ kobject-uevent 1 systemd-udevd-kernel.socket systemd-udevd.service
             <filename>default.target</filename> to the given unit.</para>
           </listitem>
         </varlistentry>
+
+        <varlistentry>
+          <term><command>edit <replaceable>NAME</replaceable>...</command></term>
+
+          <listitem>
+            <para>Edit a drop-in snippet or a whole replacement file if
+            <option>--full</option> is specified, to extend or override the
+            specified unit.</para>
+
+            <para>Depending on whether <option>--system</option> (the default),
+            <option>--user</option>, or <option>--global</option> is specified,
+            this create a drop-in file for each units either for the system,
+            for the calling user or for all futures logins of all users. Then
+            the editor (see section "Environment" below) is invoked on temporary
+            files which will be saved as their corresponding files if the editor
+            exited successfully.</para>
+
+            <para>If <option>--full</option> is specified, this will copy the
+            original units instead of creating drop-in files.</para>
+
+            <para>If <option>--runtime</option> is specified, the changes will
+            be made temporarily in <filename>/run</filename> and they will be
+            lost on the next reboot.</para>
+
+            <para>If the temporary file is empty the modification of the related
+            unit is canceled</para>
+
+            <para>After the units have been edited, the systemd configuration is
+            reloaded (in a way that is equivalent to <command>daemon-reload</command>),
+            but it does not restart or reload the units.</para>
+
+            <para>Note that this command cannot be used to remotely edit units
+            and that you cannot temporarily edit units which are in
+            <filename>/etc</filename> since they take precedence over
+            <filename>/run</filename>.</para>
+          </listitem>
+        </varlistentry>
       </variablelist>
     </refsect2>
 
@@ -1608,7 +1645,28 @@ kobject-uevent 1 systemd-udevd-kernel.socket systemd-udevd.service
     code otherwise.</para>
   </refsect1>
 
-  <xi:include href="less-variables.xml" />
+  <refsect1>
+    <title>Environment</title>
+
+    <variablelist class='environment-variables'>
+      <varlistentry>
+        <term><varname>$SYSTEMD_EDITOR</varname></term>
+
+        <listitem><para>Editor to use when editing units; overrides
+        <varname>$EDITOR</varname> and <varname>$VISUAL</varname>. If neither
+        <varname>$SYSTEMD_EDITOR</varname> nor <varname>$EDITOR</varname> nor
+        <varname>$VISUAL</varname> are present or if it is set to an empty
+        string or if their execution failed, systemctl will try to execute well
+        known editors in this order:
+        <citerefentry><refentrytitle>nano</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+        <citerefentry><refentrytitle>vim</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+        <citerefentry><refentrytitle>vi</refentrytitle><manvolnum>1</manvolnum></citerefentry>.
+        </para></listitem>
+      </varlistentry>
+    </variablelist>
+    <xi:include href="less-variables.xml" xpointer="pager"/>
+    <xi:include href="less-variables.xml" xpointer="less"/>
+  </refsect1>
 
   <refsect1>
     <title>See Also</title>
@@ -1621,7 +1679,7 @@ kobject-uevent 1 systemd-udevd-kernel.socket systemd-udevd.service
       <citerefentry><refentrytitle>systemd.resource-management</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
       <citerefentry><refentrytitle>systemd.special</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
       <citerefentry project='man-pages'><refentrytitle>wall</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
-      <citerefentry><refentrytitle>systemd.preset</refentrytitle><manvolnum>5</manvolnum></citerefentry>
+      <citerefentry><refentrytitle>systemd.preset</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
       <citerefentry><refentrytitle>glob</refentrytitle><manvolnum>7</manvolnum></citerefentry>
     </para>
   </refsect1>
diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c
index 28eaa6a..f3edcc0 100644
--- a/src/systemctl/systemctl.c
+++ b/src/systemctl/systemctl.c
@@ -72,6 +72,8 @@
 #include "bus-message.h"
 #include "bus-error.h"
 #include "bus-errors.h"
+#include "copy.h"
+#include "mkdir.h"
 
 static char **arg_types = NULL;
 static char **arg_states = NULL;
@@ -5642,6 +5644,520 @@ static int is_system_running(sd_bus *bus, char **args) {
         return streq(state, "running") ? EXIT_SUCCESS : EXIT_FAILURE;
 }
 
+static int unit_file_find_path(LookupPaths *lp, const char *unit_name, char **unit_path) {
+        char **p;
+
+        assert(lp);
+        assert(unit_name);
+        assert(unit_path);
+
+        STRV_FOREACH(p, lp->unit_path) {
+                char *path;
+
+                path = strjoin(*p, "/", unit_name, NULL);
+                if (!path)
+                        return log_oom();
+
+                if (access(path, F_OK) == 0) {
+                        *unit_path = path;
+                        return 1;
+                }
+
+                free(path);
+        }
+
+        return 0;
+}
+
+static int create_edit_temp_file(const char *new_path, const char *original_path, char **ret_tmp_fn) {
+        _cleanup_close_ int fd = -1;
+        int r;
+        char *t;
+
+        assert(new_path);
+        assert(original_path);
+        assert(ret_tmp_fn);
+
+        t = tempfn_random(new_path);
+        if (!t)
+                return log_oom();
+
+        r = mkdir_parents(new_path, 0755);
+        if (r < 0) {
+                log_error("Failed to create directories for %s: %s", new_path, strerror(-r));
+                return r;
+        }
+
+        r = copy_file(original_path, t, 0, 0644);
+        if (r == -ENOENT) {
+                r = touch(t);
+                if (r < 0) {
+                        log_error("Failed to create temporary file %s: %s", t, strerror(-r));
+                        free(t);
+                        return r;
+                }
+        } else if (r < 0) {
+                log_error("Failed to copy %s to %s: %s", original_path, t, strerror(-r));
+                free(t);
+                return r;
+        }
+
+        *ret_tmp_fn = t;
+
+        return 0;
+}
+
+static int get_drop_in_to_edit(const char *unit_name, const char *user_home, const char *user_runtime, char **ret_path) {
+        _cleanup_free_ char *tmp = NULL;
+        char *tmp_new_path;
+
+        assert(unit_name);
+        assert(ret_path);
+
+        switch (arg_scope) {
+                case UNIT_FILE_SYSTEM:
+                        if (arg_runtime)
+                                tmp = strjoin("/run/systemd/system/", unit_name, ".d/override.conf", NULL);
+                        else
+                                tmp = strjoin(SYSTEM_CONFIG_UNIT_PATH "/", unit_name, ".d/override.conf", NULL);
+                        break;
+                case UNIT_FILE_GLOBAL:
+                        if (arg_runtime)
+                                tmp = strjoin("/run/systemd/user/", unit_name, ".d/override.conf", NULL);
+                        else
+                                tmp = strjoin(USER_CONFIG_UNIT_PATH "/", unit_name, ".d/override.conf", NULL);
+                        break;
+                case UNIT_FILE_USER:
+                        assert(user_home);
+                        assert(user_runtime);
+
+                        if (arg_runtime)
+                                tmp = strjoin(user_runtime, "/", unit_name, ".d/override.conf", NULL);
+                        else
+                                tmp = strjoin(user_home, "/", unit_name, ".d/override.conf", NULL);
+                        break;
+                default:
+                        assert_not_reached("Invalid scope");
+        }
+        if (!tmp)
+                return log_oom();
+
+        tmp_new_path = path_join(arg_root, tmp, NULL);
+        if (!tmp_new_path)
+                return log_oom();
+
+        *ret_path = tmp_new_path;
+
+        return 0;
+}
+
+static int unit_file_create_drop_in(const char *unit_name, const char *user_home, const char *user_runtime, char **ret_new_path, char **ret_tmp_path) {
+        char *tmp_new_path;
+        char *tmp_tmp_path;
+        int r;
+
+        assert(unit_name);
+        assert(ret_new_path);
+        assert(ret_tmp_path);
+
+        r = get_drop_in_to_edit(unit_name, user_home, user_runtime, &tmp_new_path);
+        if (r < 0)
+                return r;
+
+        r = create_edit_temp_file(tmp_new_path, tmp_new_path, &tmp_tmp_path);
+        if (r < 0) {
+                free(tmp_new_path);
+                return r;
+        }
+
+        *ret_new_path = tmp_new_path;
+        *ret_tmp_path = tmp_tmp_path;
+
+        return 0;
+}
+
+static int get_copy_to_edit(const char *unit_name, const char *fragment_path, const char *user_home, const char *user_runtime, char **ret_path) {
+        _cleanup_free_ char *tmp = NULL;
+        char *tmp_new_path;
+
+        assert(unit_name);
+        assert(ret_path);
+
+        switch (arg_scope) {
+                case UNIT_FILE_SYSTEM:
+                        if (arg_runtime) {
+                                if (path_startswith(fragment_path, "/etc/systemd/system") ||
+                                    path_startswith(fragment_path, SYSTEM_CONFIG_UNIT_PATH)) {
+                                        log_error("%s ignored: cannot temporarily edit units from /etc/systemd/system/%s",
+                                                  unit_name,
+                                                  path_equal("/etc/systemd/system/", SYSTEM_CONFIG_UNIT_PATH) ? "" : " or " SYSTEM_CONFIG_UNIT_PATH);
+                                        return -EINVAL;
+                                }
+                                tmp_new_path = path_join(arg_root, "/run/systemd/system/", unit_name);
+                        } else
+                                tmp_new_path = path_join(arg_root, SYSTEM_CONFIG_UNIT_PATH "/", unit_name);
+                        break;
+                case UNIT_FILE_GLOBAL:
+                        if (arg_runtime) {
+                                if (path_startswith(fragment_path, "/etc/systemd/user") ||
+                                    path_startswith(fragment_path, USER_CONFIG_UNIT_PATH)) {
+                                        log_error("%s ignored: cannot temporarily edit units from /etc/systemd/user/%s",
+                                                  unit_name,
+                                                  path_equal("/etc/systemd/user/", USER_CONFIG_UNIT_PATH) ? "" : " or " USER_CONFIG_UNIT_PATH);
+                                        return -EINVAL;
+                                }
+                                tmp_new_path = path_join(arg_root, "/run/systemd/user/", unit_name);
+                        } else
+                                tmp_new_path = path_join(arg_root, USER_CONFIG_UNIT_PATH "/", unit_name);
+                        break;
+                case UNIT_FILE_USER:
+                        assert(user_home);
+                        assert(user_runtime);
+
+                        if (arg_runtime) {
+                                if (path_startswith(fragment_path, "/etc/systemd/user") ||
+                                    path_startswith(fragment_path, USER_CONFIG_UNIT_PATH) ||
+                                    path_startswith(fragment_path, user_home)) {
+                                        log_error("%s ignored: cannot temporarily edit units from %s or /etc/systemd/user/%s",
+                                                  unit_name,
+                                                  user_home,
+                                                  path_equal("/etc/systemd/user/", USER_CONFIG_UNIT_PATH) ? "" : " or " USER_CONFIG_UNIT_PATH);
+                                        return -EINVAL;
+                                }
+                                tmp = strjoin(user_runtime, "/", unit_name, NULL);
+                        }
+                        else
+                                tmp = strjoin(user_home, "/", unit_name, NULL);
+
+                        if (!tmp)
+                                return log_oom();
+
+                        tmp_new_path = path_join(arg_root, tmp, NULL);
+                        break;
+                default:
+                        assert_not_reached("Invalid scope");
+        }
+        if (!tmp_new_path)
+                return log_oom();
+
+        *ret_path = tmp_new_path;
+
+        return 0;
+}
+
+static int unit_file_create_copy(const char *unit_name,
+                                 const char *fragment_path,
+                                 const char *user_home,
+                                 const char *user_runtime,
+                                 char **ret_new_path,
+                                 char **ret_tmp_path) {
+        char *tmp_new_path;
+        char *tmp_tmp_path;
+        int r;
+
+        assert(fragment_path);
+        assert(unit_name);
+        assert(ret_new_path);
+        assert(ret_tmp_path);
+
+        r = get_copy_to_edit(unit_name, fragment_path, user_home, user_runtime, &tmp_new_path);
+        if (r < 0)
+                return r;
+
+        if (!path_equal(fragment_path, tmp_new_path) && access(tmp_new_path, F_OK) == 0) {
+                char response;
+
+                r = ask_char(&response, "yn", "%s already exists, are you sure to overwrite it with %s? [(y)es, (n)o] ", tmp_new_path, fragment_path);
+                if (r < 0) {
+                        free(tmp_new_path);
+                        return r;
+                }
+                if (response != 'y') {
+                        log_warning("%s ignored", unit_name);
+                        free(tmp_new_path);
+                        return -1;
+                }
+        }
+
+        r = create_edit_temp_file(tmp_new_path, fragment_path, &tmp_tmp_path);
+        if (r < 0) {
+                log_error("Failed to create temporary file for %s: %s", tmp_new_path, strerror(-r));
+                free(tmp_new_path);
+                return r;
+        }
+
+        *ret_new_path = tmp_new_path;
+        *ret_tmp_path = tmp_tmp_path;
+
+        return 0;
+}
+
+static int run_editor(char **paths) {
+        pid_t pid;
+        int r;
+
+        assert(paths);
+
+        pid = fork();
+        if (pid < 0) {
+                log_error("Failed to fork: %m");
+                return -errno;
+        }
+
+        if (pid == 0) {
+                const char **args;
+                char **backup_editors = STRV_MAKE("nano", "vim", "vi");
+                char *editor;
+                char **tmp_path, **original_path, **p;
+                unsigned i = 1;
+                size_t argc;
+
+                argc = strv_length(paths)/2 + 1;
+                args = newa(const char*, argc + 1);
+
+                args[0] = NULL;
+                STRV_FOREACH_PAIR(original_path, tmp_path, paths) {
+                        args[i] = *tmp_path;
+                        i++;
+                }
+                args[argc] = NULL;
+
+                /* SYSTEMD_EDITOR takes precedence over EDITOR which takes precedence over VISUAL
+                 * If neither SYSTEMD_EDITOR nor EDITOR nor VISUAL are present,
+                 * we try to execute well known editors
+                 */
+                editor = getenv("SYSTEMD_EDITOR");
+                if (!editor)
+                        editor = getenv("EDITOR");
+                if (!editor)
+                        editor = getenv("VISUAL");
+
+                if (!isempty(editor)) {
+                        args[0] = editor;
+                        execvp(editor, (char* const*) args);
+                }
+
+                STRV_FOREACH(p, backup_editors) {
+                        args[0] = *p;
+                        execvp(*p, (char* const*) args);
+                        /* We do not fail if the editor doesn't exist
+                         * because we want to try each one of them before
+                         * failing.
+                         */
+                        if (errno != ENOENT) {
+                                log_error("Failed to execute %s: %m", editor);
+                                _exit(EXIT_FAILURE);
+                        }
+                }
+
+                log_error("Cannot edit unit(s): No editor available. Please set either SYSTEMD_EDITOR or EDITOR or VISUAL environment variable");
+                _exit(EXIT_FAILURE);
+        }
+
+        r = wait_for_terminate_and_warn("editor", pid);
+        if (r < 0) {
+                log_error("Failed to wait for child: %s", strerror(-r));
+                return r;
+        }
+
+        return r;
+}
+
+static int find_paths_to_edit(sd_bus *bus, char **names, char ***paths) {
+        _cleanup_free_ char *user_home = NULL;
+        _cleanup_free_ char *user_runtime = NULL;
+        char **name;
+        int r;
+
+        assert(names);
+        assert(paths);
+
+        if (arg_scope == UNIT_FILE_USER) {
+                r = user_config_home(&user_home);
+                if (r < 0)
+                        return log_oom();
+                else if (r == 0) {
+                        log_error("Cannot edit units for the user instance: home directory unknown");
+                        return -1;
+                }
+
+                r = user_runtime_dir(&user_runtime);
+                if (r < 0)
+                        return log_oom();
+                else if (r == 0) {
+                        log_error("Cannot edit units for the user instance: runtime directory unknown");
+                        return -1;
+                }
+        }
+
+        if (!bus || avoid_bus()) {
+                _cleanup_lookup_paths_free_ LookupPaths lp = {};
+
+                /* If there is no bus, we try to find the units by testing each available directory
+                 * according to the scope.
+                 */
+                r = lookup_paths_init(&lp,
+                                arg_scope == UNIT_FILE_SYSTEM ? SYSTEMD_SYSTEM : SYSTEMD_USER,
+                                arg_scope == UNIT_FILE_USER,
+                                arg_root,
+                                NULL, NULL, NULL);
+                if (r < 0) {
+                        log_error("Failed get lookup paths: %s", strerror(-r));
+                        return r;
+                }
+
+                STRV_FOREACH(name, names) {
+                        _cleanup_free_ char *path = NULL;
+                        char *new_path, *tmp_path;
+
+                        r = unit_file_find_path(&lp, *name, &path);
+                        if (r < 0)
+                                return r;
+                        if (r == 0) {
+                                log_warning("%s ignored: not found", *name);
+                                continue;
+                        }
+
+                        if (arg_full)
+                                r = unit_file_create_copy(*name, path, user_home, user_runtime, &new_path, &tmp_path);
+                        else
+                                r = unit_file_create_drop_in(*name, user_home, user_runtime, &new_path, &tmp_path);
+
+                        if (r < 0)
+                                continue;
+
+                        r = strv_push(paths, new_path);
+                        if (r < 0)
+                                return log_oom();
+
+                        r = strv_push(paths, tmp_path);
+                        if (r < 0)
+                                return log_oom();
+                }
+        } else {
+                STRV_FOREACH(name, names) {
+                        _cleanup_bus_error_free_ sd_bus_error error = SD_BUS_ERROR_NULL;
+                        _cleanup_free_ char *fragment_path = NULL;
+                        _cleanup_free_ char *unit = NULL;
+                        char *new_path, *tmp_path;
+
+                        unit = unit_dbus_path_from_name(*name);
+                        if (!unit)
+                                return log_oom();
+
+                        if (need_daemon_reload(bus, *name) > 0) {
+                                log_warning("%s ignored: unit file changed on disk. Run 'systemctl%s daemon-reload'.",
+                                        *name, arg_scope == UNIT_FILE_SYSTEM ? "" : " --user");
+                                continue;
+                        }
+
+                        r = sd_bus_get_property_string(
+                                        bus,
+                                        "org.freedesktop.systemd1",
+                                        unit,
+                                        "org.freedesktop.systemd1.Unit",
+                                        "FragmentPath",
+                                        &error,
+                                        &fragment_path);
+                        if (r < 0) {
+                                log_warning("Failed to get FragmentPath: %s", bus_error_message(&error, r));
+                                continue;
+                        }
+
+                        if (isempty(fragment_path)) {
+                                log_warning("%s ignored: not found", *name);
+                                continue;
+                        }
+
+                        if (arg_full)
+                                r = unit_file_create_copy(*name, fragment_path, user_home, user_runtime, &new_path, &tmp_path);
+                        else
+                                r = unit_file_create_drop_in(*name, user_home, user_runtime, &new_path, &tmp_path);
+                        if (r < 0)
+                                continue;
+
+                        r = strv_push(paths, new_path);
+                        if (r < 0)
+                                return log_oom();
+
+                        r = strv_push(paths, tmp_path);
+                        if (r < 0)
+                                return log_oom();
+                }
+        }
+
+        return 0;
+}
+
+static int edit(sd_bus *bus, char **args) {
+        _cleanup_strv_free_ char **names = NULL;
+        _cleanup_strv_free_ char **paths = NULL;
+        char **original, **tmp;
+        int r;
+
+        assert(args);
+
+        if (!on_tty()) {
+                log_error("Cannot edit units if we are not on a tty");
+                return -EINVAL;
+        }
+
+        if (arg_transport != BUS_TRANSPORT_LOCAL) {
+                log_error("Cannot remotely edit units");
+                return -EINVAL;
+        }
+
+        r = expand_names(bus, args + 1, NULL, &names);
+        if (r < 0) {
+                log_error("Failed to expand names: %s", strerror(-r));
+                return r;
+        }
+
+        if (!names) {
+                log_error("No unit name found by expanding names");
+                return -ENOENT;
+        }
+
+        r = find_paths_to_edit(bus, names, &paths);
+        if (r < 0)
+                return r;
+
+        if (strv_isempty(paths)) {
+                log_error("Cannot find any units to edit");
+                return -ENOENT;
+        }
+
+        r = run_editor(paths);
+        if (r < 0)
+                goto end;
+
+        STRV_FOREACH_PAIR(original, tmp, paths) {
+                /* If the temporary file is empty we ignore it.
+                 * It's useful if the user wants to cancel its modification
+                 */
+                if (null_or_empty_path(*tmp)) {
+                        log_warning("Edition of %s canceled: temporary file empty", *original);
+                        continue;
+                }
+                r = rename(*tmp, *original);
+                if (r < 0) {
+                        log_error("Failed to rename %s to %s: %m", *tmp, *original);
+                        r = -errno;
+                        goto end;
+                }
+        }
+
+        if (!arg_no_reload && bus && !avoid_bus())
+                r = daemon_reload(bus, args);
+
+end:
+        STRV_FOREACH_PAIR(original, tmp, paths) {
+                unlink_noerrno(*tmp);
+        }
+
+        return r;
+}
+
 static void systemctl_help(void) {
 
         pager_open_if_enabled();
@@ -5739,7 +6255,9 @@ static void systemctl_help(void) {
                "  add-requires TARGET NAME...     Add 'Requires' dependency for the target\n"
                "                                  on specified one or more units\n"
                "  get-default                     Get the name of the default target\n"
-               "  set-default NAME                Set the default target\n\n"
+               "  set-default NAME                Set the default target\n"
+               "  edit NAME...                    Edit one or more unit files\n"
+               "\n"
                "Machine Commands:\n"
                "  list-machines [PATTERN...]      List local containers and host\n\n"
                "Job Commands:\n"
@@ -6748,8 +7266,9 @@ static int systemctl_main(sd_bus *bus, int argc, char *argv[], int bus_error) {
                 { "get-default",           EQUAL, 1, get_default,      NOBUS },
                 { "set-property",          MORE,  3, set_property      },
                 { "is-system-running",     EQUAL, 1, is_system_running },
-                { "add-wants",             MORE,  3, add_dependency,        NOBUS },
-                { "add-requires",          MORE,  3, add_dependency,        NOBUS },
+                { "add-wants",             MORE,  3, add_dependency,   NOBUS },
+                { "add-requires",          MORE,  3, add_dependency,   NOBUS },
+                { "edit",                  MORE,  2, edit,             NOBUS },
                 {}
         }, *verb = verbs;
 
-- 
2.1.2



More information about the systemd-devel mailing list