summary refs log tree commit diff
path: root/src/nmcli/connections.c
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
committerMichael Biebl <biebl@debian.org>2021-10-01 23:05:04 +0200
commite74c568b07b50b97873fb4ee1d776dedefbd54d6 (patch)
tree3469f17ea9af91f7ff169b890633bda68b0cf76e /src/nmcli/connections.c
parentbfe522304da217296e2a61040f58e35ec5d6f3f2 (diff)
New upstream version 1.32.12 upstream/1.32.12
Diffstat (limited to 'src/nmcli/connections.c')
-rw-r--r--src/nmcli/connections.c9654
1 files changed, 9654 insertions, 0 deletions
diff --git a/src/nmcli/connections.c b/src/nmcli/connections.c
new file mode 100644
index 00000000..9f700cae
--- /dev/null
+++ b/src/nmcli/connections.c
@@ -0,0 +1,9654 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2010 - 2018 Red Hat, Inc.
+ */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "connections.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <signal.h>
+#include <readline/readline.h>
+#include <readline/history.h>
+#include <fcntl.h>
+
+#include "libnm-glib-aux/nm-dbus-aux.h"
+#include "libnmc-base/nm-client-utils.h"
+#include "libnmc-base/nm-vpn-helpers.h"
+#include "libnmc-setting/nm-meta-setting-access.h"
+#include "libnmc-base/nm-secret-agent-simple.h"
+
+#include "utils.h"
+#include "common.h"
+#include "settings.h"
+#include "devices.h"
+#include "polkit-agent.h"
+
+/*****************************************************************************/
+
+typedef enum {
+    PROPERTY_INF_FLAG_NONE     = 0x0,
+    PROPERTY_INF_FLAG_DISABLED = 0x1, /* Don't ask due to runtime decision. */
+    PROPERTY_INF_FLAG_ENABLED =
+        0x2, /* Override NM_META_PROPERTY_INF_FLAG_DONT_ASK due to runtime decision. */
+    PROPERTY_INF_FLAG_ALL = 0x3,
+} PropertyInfFlags;
+
+typedef char *(*CompEntryFunc)(const char *, int);
+
+typedef struct _OptionInfo {
+    const NMMetaSettingInfoEditor *setting_info;
+    const char *                   property;
+    const char *                   option;
+    gboolean (*check_and_set)(NmCli *                   nmc,
+                              NMConnection *            connection,
+                              const struct _OptionInfo *option,
+                              const char *              value,
+                              GError **                 error);
+    CompEntryFunc generator_func;
+} OptionInfo;
+
+/* define some prompts for connection editor */
+#define EDITOR_PROMPT_SETTING  _("Setting name? ")
+#define EDITOR_PROMPT_PROPERTY _("Property name? ")
+#define EDITOR_PROMPT_CON_TYPE _("Enter connection type: ")
+
+/* define some other prompts */
+
+#define PROMPT_CONNECTION         _("Connection (name, UUID, or path): ")
+#define PROMPT_VPN_CONNECTION     _("VPN connection (name, UUID, or path): ")
+#define PROMPT_CONNECTIONS        _("Connection(s) (name, UUID, or path): ")
+#define PROMPT_ACTIVE_CONNECTIONS _("Connection(s) (name, UUID, path or apath): ")
+
+#define BASE_PROMPT "nmcli> "
+
+/*****************************************************************************/
+
+static NM_UTILS_LOOKUP_STR_DEFINE(
+    active_connection_state_to_string,
+    NMActiveConnectionState,
+    NM_UTILS_LOOKUP_DEFAULT(N_("unknown")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_ACTIVATING, N_("activating")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_ACTIVATED, N_("activated")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_DEACTIVATING, N_("deactivating")),
+    NM_UTILS_LOOKUP_ITEM(NM_ACTIVE_CONNECTION_STATE_DEACTIVATED, N_("deactivated")),
+    NM_UTILS_LOOKUP_ITEM_IGNORE(NM_ACTIVE_CONNECTION_STATE_UNKNOWN), );
+
+static NM_UTILS_LOOKUP_STR_DEFINE(
+    vpn_connection_state_to_string,
+    NMVpnConnectionState,
+    NM_UTILS_LOOKUP_DEFAULT(N_("unknown")),
+    NM_UTILS_LOOKUP_ITEM(NM_VPN_CONNECTION_STATE_PREPARE, N_("VPN connecting (prepare)")),
+    NM_UTILS_LOOKUP_ITEM(NM_VPN_CONNECTION_STATE_NEED_AUTH,
+                         N_("VPN connecting (need authentication)")),
+    NM_UTILS_LOOKUP_ITEM(NM_VPN_CONNECTION_STATE_CONNECT, N_("VPN connecting")),
+    NM_UTILS_LOOKUP_ITEM(NM_VPN_CONNECTION_STATE_IP_CONFIG_GET,
+                         N_("VPN connecting (getting IP configuration)")),
+    NM_UTILS_LOOKUP_ITEM(NM_VPN_CONNECTION_STATE_ACTIVATED, N_("VPN connected")),
+    NM_UTILS_LOOKUP_ITEM(NM_VPN_CONNECTION_STATE_FAILED, N_("VPN connection failed")),
+    NM_UTILS_LOOKUP_ITEM(NM_VPN_CONNECTION_STATE_DISCONNECTED, N_("VPN disconnected")),
+    NM_UTILS_LOOKUP_ITEM_IGNORE(NM_VPN_CONNECTION_STATE_UNKNOWN), );
+
+/*****************************************************************************/
+
+typedef struct {
+    NmCli *nmc;
+    char * orig_id;
+    char * orig_uuid;
+    char * new_id;
+} AddConnectionInfo;
+
+static AddConnectionInfo *
+_add_connection_info_new(NmCli *nmc, NMConnection *orig_connection, NMConnection *new_connection)
+{
+    AddConnectionInfo *info;
+
+    info  = g_slice_new(AddConnectionInfo);
+    *info = (AddConnectionInfo){
+        .nmc       = nmc,
+        .orig_id   = orig_connection ? g_strdup(nm_connection_get_id(orig_connection)) : NULL,
+        .orig_uuid = orig_connection ? g_strdup(nm_connection_get_uuid(orig_connection)) : NULL,
+        .new_id    = g_strdup(nm_connection_get_id(new_connection)),
+    };
+    return info;
+}
+
+static void
+_add_connection_info_free(AddConnectionInfo *info)
+{
+    g_free(info->orig_id);
+    g_free(info->orig_uuid);
+    g_free(info->new_id);
+    nm_g_slice_free(info);
+}
+
+NM_AUTO_DEFINE_FCN(AddConnectionInfo *,
+                   _nm_auto_free_add_connection_info,
+                   _add_connection_info_free);
+
+#define nm_auto_free_add_connection_info nm_auto(_nm_auto_free_add_connection_info)
+
+/*****************************************************************************/
+
+/* Essentially a version of nm_setting_connection_get_connection_type() that
+ * prefers an alias instead of the settings name when in pretty print mode.
+ * That is so that we print "wifi" instead of "802-11-wireless" in "nmcli c". */
+static const char *
+connection_type_to_display(const char *type, NMMetaAccessorGetType get_type)
+{
+    const NMMetaSettingInfoEditor *editor;
+    int                            i;
+
+    nm_assert(
+        NM_IN_SET(get_type, NM_META_ACCESSOR_GET_TYPE_PRETTY, NM_META_ACCESSOR_GET_TYPE_PARSABLE));
+
+    if (!type)
+        return NULL;
+
+    if (get_type != NM_META_ACCESSOR_GET_TYPE_PRETTY)
+        return type;
+
+    for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++) {
+        editor = &nm_meta_setting_infos_editor[i];
+        if (nm_streq(type, editor->general->setting_name))
+            return editor->alias ?: type;
+    }
+    return type;
+}
+
+static int
+active_connection_get_state_ord(NMActiveConnection *active)
+{
+    /* returns an integer related to @active's state, that can be used for sorting
+     * active connections based on their activation state. */
+    if (!active)
+        return -2;
+
+    switch (nm_active_connection_get_state(active)) {
+    case NM_ACTIVE_CONNECTION_STATE_UNKNOWN:
+        return 0;
+    case NM_ACTIVE_CONNECTION_STATE_DEACTIVATED:
+        return 1;
+    case NM_ACTIVE_CONNECTION_STATE_DEACTIVATING:
+        return 2;
+    case NM_ACTIVE_CONNECTION_STATE_ACTIVATING:
+        return 3;
+    case NM_ACTIVE_CONNECTION_STATE_ACTIVATED:
+        return 4;
+    }
+    return -1;
+}
+
+int
+nmc_active_connection_cmp(NMActiveConnection *ac_a, NMActiveConnection *ac_b)
+{
+    NMSettingIPConfig * s_ip;
+    NMRemoteConnection *conn;
+    NMIPConfig *        da_ip;
+    NMIPConfig *        db_ip;
+    int                 da_num_addrs;
+    int                 db_num_addrs;
+    int                 cmp = 0;
+
+    /* Non-active sort last. */
+    NM_CMP_SELF(ac_a, ac_b);
+    NM_CMP_DIRECT(active_connection_get_state_ord(ac_b), active_connection_get_state_ord(ac_a));
+
+    /* Shared connections (likely hotspots) go on the top if possible */
+    conn = nm_active_connection_get_connection(ac_a);
+    s_ip = conn ? nm_connection_get_setting_ip6_config(NM_CONNECTION(conn)) : NULL;
+    if (s_ip
+        && strcmp(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP6_CONFIG_METHOD_SHARED) == 0)
+        cmp++;
+    conn = nm_active_connection_get_connection(ac_b);
+    s_ip = conn ? nm_connection_get_setting_ip6_config(NM_CONNECTION(conn)) : NULL;
+    if (s_ip
+        && strcmp(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP6_CONFIG_METHOD_SHARED) == 0)
+        cmp--;
+    NM_CMP_RETURN(cmp);
+
+    conn = nm_active_connection_get_connection(ac_a);
+    s_ip = conn ? nm_connection_get_setting_ip4_config(NM_CONNECTION(conn)) : NULL;
+    if (s_ip
+        && strcmp(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP4_CONFIG_METHOD_SHARED) == 0)
+        cmp++;
+    conn = nm_active_connection_get_connection(ac_b);
+    s_ip = conn ? nm_connection_get_setting_ip4_config(NM_CONNECTION(conn)) : NULL;
+    if (s_ip
+        && strcmp(nm_setting_ip_config_get_method(s_ip), NM_SETTING_IP4_CONFIG_METHOD_SHARED) == 0)
+        cmp--;
+    NM_CMP_RETURN(cmp);
+
+    /* VPNs go next */
+    NM_CMP_DIRECT(!!nm_active_connection_get_vpn(ac_a), !!nm_active_connection_get_vpn(ac_b));
+
+    /* Default devices are prioritized */
+    NM_CMP_DIRECT(nm_active_connection_get_default(ac_a), nm_active_connection_get_default(ac_b));
+
+    /* Default IPv6 devices are prioritized */
+    NM_CMP_DIRECT(nm_active_connection_get_default6(ac_a), nm_active_connection_get_default6(ac_b));
+
+    /* Sort by number of addresses. */
+    da_ip        = nm_active_connection_get_ip4_config(ac_a);
+    da_num_addrs = da_ip ? nm_ip_config_get_addresses(da_ip)->len : 0;
+    db_ip        = nm_active_connection_get_ip4_config(ac_b);
+    db_num_addrs = db_ip ? nm_ip_config_get_addresses(db_ip)->len : 0;
+
+    da_ip = nm_active_connection_get_ip6_config(ac_a);
+    da_num_addrs += da_ip ? nm_ip_config_get_addresses(da_ip)->len : 0;
+    db_ip = nm_active_connection_get_ip6_config(ac_b);
+    db_num_addrs += db_ip ? nm_ip_config_get_addresses(db_ip)->len : 0;
+
+    NM_CMP_DIRECT(da_num_addrs, db_num_addrs);
+
+    return 0;
+}
+
+static char *
+get_ac_device_string(NMActiveConnection *active)
+{
+    GString *        dev_str;
+    const GPtrArray *devices;
+    guint            i;
+
+    if (!active)
+        return NULL;
+
+    /* Get devices of the active connection */
+    dev_str = g_string_new(NULL);
+    devices = nm_active_connection_get_devices(active);
+    for (i = 0; i < devices->len; i++) {
+        NMDevice *  device    = g_ptr_array_index(devices, i);
+        const char *dev_iface = nm_device_get_iface(device);
+
+        if (dev_iface) {
+            g_string_append(dev_str, dev_iface);
+            g_string_append_c(dev_str, ',');
+        }
+    }
+    if (dev_str->len > 0)
+        g_string_truncate(dev_str, dev_str->len - 1); /* Cut off last ',' */
+
+    return g_string_free(dev_str, FALSE);
+}
+
+/*****************************************************************************/
+
+/* FIXME: The same or similar code for VPN info appears also in nm-applet (applet-dialogs.c),
+ * and in gnome-control-center as well. It could probably be shared somehow. */
+
+static const char *
+get_vpn_connection_type(NMConnection *connection)
+{
+    NMSettingVpn *s_vpn;
+    const char *  type, *p;
+
+    s_vpn = nm_connection_get_setting_vpn(connection);
+    if (!s_vpn)
+        return NULL;
+
+    /* The service type is in form of "org.freedesktop.NetworkManager.vpnc".
+     * Extract end part after last dot, e.g. "vpnc"
+     */
+    type = nm_setting_vpn_get_service_type(nm_connection_get_setting_vpn(connection));
+    if (!type)
+        return NULL;
+    p = strrchr(type, '.');
+    return p ? p + 1 : type;
+}
+
+/* VPN parameters can be found at:
+ * http://git.gnome.org/browse/network-manager-openvpn/tree/src/nm-openvpn-service.h
+ * http://git.gnome.org/browse/network-manager-vpnc/tree/src/nm-vpnc-service.h
+ * http://git.gnome.org/browse/network-manager-pptp/tree/src/nm-pptp-service.h
+ * http://git.gnome.org/browse/network-manager-openconnect/tree/src/nm-openconnect-service.h
+ * http://git.gnome.org/browse/network-manager-openswan/tree/src/nm-openswan-service.h
+ * See also 'properties' directory in these plugins.
+ */
+static const char *
+find_vpn_gateway_key(const char *vpn_type)
+{
+    if (vpn_type) {
+        if (nm_streq(vpn_type, "openvpn"))
+            return "remote";
+        if (nm_streq(vpn_type, "vpnc"))
+            return "IPSec gateway";
+        if (nm_streq(vpn_type, "pptp"))
+            return "gateway";
+        if (nm_streq(vpn_type, "openconnect"))
+            return "gateway";
+        if (nm_streq(vpn_type, "openswan"))
+            return "right";
+        if (nm_streq(vpn_type, "libreswan"))
+            return "right";
+        if (nm_streq(vpn_type, "ssh"))
+            return "remote";
+        if (nm_streq(vpn_type, "l2tp"))
+            return "gateway";
+    }
+    return NULL;
+}
+
+static const char *
+find_vpn_username_key(const char *vpn_type)
+{
+    if (vpn_type) {
+        if (nm_streq(vpn_type, "openvpn"))
+            return "username";
+        if (nm_streq(vpn_type, "vpnc"))
+            return "Xauth username";
+        if (nm_streq(vpn_type, "pptp"))
+            return "user";
+        if (nm_streq(vpn_type, "openconnect"))
+            return "username";
+        if (nm_streq(vpn_type, "openswan"))
+            return "leftxauthusername";
+        if (nm_streq(vpn_type, "libreswan"))
+            return "leftxauthusername";
+        if (nm_streq(vpn_type, "l2tp"))
+            return "user";
+    }
+    return NULL;
+}
+
+enum VpnDataItem { VPN_DATA_ITEM_GATEWAY, VPN_DATA_ITEM_USERNAME };
+
+static const char *
+get_vpn_data_item(NMConnection *connection, enum VpnDataItem vpn_data_item)
+{
+    const char *type;
+    const char *key = NULL;
+
+    type = get_vpn_connection_type(connection);
+
+    switch (vpn_data_item) {
+    case VPN_DATA_ITEM_GATEWAY:
+        key = find_vpn_gateway_key(type);
+        break;
+    case VPN_DATA_ITEM_USERNAME:
+        key = find_vpn_username_key(type);
+        break;
+    default:
+        break;
+    }
+
+    if (!key)
+        return NULL;
+    return nm_setting_vpn_get_data_item(nm_connection_get_setting_vpn(connection), key);
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    NMConnection *      connection;
+    NMActiveConnection *primary_active;
+    GPtrArray *         all_active;
+    bool                show_active_fields;
+} MetagenConShowRowData;
+
+static MetagenConShowRowData *
+_metagen_con_show_row_data_new_for_connection(NMRemoteConnection *connection,
+                                              gboolean            show_active_fields)
+{
+    MetagenConShowRowData *row_data;
+
+    row_data                     = g_slice_new0(MetagenConShowRowData);
+    row_data->connection         = g_object_ref(NM_CONNECTION(connection));
+    row_data->show_active_fields = show_active_fields;
+    return row_data;
+}
+
+static MetagenConShowRowData *
+_metagen_con_show_row_data_new_for_active_connection(NMRemoteConnection *connection,
+                                                     NMActiveConnection *active,
+                                                     gboolean            show_active_fields)
+{
+    MetagenConShowRowData *row_data;
+
+    row_data = g_slice_new0(MetagenConShowRowData);
+    if (connection)
+        row_data->connection = g_object_ref(NM_CONNECTION(connection));
+    row_data->primary_active     = g_object_ref(active);
+    row_data->show_active_fields = show_active_fields;
+    return row_data;
+}
+
+static void
+_metagen_con_show_row_data_add_active_connection(MetagenConShowRowData *row_data,
+                                                 NMActiveConnection *   active)
+{
+    if (!row_data->primary_active) {
+        row_data->primary_active = g_object_ref(active);
+        return;
+    }
+    if (!row_data->all_active) {
+        row_data->all_active = g_ptr_array_new_with_free_func(g_object_unref);
+        g_ptr_array_add(row_data->all_active, g_object_ref(row_data->primary_active));
+    }
+    g_ptr_array_add(row_data->all_active, g_object_ref(active));
+}
+
+static void
+_metagen_con_show_row_data_init_primary_active(MetagenConShowRowData *row_data)
+{
+    NMActiveConnection *ac, *best_ac;
+    guint               i;
+
+    if (!row_data->all_active)
+        return;
+
+    best_ac = row_data->all_active->pdata[0];
+    for (i = 1; i < row_data->all_active->len; i++) {
+        ac = row_data->all_active->pdata[i];
+
+        if (active_connection_get_state_ord(ac) > active_connection_get_state_ord(best_ac))
+            best_ac = ac;
+    }
+
+    if (row_data->primary_active != best_ac) {
+        g_object_unref(row_data->primary_active);
+        row_data->primary_active = g_object_ref(best_ac);
+    }
+    nm_clear_pointer(&row_data->all_active, g_ptr_array_unref);
+}
+
+static void
+_metagen_con_show_row_data_destroy(gpointer data)
+{
+    MetagenConShowRowData *row_data = data;
+
+    if (!row_data)
+        return;
+
+    g_clear_object(&row_data->connection);
+    g_clear_object(&row_data->primary_active);
+    nm_clear_pointer(&row_data->all_active, g_ptr_array_unref);
+    g_slice_free(MetagenConShowRowData, row_data);
+}
+
+static const char *
+_con_show_fcn_get_id(NMConnection *c, NMActiveConnection *ac)
+{
+    NMSettingConnection *s_con = NULL;
+    const char *         s;
+
+    if (c)
+        s_con = nm_connection_get_setting_connection(c);
+
+    s = s_con ? nm_setting_connection_get_id(s_con) : NULL;
+    if (!s && ac) {
+        /* note that if we have no s_con, that usually means that the user has no permissions
+         * to see the connection. We still fall to get the ID from the active-connection,
+         * which exposes it despite the user having no permissions.
+         *
+         * That might be unexpected, because the user is shown an ID, which he later
+         * is unable to resolve in other operations. */
+        s = nm_active_connection_get_id(ac);
+    }
+    return s;
+}
+
+static const char *
+_con_show_fcn_get_type(NMConnection *c, NMActiveConnection *ac, NMMetaAccessorGetType get_type)
+{
+    NMSettingConnection *s_con = NULL;
+    const char *         s;
+
+    if (c)
+        s_con = nm_connection_get_setting_connection(c);
+
+    s = s_con ? nm_setting_connection_get_connection_type(s_con) : NULL;
+    if (!s && ac) {
+        /* see _con_show_fcn_get_id() for why we fallback to get the value
+         * from @ac. */
+        s = nm_active_connection_get_connection_type(ac);
+    }
+    return connection_type_to_display(s, get_type);
+}
+
+static gconstpointer _metagen_con_show_get_fcn(NMC_META_GENERIC_INFO_GET_FCN_ARGS)
+{
+    const MetagenConShowRowData *row_data = target;
+    NMConnection *               c        = row_data->connection;
+    NMActiveConnection *         ac       = row_data->primary_active;
+    NMSettingConnection *        s_con    = NULL;
+    const char *                 s;
+    char *                       s_mut;
+
+    NMC_HANDLE_COLOR(nmc_active_connection_state_to_color(ac));
+
+    if (c)
+        s_con = nm_connection_get_setting_connection(c);
+
+    if (!row_data->show_active_fields) {
+        /* we are not supposed to show any fields of the active connection.
+         * We only tracked the primary_active to get the coloring right.
+         * From now on, there is no active connection. */
+        ac = NULL;
+
+        /* in this mode, we expect that we are called only with connections that
+         * have a [connection] setting and a UUID. Otherwise, the connection is
+         * effectively invisible to the user, and should be hidden.
+         *
+         * But in that case, we expect that the caller pre-filtered this row out.
+         * So assert(). */
+        nm_assert(s_con);
+        nm_assert(nm_setting_connection_get_uuid(s_con));
+    }
+
+    nm_assert(
+        NM_IN_SET(get_type, NM_META_ACCESSOR_GET_TYPE_PRETTY, NM_META_ACCESSOR_GET_TYPE_PARSABLE));
+
+    switch (info->info_type) {
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_NAME:
+        return _con_show_fcn_get_id(c, ac);
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_UUID:
+        s = s_con ? nm_setting_connection_get_uuid(s_con) : NULL;
+        if (!s && ac) {
+            /* see _con_show_fcn_get_id() for why we fallback to get the value
+             * from @ac. */
+            s = nm_active_connection_get_uuid(ac);
+        }
+        return s;
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_TYPE:
+        return _con_show_fcn_get_type(c, ac, get_type);
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_TIMESTAMP:
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_TIMESTAMP_REAL:
+        if (!s_con)
+            return NULL;
+        {
+            guint64 timestamp;
+            time_t  timestamp_real;
+
+            timestamp = nm_setting_connection_get_timestamp(s_con);
+
+            if (info->info_type == NMC_GENERIC_INFO_TYPE_CON_SHOW_TIMESTAMP)
+                return (*out_to_free = g_strdup_printf("%" G_GUINT64_FORMAT, timestamp));
+            else {
+                struct tm localtime_result;
+
+                if (!timestamp) {
+                    if (get_type == NM_META_ACCESSOR_GET_TYPE_PRETTY)
+                        return _("never");
+                    return "never";
+                }
+                timestamp_real = timestamp;
+                s_mut          = g_malloc0(128);
+                strftime(s_mut, 127, "%c", localtime_r(&timestamp_real, &localtime_result));
+                return (*out_to_free = s_mut);
+            }
+        }
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_AUTOCONNECT:
+        if (!s_con)
+            return NULL;
+        return nmc_meta_generic_get_bool(nm_setting_connection_get_autoconnect(s_con), get_type);
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_AUTOCONNECT_PRIORITY:
+        if (!s_con)
+            return NULL;
+        return (*out_to_free =
+                    g_strdup_printf("%d", nm_setting_connection_get_autoconnect_priority(s_con)));
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_READONLY:
+        if (!s_con)
+            return NULL;
+        return nmc_meta_generic_get_bool(nm_setting_connection_get_read_only(s_con), get_type);
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_DBUS_PATH:
+        if (!c)
+            return NULL;
+        return nm_connection_get_path(c);
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_ACTIVE:
+        return nmc_meta_generic_get_bool(!!ac, get_type);
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_DEVICE:
+        if (ac)
+            return (*out_to_free = get_ac_device_string(ac));
+        return NULL;
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_STATE:
+        return nmc_meta_generic_get_str_i18n(
+            ac ? active_connection_state_to_string(nm_active_connection_get_state(ac)) : NULL,
+            get_type);
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_ACTIVE_PATH:
+        if (ac)
+            return nm_object_get_path(NM_OBJECT(ac));
+        return NULL;
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_SLAVE:
+        if (!s_con)
+            return NULL;
+        return nm_setting_connection_get_slave_type(s_con);
+    case NMC_GENERIC_INFO_TYPE_CON_SHOW_FILENAME:
+        if (!NM_IS_REMOTE_CONNECTION(c))
+            return NULL;
+        return nm_remote_connection_get_filename(NM_REMOTE_CONNECTION(c));
+    default:
+        break;
+    }
+
+    g_return_val_if_reached(NULL);
+}
+
+const NmcMetaGenericInfo *const metagen_con_show[_NMC_GENERIC_INFO_TYPE_CON_SHOW_NUM + 1] = {
+#define _METAGEN_CON_SHOW(type, name) \
+    [type] = NMC_META_GENERIC(name, .info_type = type, .get_fcn = _metagen_con_show_get_fcn)
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_NAME, "NAME"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_UUID, "UUID"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_TYPE, "TYPE"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_TIMESTAMP, "TIMESTAMP"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_TIMESTAMP_REAL, "TIMESTAMP-REAL"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_AUTOCONNECT, "AUTOCONNECT"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_AUTOCONNECT_PRIORITY, "AUTOCONNECT-PRIORITY"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_READONLY, "READONLY"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_DBUS_PATH, "DBUS-PATH"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_ACTIVE, "ACTIVE"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_DEVICE, "DEVICE"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_STATE, "STATE"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_ACTIVE_PATH, "ACTIVE-PATH"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_SLAVE, "SLAVE"),
+    _METAGEN_CON_SHOW(NMC_GENERIC_INFO_TYPE_CON_SHOW_FILENAME, "FILENAME"),
+};
+#define NMC_FIELDS_CON_SHOW_COMMON "NAME,UUID,TYPE,DEVICE"
+
+/*****************************************************************************/
+
+static gconstpointer _metagen_con_active_general_get_fcn(NMC_META_GENERIC_INFO_GET_FCN_ARGS)
+{
+    NMActiveConnection * ac = target;
+    NMConnection *       c;
+    NMSettingConnection *s_con = NULL;
+    NMDevice *           dev;
+    guint                i;
+    const char *         s;
+
+    NMC_HANDLE_COLOR(NM_META_COLOR_NONE);
+
+    nm_assert(
+        NM_IN_SET(get_type, NM_META_ACCESSOR_GET_TYPE_PRETTY, NM_META_ACCESSOR_GET_TYPE_PARSABLE));
+
+    c = NM_CONNECTION(nm_active_connection_get_connection(ac));
+    if (c)
+        s_con = nm_connection_get_setting_connection(c);
+
+    switch (info->info_type) {
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_NAME:
+        return nm_active_connection_get_id(ac);
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_UUID:
+        return nm_active_connection_get_uuid(ac);
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_DEVICES:
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_IP_IFACE:
+    {
+        GString *        str = NULL;
+        const GPtrArray *devices;
+
+        s       = NULL;
+        devices = nm_active_connection_get_devices(ac);
+        if (devices) {
+            for (i = 0; i < devices->len; i++) {
+                NMDevice *  device = g_ptr_array_index(devices, i);
+                const char *iface;
+
+                if (info->info_type == NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_DEVICES) {
+                    iface = nm_device_get_iface(device);
+                } else {
+                    iface = nm_device_get_ip_iface(device);
+                }
+
+                if (!iface)
+                    continue;
+                if (!s) {
+                    s = iface;
+                    continue;
+                }
+                if (!str)
+                    str = g_string_new(s);
+                g_string_append_c(str, ',');
+                g_string_append(str, iface);
+            }
+        }
+        if (str)
+            return (*out_to_free = g_string_free(str, FALSE));
+        return s;
+    }
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_STATE:
+        return nmc_meta_generic_get_str_i18n(
+            active_connection_state_to_string(nm_active_connection_get_state(ac)),
+            get_type);
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_DEFAULT:
+        return nmc_meta_generic_get_bool(nm_active_connection_get_default(ac), get_type);
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_DEFAULT6:
+        return nmc_meta_generic_get_bool(nm_active_connection_get_default6(ac), get_type);
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_SPEC_OBJECT:
+        return nm_active_connection_get_specific_object_path(ac);
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_VPN:
+        return nmc_meta_generic_get_bool(NM_IS_VPN_CONNECTION(ac), get_type);
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_DBUS_PATH:
+        return nm_object_get_path(NM_OBJECT(ac));
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_CON_PATH:
+        return c ? nm_connection_get_path(c) : NULL;
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_ZONE:
+        /* this is really ugly, because the zone is not a property of the active-connection,
+         * but the settings-connection profile. There is no guarantee, that they agree. */
+        return s_con ? nm_setting_connection_get_zone(s_con) : NULL;
+    case NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_MASTER_PATH:
+        dev = nm_active_connection_get_master(ac);
+        return dev ? nm_object_get_path(NM_OBJECT(dev)) : NULL;
+    default:
+        break;
+    }
+
+    g_return_val_if_reached(NULL);
+}
+
+const NmcMetaGenericInfo
+    *const metagen_con_active_general[_NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_NUM + 1] = {
+#define _METAGEN_CON_ACTIVE_GENERAL(type, name) \
+    [type] =                                    \
+        NMC_META_GENERIC(name, .info_type = type, .get_fcn = _metagen_con_active_general_get_fcn)
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_NAME, "NAME"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_UUID, "UUID"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_DEVICES, "DEVICES"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_IP_IFACE, "IP-IFACE"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_STATE, "STATE"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_DEFAULT, "DEFAULT"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_DEFAULT6, "DEFAULT6"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_SPEC_OBJECT,
+                                    "SPEC-OBJECT"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_VPN, "VPN"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_DBUS_PATH,
+                                    "DBUS-PATH"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_CON_PATH, "CON-PATH"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_ZONE, "ZONE"),
+        _METAGEN_CON_ACTIVE_GENERAL(NMC_GENERIC_INFO_TYPE_CON_ACTIVE_GENERAL_MASTER_PATH,
+                                    "MASTER-PATH"),
+};
+
+/*****************************************************************************/
+
+static gconstpointer _metagen_con_active_vpn_get_fcn(NMC_META_GENERIC_INFO_GET_FCN_ARGS)
+{
+    NMActiveConnection * ac = target;
+    NMConnection *       c;
+    NMSettingVpn *       s_vpn = NULL;
+    NMVpnConnectionState vpn_state;
+    guint                i;
+    const char *         s;
+    char **              arr = NULL;
+
+    nm_assert(NM_IS_VPN_CONNECTION(ac));
+
+    NMC_HANDLE_COLOR(NM_META_COLOR_NONE);
+
+    nm_assert(
+        NM_IN_SET(get_type, NM_META_ACCESSOR_GET_TYPE_PRETTY, NM_META_ACCESSOR_GET_TYPE_PARSABLE));
+
+    c = NM_CONNECTION(nm_active_connection_get_connection(ac));
+    if (c)
+        s_vpn = nm_connection_get_setting_vpn(c);
+
+    switch (info->info_type) {
+    case NMC_GENERIC_INFO_TYPE_CON_VPN_TYPE:
+        return c ? get_vpn_connection_type(c) : NULL;
+    case NMC_GENERIC_INFO_TYPE_CON_VPN_USERNAME:
+        if (s_vpn && (s = nm_setting_vpn_get_user_name(s_vpn)))
+            return s;
+        return c ? get_vpn_data_item(c, VPN_DATA_ITEM_USERNAME) : NULL;
+    case NMC_GENERIC_INFO_TYPE_CON_VPN_GATEWAY:
+        return c ? get_vpn_data_item(c, VPN_DATA_ITEM_GATEWAY) : NULL;
+    case NMC_GENERIC_INFO_TYPE_CON_VPN_BANNER:
+        s = nm_vpn_connection_get_banner(NM_VPN_CONNECTION(ac));
+        if (s)
+            return (*out_to_free = g_strescape(s, ""));
+        return NULL;
+    case NMC_GENERIC_INFO_TYPE_CON_VPN_VPN_STATE:
+        vpn_state = nm_vpn_connection_get_vpn_state(NM_VPN_CONNECTION(ac));
+        return (*out_to_free =
+                    nmc_meta_generic_get_enum_with_detail(NMC_META_GENERIC_GET_ENUM_TYPE_DASH,
+                                                          vpn_state,
+                                                          vpn_connection_state_to_string(vpn_state),
+                                                          get_type));
+    case NMC_GENERIC_INFO_TYPE_CON_VPN_CFG:
+        if (!NM_FLAGS_HAS(get_flags, NM_META_ACCESSOR_GET_FLAGS_ACCEPT_STRV))
+            return NULL;
+        if (s_vpn) {
+            gs_free char **arr2 = NULL;
+            guint          n;
+
+            arr2 = (char **) nm_setting_vpn_get_data_keys(s_vpn, &n);
+            if (!n)
+                goto arr_out;
+
+            nm_assert(arr2 && !arr2[n]);
+            for (i = 0; i < n; i++) {
+                const char *k = arr2[i];
+                const char *v;
+
+                nm_assert(k);
+                v = nm_setting_vpn_get_data_item(s_vpn, k);
+                /* update the arr array in-place. Previously it contained
+                 * the constant keys, now it contains the strdup'ed output text. */
+                arr2[i] = g_strdup_printf("%s = %s", k, v);
+            }
+
+            arr = g_steal_pointer(&arr2);
+        }
+        goto arr_out;
+    default:
+        break;
+    }
+
+    g_return_val_if_reached(NULL);
+
+arr_out:
+    NM_SET_OUT(out_is_default, !arr || !arr[0]);
+    *out_flags |= NM_META_ACCESSOR_GET_OUT_FLAGS_STRV;
+    *out_to_free = arr;
+    return arr;
+}
+
+const NmcMetaGenericInfo
+    *const metagen_con_active_vpn[_NMC_GENERIC_INFO_TYPE_CON_ACTIVE_VPN_NUM + 1] = {
+#define _METAGEN_CON_ACTIVE_VPN(type, name) \
+    [type] = NMC_META_GENERIC(name, .info_type = type, .get_fcn = _metagen_con_active_vpn_get_fcn)
+        _METAGEN_CON_ACTIVE_VPN(NMC_GENERIC_INFO_TYPE_CON_VPN_TYPE, "TYPE"),
+        _METAGEN_CON_ACTIVE_VPN(NMC_GENERIC_INFO_TYPE_CON_VPN_USERNAME, "USERNAME"),
+        _METAGEN_CON_ACTIVE_VPN(NMC_GENERIC_INFO_TYPE_CON_VPN_GATEWAY, "GATEWAY"),
+        _METAGEN_CON_ACTIVE_VPN(NMC_GENERIC_INFO_TYPE_CON_VPN_BANNER, "BANNER"),
+        _METAGEN_CON_ACTIVE_VPN(NMC_GENERIC_INFO_TYPE_CON_VPN_VPN_STATE, "VPN-STATE"),
+        _METAGEN_CON_ACTIVE_VPN(NMC_GENERIC_INFO_TYPE_CON_VPN_CFG, "CFG"),
+};
+
+/*****************************************************************************/
+
+#define NMC_FIELDS_SETTINGS_NAMES_ALL                                                  \
+    NM_SETTING_CONNECTION_SETTING_NAME                                                 \
+    "," NM_SETTING_MATCH_SETTING_NAME "," NM_SETTING_WIRED_SETTING_NAME                \
+    "," NM_SETTING_VETH_SETTING_NAME "," NM_SETTING_802_1X_SETTING_NAME                \
+    "," NM_SETTING_WIRELESS_SETTING_NAME "," NM_SETTING_WIRELESS_SECURITY_SETTING_NAME \
+    "," NM_SETTING_IP4_CONFIG_SETTING_NAME "," NM_SETTING_IP6_CONFIG_SETTING_NAME      \
+    "," NM_SETTING_SERIAL_SETTING_NAME "," NM_SETTING_WIFI_P2P_SETTING_NAME            \
+    "," NM_SETTING_PPP_SETTING_NAME "," NM_SETTING_PPPOE_SETTING_NAME                  \
+    "," NM_SETTING_ADSL_SETTING_NAME "," NM_SETTING_GSM_SETTING_NAME                   \
+    "," NM_SETTING_CDMA_SETTING_NAME "," NM_SETTING_BLUETOOTH_SETTING_NAME             \
+    "," NM_SETTING_OLPC_MESH_SETTING_NAME "," NM_SETTING_VPN_SETTING_NAME              \
+    "," NM_SETTING_INFINIBAND_SETTING_NAME "," NM_SETTING_BOND_SETTING_NAME            \
+    "," NM_SETTING_VLAN_SETTING_NAME "," NM_SETTING_BRIDGE_SETTING_NAME                \
+    "," NM_SETTING_BRIDGE_PORT_SETTING_NAME "," NM_SETTING_TEAM_SETTING_NAME           \
+    "," NM_SETTING_TEAM_PORT_SETTING_NAME "," NM_SETTING_OVS_BRIDGE_SETTING_NAME       \
+    "," NM_SETTING_OVS_INTERFACE_SETTING_NAME "," NM_SETTING_OVS_PATCH_SETTING_NAME    \
+    "," NM_SETTING_OVS_PORT_SETTING_NAME "," NM_SETTING_DCB_SETTING_NAME               \
+    "," NM_SETTING_TUN_SETTING_NAME "," NM_SETTING_IP_TUNNEL_SETTING_NAME              \
+    "," NM_SETTING_MACSEC_SETTING_NAME "," NM_SETTING_MACVLAN_SETTING_NAME             \
+    "," NM_SETTING_VXLAN_SETTING_NAME "," NM_SETTING_VRF_SETTING_NAME                  \
+    "," NM_SETTING_WPAN_SETTING_NAME "," NM_SETTING_6LOWPAN_SETTING_NAME               \
+    "," NM_SETTING_WIREGUARD_SETTING_NAME "," NM_SETTING_PROXY_SETTING_NAME            \
+    "," NM_SETTING_TC_CONFIG_SETTING_NAME "," NM_SETTING_SRIOV_SETTING_NAME            \
+    "," NM_SETTING_ETHTOOL_SETTING_NAME "," NM_SETTING_OVS_DPDK_SETTING_NAME           \
+    "," NM_SETTING_HOSTNAME_SETTING_NAME /* NM_SETTING_DUMMY_SETTING_NAME NM_SETTING_WIMAX_SETTING_NAME */
+
+const NmcMetaGenericInfo *const nmc_fields_con_active_details_groups[] = {
+    NMC_META_GENERIC_WITH_NESTED("GENERAL", metagen_con_active_general), /* 0 */
+    NMC_META_GENERIC_WITH_NESTED("IP4", metagen_ip4_config),             /* 1 */
+    NMC_META_GENERIC_WITH_NESTED("DHCP4", metagen_dhcp_config),          /* 2 */
+    NMC_META_GENERIC_WITH_NESTED("IP6", metagen_ip6_config),             /* 3 */
+    NMC_META_GENERIC_WITH_NESTED("DHCP6", metagen_dhcp_config),          /* 4 */
+    NMC_META_GENERIC_WITH_NESTED("VPN", metagen_con_active_vpn),         /* 5 */
+    NULL,
+};
+
+/* Pseudo group names for 'connection show <con>' */
+/* e.g.: nmcli -f profile con show my-eth0 */
+/* e.g.: nmcli -f active con show my-eth0 */
+#define CON_SHOW_DETAIL_GROUP_PROFILE "profile"
+#define CON_SHOW_DETAIL_GROUP_ACTIVE  "active"
+
+static guint progress_id = 0; /* ID of event source for displaying progress */
+
+/* for readline TAB completion in editor */
+typedef struct {
+    NmCli *       nmc;
+    char *        con_type;
+    NMConnection *connection;
+    NMSetting *   setting;
+    const char *  property;
+    char **       words;
+} TabCompletionInfo;
+
+static TabCompletionInfo nmc_tab_completion;
+
+/*****************************************************************************/
+
+static void
+usage(void)
+{
+    g_printerr(
+        _("Usage: nmcli connection { COMMAND | help }\n\n"
+          "COMMAND := { show | up | down | add | modify | clone | edit | delete | monitor | reload "
+          "| load | import | export }\n\n"
+          "  show [--active] [--order <order spec>]\n"
+          "  show [--active] [id | uuid | path | apath] <ID> ...\n\n"
+          "  up [[id | uuid | path] <ID>] [ifname <ifname>] [ap <BSSID>] [passwd-file <file with "
+          "passwords>]\n\n"
+          "  down [id | uuid | path | apath] <ID> ...\n\n"
+          "  add COMMON_OPTIONS TYPE_SPECIFIC_OPTIONS SLAVE_OPTIONS IP_OPTIONS [-- "
+          "([+|-]<setting>.<property> <value>)+]\n\n"
+          "  modify [--temporary] [id | uuid | path] <ID> ([+|-]<setting>.<property> <value>)+\n\n"
+          "  clone [--temporary] [id | uuid | path ] <ID> <new name>\n\n"
+          "  edit [id | uuid | path] <ID>\n"
+          "  edit [type <new_con_type>] [con-name <new_con_name>]\n\n"
+          "  delete [id | uuid | path] <ID>\n\n"
+          "  monitor [id | uuid | path] <ID> ...\n\n"
+          "  reload\n\n"
+          "  load <filename> [ <filename>... ]\n\n"
+          "  import [--temporary] type <type> file <file to import>\n\n"
+          "  export [id | uuid | path] <ID> [<output file>]\n\n"));
+}
+
+static void
+usage_connection_show(void)
+{
+    g_printerr(
+        _("Usage: nmcli connection show { ARGUMENTS | help }\n"
+          "\n"
+          "ARGUMENTS := [--active] [--order <order spec>]\n"
+          "\n"
+          "List in-memory and on-disk connection profiles, some of which may also be\n"
+          "active if a device is using that connection profile. Without a parameter, all\n"
+          "profiles are listed. When --active option is specified, only the active\n"
+          "profiles are shown. --order allows custom connection ordering (see manual page).\n"
+          "\n"
+          "ARGUMENTS := [--active] [id | uuid | path | apath] <ID> ...\n"
+          "\n"
+          "Show details for specified connections. By default, both static configuration\n"
+          "and active connection data are displayed. It is possible to filter the output\n"
+          "using global '--fields' option. Refer to the manual page for more information.\n"
+          "When --active option is specified, only the active profiles are taken into\n"
+          "account. Use global --show-secrets option to reveal associated secrets as well.\n"));
+}
+
+static void
+usage_connection_up(void)
+{
+    g_printerr(_("Usage: nmcli connection up { ARGUMENTS | help }\n"
+                 "\n"
+                 "ARGUMENTS := [id | uuid | path] <ID> [ifname <ifname>] [ap <BSSID>] [nsp <name>] "
+                 "[passwd-file <file with passwords>]\n"
+                 "\n"
+                 "Activate a connection on a device. The profile to activate is identified by its\n"
+                 "name, UUID or D-Bus path.\n"
+                 "\n"
+                 "ARGUMENTS := ifname <ifname> [ap <BSSID>] [nsp <name>] [passwd-file <file with "
+                 "passwords>]\n"
+                 "\n"
+                 "Activate a device with a connection. The connection profile is selected\n"
+                 "automatically by NetworkManager.\n"
+                 "\n"
+                 "ifname      - specifies the device to active the connection on\n"
+                 "ap          - specifies AP to connect to (only valid for Wi-Fi)\n"
+                 "nsp         - specifies NSP to connect to (only valid for WiMAX)\n"
+                 "passwd-file - file with password(s) required to activate the connection\n\n"));
+}
+
+static void
+usage_connection_down(void)
+{
+    g_printerr(_("Usage: nmcli connection down { ARGUMENTS | help }\n"
+                 "\n"
+                 "ARGUMENTS := [id | uuid | path | apath] <ID> ...\n"
+                 "\n"
+                 "Deactivate a connection from a device (without preventing the device from\n"
+                 "further auto-activation). The profile to deactivate is identified by its name,\n"
+                 "UUID or D-Bus path.\n\n"));
+}
+
+static void
+usage_connection_add(void)
+{
+    g_printerr(_("Usage: nmcli connection add { ARGUMENTS | help }\n"
+                 "\n"
+                 "ARGUMENTS := COMMON_OPTIONS TYPE_SPECIFIC_OPTIONS SLAVE_OPTIONS IP_OPTIONS [-- "
+                 "([+|-]<setting>.<property> <value>)+]\n\n"
+                 "  COMMON_OPTIONS:\n"
+                 "                  type <type>\n"
+                 "                  ifname <interface name> | \"*\"\n"
+                 "                  [con-name <connection name>]\n"
+                 "                  [autoconnect yes|no]\n"
+                 "                  [save yes|no]\n"
+                 "                  [master <master (ifname, or connection UUID or name)>]\n"
+                 "                  [slave-type <master connection type>]\n\n"
+                 "  TYPE_SPECIFIC_OPTIONS:\n"
+                 "    ethernet:     [mac <MAC address>]\n"
+                 "                  [cloned-mac <cloned MAC address>]\n"
+                 "                  [mtu <MTU>]\n\n"
+                 "    wifi:         ssid <SSID>\n"
+                 "                  [mac <MAC address>]\n"
+                 "                  [cloned-mac <cloned MAC address>]\n"
+                 "                  [mtu <MTU>]\n"
+                 "                  [mode infrastructure|ap|adhoc]\n\n"
+                 "    wimax:        [mac <MAC address>]\n"
+                 "                  [nsp <NSP>]\n\n"
+                 "    pppoe:        username <PPPoE username>\n"
+                 "                  [password <PPPoE password>]\n"
+                 "                  [service <PPPoE service name>]\n"
+                 "                  [mtu <MTU>]\n"
+                 "                  [mac <MAC address>]\n\n"
+                 "    gsm:          apn <APN>\n"
+                 "                  [user <username>]\n"
+                 "                  [password <password>]\n\n"
+                 "    cdma:         [user <username>]\n"
+                 "                  [password <password>]\n\n"
+                 "    infiniband:   [mac <MAC address>]\n"
+                 "                  [mtu <MTU>]\n"
+                 "                  [transport-mode datagram | connected]\n"
+                 "                  [parent <ifname>]\n"
+                 "                  [p-key <IPoIB P_Key>]\n\n"
+                 "    bluetooth:    [addr <bluetooth address>]\n"
+                 "                  [bt-type panu|nap|dun-gsm|dun-cdma]\n\n"
+                 "    vlan:         dev <parent device (connection UUID, ifname, or MAC)>\n"
+                 "                  id <VLAN ID>\n"
+                 "                  [flags <VLAN flags>]\n"
+                 "                  [ingress <ingress priority mapping>]\n"
+                 "                  [egress <egress priority mapping>]\n"
+                 "                  [mtu <MTU>]\n\n"
+                 "    bond:         [mode balance-rr (0) | active-backup (1) | balance-xor (2) | "
+                 "broadcast (3) |\n"
+                 "                        802.3ad    (4) | balance-tlb   (5) | balance-alb (6)]\n"
+                 "                  [primary <ifname>]\n"
+                 "                  [miimon <num>]\n"
+                 "                  [downdelay <num>]\n"
+                 "                  [updelay <num>]\n"
+                 "                  [arp-interval <num>]\n"
+                 "                  [arp-ip-target <num>]\n"
+                 "                  [lacp-rate slow (0) | fast (1)]\n\n"
+                 "    bond-slave:   master <master (ifname, or connection UUID or name)>\n\n"
+                 "    team:         [config <file>|<raw JSON data>]\n\n"
+                 "    team-slave:   master <master (ifname, or connection UUID or name)>\n"
+                 "                  [config <file>|<raw JSON data>]\n\n"
+                 "    bridge:       [stp yes|no]\n"
+                 "                  [priority <num>]\n"
+                 "                  [forward-delay <2-30>]\n"
+                 "                  [hello-time <1-10>]\n"
+                 "                  [max-age <6-40>]\n"
+                 "                  [ageing-time <0-1000000>]\n"
+                 "                  [multicast-snooping yes|no]\n"
+                 "                  [mac <MAC address>]\n\n"
+                 "    bridge-slave: master <master (ifname, or connection UUID or name)>\n"
+                 "                  [priority <0-63>]\n"
+                 "                  [path-cost <1-65535>]\n"
+                 "                  [hairpin yes|no]\n\n"
+                 "    vpn:          vpn-type "
+                 "vpnc|openvpn|pptp|openconnect|openswan|libreswan|ssh|l2tp|iodine|...\n"
+                 "                  [user <username>]\n\n"
+                 "    olpc-mesh:    ssid <SSID>\n"
+                 "                  [channel <1-13>]\n"
+                 "                  [dhcp-anycast <MAC address>]\n\n"
+                 "    adsl:         username <username>\n"
+                 "                  protocol pppoa|pppoe|ipoatm\n"
+                 "                  [password <password>]\n"
+                 "                  [encapsulation vcmux|llc]\n\n"
+                 "    tun:          mode tun|tap\n"
+                 "                  [owner <UID>]\n"
+                 "                  [group <GID>]\n"
+                 "                  [pi yes|no]\n"
+                 "                  [vnet-hdr yes|no]\n"
+                 "                  [multi-queue yes|no]\n\n"
+                 "    ip-tunnel:    mode ipip|gre|sit|isatap|vti|ip6ip6|ipip6|ip6gre|vti6\n"
+                 "                  remote <remote endpoint IP>\n"
+                 "                  [local <local endpoint IP>]\n"
+                 "                  [dev <parent device (ifname or connection UUID)>]\n\n"
+                 "    macsec:       dev <parent device (connection UUID, ifname, or MAC)>\n"
+                 "                  mode <psk|eap>\n"
+                 "                  [cak <key> ckn <key>]\n"
+                 "                  [encrypt yes|no]\n"
+                 "                  [port 1-65534]\n\n\n"
+                 "    macvlan:      dev <parent device (connection UUID, ifname, or MAC)>\n"
+                 "                  mode vepa|bridge|private|passthru|source\n"
+                 "                  [tap yes|no]\n\n"
+                 "    vxlan:        id <VXLAN ID>\n"
+                 "                  [remote <IP of multicast group or remote address>]\n"
+                 "                  [local <source IP>]\n"
+                 "                  [dev <parent device (ifname or connection UUID)>]\n"
+                 "                  [source-port-min <0-65535>]\n"
+                 "                  [source-port-max <0-65535>]\n"
+                 "                  [destination-port <0-65535>]\n\n"
+                 "    wpan:         [short-addr <0x0000-0xffff>]\n"
+                 "                  [pan-id <0x0000-0xffff>]\n"
+                 "                  [page <default|0-31>]\n"
+                 "                  [channel <default|0-26>]\n"
+                 "                  [mac <MAC address>]\n\n"
+                 "    6lowpan:      dev <parent device (connection UUID, ifname, or MAC)>\n"
+                 "    dummy:\n\n"
+                 "  SLAVE_OPTIONS:\n"
+                 "    bridge:       [priority <0-63>]\n"
+                 "                  [path-cost <1-65535>]\n"
+                 "                  [hairpin yes|no]\n\n"
+                 "    team:         [config <file>|<raw JSON data>]\n\n"
+                 "  IP_OPTIONS:\n"
+                 "                  [ip4 <IPv4 address>] [gw4 <IPv4 gateway>]\n"
+                 "                  [ip6 <IPv6 address>] [gw6 <IPv6 gateway>]\n\n"));
+}
+
+static void
+usage_connection_modify(void)
+{
+    g_printerr(
+        _("Usage: nmcli connection modify { ARGUMENTS | help }\n"
+          "\n"
+          "ARGUMENTS := [id | uuid | path] <ID> ([+|-]<setting>.<property> <value>)+\n"
+          "\n"
+          "Modify one or more properties of the connection profile.\n"
+          "The profile is identified by its name, UUID or D-Bus path. For multi-valued\n"
+          "properties you can use optional '+' or '-' prefix to the property name.\n"
+          "The '+' sign allows appending items instead of overwriting the whole value.\n"
+          "The '-' sign allows removing selected items instead of the whole value.\n"
+          "\n"
+          "ARGUMENTS := remove <setting>\n"
+          "\n"
+          "Remove a setting from the connection profile.\n"
+          "\n"
+          "Examples:\n"
+          "nmcli con mod home-wifi wifi.ssid rakosnicek\n"
+          "nmcli con mod em1-1 ipv4.method manual ipv4.addr \"192.168.1.2/24, 10.10.1.5/8\"\n"
+          "nmcli con mod em1-1 +ipv4.dns 8.8.4.4\n"
+          "nmcli con mod em1-1 -ipv4.dns 1\n"
+          "nmcli con mod em1-1 -ipv6.addr \"abbe::cafe/56\"\n"
+          "nmcli con mod bond0 +bond.options mii=500\n"
+          "nmcli con mod bond0 -bond.options downdelay\n"
+          "nmcli con mod em1-1 remove sriov\n\n"));
+}
+
+static void
+usage_connection_clone(void)
+{
+    g_printerr(_("Usage: nmcli connection clone { ARGUMENTS | help }\n"
+                 "\n"
+                 "ARGUMENTS := [--temporary] [id | uuid | path] <ID> <new name>\n"
+                 "\n"
+                 "Clone an existing connection profile. The newly created connection will be\n"
+                 "the exact copy of the <ID>, except the uuid property (will be generated) and\n"
+                 "id (provided as <new name> argument).\n\n"));
+}
+
+static void
+usage_connection_edit(void)
+{
+    g_printerr(_("Usage: nmcli connection edit { ARGUMENTS | help }\n"
+                 "\n"
+                 "ARGUMENTS := [id | uuid | path] <ID>\n"
+                 "\n"
+                 "Edit an existing connection profile in an interactive editor.\n"
+                 "The profile is identified by its name, UUID or D-Bus path\n"
+                 "\n"
+                 "ARGUMENTS := [type <new connection type>] [con-name <new connection name>]\n"
+                 "\n"
+                 "Add a new connection profile in an interactive editor.\n\n"));
+}
+
+static void
+usage_connection_delete(void)
+{
+    g_printerr(_("Usage: nmcli connection delete { ARGUMENTS | help }\n"
+                 "\n"
+                 "ARGUMENTS := [id | uuid | path] <ID>\n"
+                 "\n"
+                 "Delete a connection profile.\n"
+                 "The profile is identified by its name, UUID or D-Bus path.\n\n"));
+}
+
+static void
+usage_connection_monitor(void)
+{
+    g_printerr(_("Usage: nmcli connection monitor { ARGUMENTS | help }\n"
+                 "\n"
+                 "ARGUMENTS := [id | uuid | path] <ID> ...\n"
+                 "\n"
+                 "Monitor connection profile activity.\n"
+                 "This command prints a line whenever the specified connection changes.\n"
+                 "Monitors all connection profiles in case none is specified.\n\n"));
+}
+
+static void
+usage_connection_reload(void)
+{
+    g_printerr(_("Usage: nmcli connection reload { help }\n"
+                 "\n"
+                 "Reload all connection files from disk.\n\n"));
+}
+
+static void
+usage_connection_load(void)
+{
+    g_printerr(_("Usage: nmcli connection load { ARGUMENTS | help }\n"
+                 "\n"
+                 "ARGUMENTS := <filename> [<filename>...]\n"
+                 "\n"
+                 "Load/reload one or more connection files from disk. Use this after manually\n"
+                 "editing a connection file to ensure that NetworkManager is aware of its latest\n"
+                 "state.\n\n"));
+}
+
+static void
+usage_connection_import(void)
+{
+    g_printerr(
+        _("Usage: nmcli connection import { ARGUMENTS | help }\n"
+          "\n"
+          "ARGUMENTS := [--temporary] type <type> file <file to import>\n"
+          "\n"
+          "Import an external/foreign configuration as a NetworkManager connection profile.\n"
+          "The type of the input file is specified by type option.\n"
+          "Only VPN configurations are supported at the moment. The configuration\n"
+          "is imported by NetworkManager VPN plugins.\n\n"));
+}
+
+static void
+usage_connection_export(void)
+{
+    g_printerr(_("Usage: nmcli connection export { ARGUMENTS | help }\n"
+                 "\n"
+                 "ARGUMENTS := [id | uuid | path] <ID> [<output file>]\n"
+                 "\n"
+                 "Export a connection. Only VPN connections are supported at the moment.\n"
+                 "The data are directed to standard output or to a file if a name is given.\n\n"));
+}
+
+static void
+quit(void)
+{
+    if (nm_clear_g_source(&progress_id))
+        nmc_terminal_erase_line();
+    g_main_loop_quit(loop);
+}
+
+static char *
+construct_header_name(const char *base, const char *spec)
+{
+    if (spec == NULL)
+        return g_strdup(base);
+
+    return g_strdup_printf("%s (%s)", base, spec);
+}
+
+static int
+get_ac_for_connection_cmp(gconstpointer pa, gconstpointer pb)
+{
+    NMActiveConnection *ac_a = *((NMActiveConnection *const *) pa);
+    NMActiveConnection *ac_b = *((NMActiveConnection *const *) pb);
+
+    NM_CMP_RETURN(nmc_active_connection_cmp(ac_a, ac_b));
+    NM_CMP_DIRECT_STRCMP0(nm_active_connection_get_id(ac_a), nm_active_connection_get_id(ac_b));
+    NM_CMP_DIRECT_STRCMP0(nm_active_connection_get_connection_type(ac_a),
+                          nm_active_connection_get_connection_type(ac_b));
+    NM_CMP_DIRECT_STRCMP0(nm_object_get_path(NM_OBJECT(ac_a)), nm_object_get_path(NM_OBJECT(ac_b)));
+
+    g_return_val_if_reached(0);
+}
+
+static NMActiveConnection *
+get_ac_for_connection(const GPtrArray *active_cons,
+                      NMConnection *   connection,
+                      GPtrArray **     out_result)
+{
+    guint               i;
+    NMActiveConnection *best_candidate = NULL;
+    GPtrArray *         result         = out_result ? *out_result : NULL;
+
+    for (i = 0; i < active_cons->len; i++) {
+        NMActiveConnection *candidate = g_ptr_array_index(active_cons, i);
+        NMRemoteConnection *con;
+
+        con = nm_active_connection_get_connection(candidate);
+        if (NM_CONNECTION(con) != connection)
+            continue;
+
+        if (!out_result)
+            return candidate;
+        if (!result)
+            result = g_ptr_array_new_with_free_func(g_object_unref);
+        g_ptr_array_add(result, g_object_ref(candidate));
+    }
+
+    if (result) {
+        g_ptr_array_sort(result, get_ac_for_connection_cmp);
+        best_candidate = result->pdata[0];
+    }
+
+    NM_SET_OUT(out_result, result);
+    return best_candidate;
+}
+
+typedef struct {
+    GMainLoop *   loop;
+    NMConnection *local;
+    const char *  setting_name;
+} GetSecretsData;
+
+static void
+got_secrets(GObject *source_object, GAsyncResult *res, gpointer user_data)
+{
+    NMRemoteConnection *remote         = NM_REMOTE_CONNECTION(source_object);
+    GetSecretsData *    data           = user_data;
+    gs_unref_variant GVariant *secrets = NULL;
+
+    secrets = nm_remote_connection_get_secrets_finish(remote, res, NULL);
+    if (secrets) {
+        gs_free_error GError *error = NULL;
+
+        if (!nm_connection_update_secrets(data->local, NULL, secrets, &error) && error) {
+            g_printerr(_("Error updating secrets for %s: %s\n"),
+                       data->setting_name,
+                       error->message);
+        }
+    }
+
+    g_main_loop_quit(data->loop);
+}
+
+/* Put secrets into local connection. */
+static void
+update_secrets_in_connection(NMRemoteConnection *remote, NMConnection *local)
+{
+    GetSecretsData data = {
+        0,
+    };
+    GType setting_type;
+    int   i;
+
+    data.local = local;
+    data.loop  = g_main_loop_new(NULL, FALSE);
+
+    for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++) {
+        setting_type = nm_meta_setting_infos[i].get_setting_gtype();
+        if (!nm_connection_get_setting(NM_CONNECTION(remote), setting_type))
+            continue;
+        if (!nm_meta_setting_info_editor_has_secrets(
+                nm_meta_setting_info_editor_find_by_gtype(setting_type)))
+            continue;
+        data.setting_name = nm_meta_setting_infos[i].setting_name;
+        nm_remote_connection_get_secrets_async(remote,
+                                               nm_meta_setting_infos[i].setting_name,
+                                               NULL,
+                                               got_secrets,
+                                               &data);
+        g_main_loop_run(data.loop);
+    }
+
+    g_main_loop_unref(data.loop);
+}
+
+static gboolean
+nmc_connection_profile_details(NMConnection *connection, NmCli *nmc)
+{
+    GError *    error = NULL;
+    GArray *    print_settings_array;
+    GPtrArray * prop_array = NULL;
+    guint       i;
+    char *      fields_str;
+    char *      fields_all    = NMC_FIELDS_SETTINGS_NAMES_ALL;
+    char *      fields_common = NMC_FIELDS_SETTINGS_NAMES_ALL;
+    const char *base_hdr      = _("Connection profile details");
+    gboolean    was_output    = FALSE;
+
+    if (!nmc->required_fields || g_ascii_strcasecmp(nmc->required_fields, "common") == 0)
+        fields_str = fields_common;
+    else if (!nmc->required_fields || g_ascii_strcasecmp(nmc->required_fields, "all") == 0)
+        fields_str = fields_all;
+    else
+        fields_str = nmc->required_fields;
+
+    print_settings_array =
+        parse_output_fields(fields_str,
+                            (const NMMetaAbstractInfo *const *) nm_meta_setting_infos_editor_p(),
+                            TRUE,
+                            &prop_array,
+                            &error);
+    if (error) {
+        g_string_printf(nmc->return_text, _("Error: 'connection show': %s"), error->message);
+        g_error_free(error);
+        nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+        return FALSE;
+    }
+    g_assert(print_settings_array);
+
+    /* Main header */
+    {
+        gs_free char *header_name          = NULL;
+        gs_free NmcOutputField *row        = NULL;
+        gs_unref_array GArray *out_indices = NULL;
+
+        header_name = construct_header_name(base_hdr, nm_connection_get_id(connection));
+        out_indices = parse_output_fields(
+            NMC_FIELDS_SETTINGS_NAMES_ALL,
+            (const NMMetaAbstractInfo *const *) nm_meta_setting_infos_editor_p(),
+            FALSE,
+            NULL,
+            NULL);
+
+        row = g_new0(NmcOutputField, _NM_META_SETTING_TYPE_NUM + 1);
+        for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++)
+            row[i].info = (const NMMetaAbstractInfo *) &nm_meta_setting_infos_editor[i];
+
+        print_required_fields(&nmc->nmc_config,
+                              &nmc->pager_data,
+                              NMC_OF_FLAG_MAIN_HEADER_ONLY,
+                              out_indices,
+                              header_name,
+                              0,
+                              row);
+    }
+
+    /* Loop through the required settings and print them. */
+    for (i = 0; i < print_settings_array->len; i++) {
+        NMSetting * setting;
+        int         section_idx = g_array_index(print_settings_array, int, i);
+        const char *prop_name   = (const char *) g_ptr_array_index(prop_array, i);
+
+        if (NM_IN_SET(nmc->nmc_config.print_output, NMC_PRINT_NORMAL, NMC_PRINT_PRETTY)
+            && !nmc->nmc_config.multiline_output && was_output)
+            g_print("\n"); /* Empty line */
+
+        was_output = FALSE;
+
+        setting = nm_connection_get_setting_by_name(
+            connection,
+            nm_meta_setting_infos_editor[section_idx].general->setting_name);
+        if (setting) {
+            setting_details(&nmc->nmc_config, setting, prop_name);
+            was_output = TRUE;
+        }
+    }
+
+    g_array_free(print_settings_array, TRUE);
+    if (prop_array)
+        g_ptr_array_free(prop_array, TRUE);
+
+    return TRUE;
+}
+
+NMMetaColor
+nmc_active_connection_state_to_color(NMActiveConnection *ac)
+{
+    NMActiveConnectionState state;
+
+    if (!ac)
+        return NM_META_COLOR_CONNECTION_UNKNOWN;
+
+    if (NM_FLAGS_HAS(nm_active_connection_get_state_flags(ac), NM_ACTIVATION_STATE_FLAG_EXTERNAL))
+        return NM_META_COLOR_CONNECTION_EXTERNAL;
+
+    state = nm_active_connection_get_state(ac);
+
+    if (state == NM_ACTIVE_CONNECTION_STATE_ACTIVATING)
+        return NM_META_COLOR_CONNECTION_ACTIVATING;
+    else if (state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED)
+        return NM_META_COLOR_CONNECTION_ACTIVATED;
+    else if (state > NM_ACTIVE_CONNECTION_STATE_ACTIVATED)
+        return NM_META_COLOR_CONNECTION_DISCONNECTING;
+    else
+        return NM_META_COLOR_CONNECTION_UNKNOWN;
+}
+
+static gboolean
+nmc_active_connection_details(NMActiveConnection *acon, NmCli *nmc)
+{
+    GError *    error = NULL;
+    GArray *    print_groups;
+    GPtrArray * group_fields = NULL;
+    int         i;
+    const char *fields_str = NULL;
+    const char *base_hdr   = _("Activate connection details");
+    gboolean    was_output = FALSE;
+
+    if (!nmc->required_fields || g_ascii_strcasecmp(nmc->required_fields, "common") == 0) {
+        /* pass */
+    } else if (!nmc->required_fields || g_ascii_strcasecmp(nmc->required_fields, "all") == 0) {
+        /* pass */
+    } else
+        fields_str = nmc->required_fields;
+
+    print_groups = parse_output_fields(
+        fields_str,
+        (const NMMetaAbstractInfo *const *) nmc_fields_con_active_details_groups,
+        TRUE,
+        &group_fields,
+        &error);
+    if (error) {
+        g_string_printf(nmc->return_text, _("Error: 'connection show': %s"), error->message);
+        g_error_free(error);
+        nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+        return FALSE;
+    }
+    g_assert(print_groups);
+
+    /* Main header */
+    {
+        gs_free char *header_name          = NULL;
+        gs_free NmcOutputField *row        = NULL;
+        gs_unref_array GArray *out_indices = NULL;
+
+        header_name = construct_header_name(base_hdr, nm_active_connection_get_uuid(acon));
+        out_indices = parse_output_fields(
+            NULL,
+            (const NMMetaAbstractInfo *const *) nmc_fields_con_active_details_groups,
+            FALSE,
+            NULL,
+            NULL);
+
+        row = g_new0(NmcOutputField, G_N_ELEMENTS(nmc_fields_con_active_details_groups) + 1);
+        for (i = 0; nmc_fields_con_active_details_groups[i]; i++)
+            row[i].info = (const NMMetaAbstractInfo *) nmc_fields_con_active_details_groups[i];
+
+        print_required_fields(&nmc->nmc_config,
+                              &nmc->pager_data,
+                              NMC_OF_FLAG_MAIN_HEADER_ONLY,
+                              out_indices,
+                              header_name,
+                              0,
+                              row);
+    }
+
+    /* Loop through the groups and print them. */
+    for (i = 0; i < print_groups->len; i++) {
+        int   group_idx = g_array_index(print_groups, int, i);
+        char *group_fld = (char *) g_ptr_array_index(group_fields, i);
+
+        if (NM_IN_SET(nmc->nmc_config.print_output, NMC_PRINT_NORMAL, NMC_PRINT_PRETTY)
+            && !nmc->nmc_config.multiline_output && was_output)
+            g_print("\n");
+
+        was_output = FALSE;
+
+        if (nmc_fields_con_active_details_groups[group_idx]->nested == metagen_con_active_general) {
+            gs_free char *f = NULL;
+
+            if (group_fld)
+                f = g_strdup_printf("GENERAL.%s", group_fld);
+
+            nmc_print(&nmc->nmc_config,
+                      (gpointer[]){acon, NULL},
+                      NULL,
+                      NULL,
+                      NMC_META_GENERIC_GROUP("GENERAL", metagen_con_active_general, N_("GROUP")),
+                      f,
+                      NULL);
+            was_output = TRUE;
+            continue;
+        }
+
+        /* IP4 */
+        if (g_ascii_strcasecmp(nmc_fields_con_active_details_groups[group_idx]->name,
+                               nmc_fields_con_active_details_groups[1]->name)
+            == 0) {
+            gboolean    b1   = FALSE;
+            NMIPConfig *cfg4 = nm_active_connection_get_ip4_config(acon);
+
+            b1         = print_ip_config(cfg4, AF_INET, &nmc->nmc_config, group_fld);
+            was_output = was_output || b1;
+        }
+
+        /* DHCP4 */
+        if (g_ascii_strcasecmp(nmc_fields_con_active_details_groups[group_idx]->name,
+                               nmc_fields_con_active_details_groups[2]->name)
+            == 0) {
+            gboolean      b1    = FALSE;
+            NMDhcpConfig *dhcp4 = nm_active_connection_get_dhcp4_config(acon);
+
+            b1         = print_dhcp_config(dhcp4, AF_INET, &nmc->nmc_config, group_fld);
+            was_output = was_output || b1;
+        }
+
+        /* IP6 */
+        if (g_ascii_strcasecmp(nmc_fields_con_active_details_groups[group_idx]->name,
+                               nmc_fields_con_active_details_groups[3]->name)
+            == 0) {
+            gboolean    b1   = FALSE;
+            NMIPConfig *cfg6 = nm_active_connection_get_ip6_config(acon);
+
+            b1         = print_ip_config(cfg6, AF_INET6, &nmc->nmc_config, group_fld);
+            was_output = was_output || b1;
+        }
+
+        /* DHCP6 */
+        if (g_ascii_strcasecmp(nmc_fields_con_active_details_groups[group_idx]->name,
+                               nmc_fields_con_active_details_groups[4]->name)
+            == 0) {
+            gboolean      b1    = FALSE;
+            NMDhcpConfig *dhcp6 = nm_active_connection_get_dhcp6_config(acon);
+
+            b1         = print_dhcp_config(dhcp6, AF_INET6, &nmc->nmc_config, group_fld);
+            was_output = was_output || b1;
+        }
+
+        if (nmc_fields_con_active_details_groups[group_idx]->nested == metagen_con_active_vpn) {
+            if (NM_IS_VPN_CONNECTION(acon)) {
+                nmc_print(&nmc->nmc_config,
+                          (gpointer[]){acon, NULL},
+                          NULL,
+                          NULL,
+                          NMC_META_GENERIC_GROUP("VPN", metagen_con_active_vpn, N_("NAME")),
+                          group_fld,
+                          NULL);
+                was_output = TRUE;
+            }
+            continue;
+        }
+    }
+
+    g_array_free(print_groups, TRUE);
+    if (group_fields)
+        g_ptr_array_free(group_fields, TRUE);
+
+    return TRUE;
+}
+
+static gboolean
+split_required_fields_for_con_show(const char *input,
+                                   char **     profile_flds,
+                                   char **     active_flds,
+                                   GError **   error)
+{
+    gs_free const char **fields = NULL;
+    const char *const *  iter;
+    nm_auto_free_gstring GString *str1          = NULL;
+    nm_auto_free_gstring GString *str2          = NULL;
+    gboolean                      group_profile = FALSE;
+    gboolean                      group_active  = FALSE;
+    gboolean                      do_free;
+
+    if (!input) {
+        *profile_flds = NULL;
+        *active_flds  = NULL;
+        return TRUE;
+    }
+
+    str1 = g_string_new(NULL);
+    str2 = g_string_new(NULL);
+
+    fields = nm_utils_strsplit_set_with_empty(input, ",");
+    for (iter = fields; iter && *iter; iter++) {
+        char *   s_mutable = (char *) (*iter);
+        char *   dot;
+        gboolean is_all;
+        gboolean is_common;
+        gboolean found;
+        int      i;
+
+        g_strstrip(s_mutable);
+        dot = strchr(s_mutable, '.');
+        if (dot)
+            *dot = '\0';
+
+        is_all    = !dot && g_ascii_strcasecmp(s_mutable, "all") == 0;
+        is_common = !dot && g_ascii_strcasecmp(s_mutable, "common") == 0;
+
+        found = FALSE;
+        for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++) {
+            if (is_all || is_common
+                || !g_ascii_strcasecmp(s_mutable, nm_meta_setting_infos[i].setting_name)) {
+                if (dot)
+                    *dot = '.';
+                g_string_append(str1, s_mutable);
+                g_string_append_c(str1, ',');
+                found = TRUE;
+                break;
+            }
+        }
+        if (found)
+            continue;
+
+        for (i = 0; nmc_fields_con_active_details_groups[i]; i++) {
+            if (is_all || is_common
+                || !g_ascii_strcasecmp(s_mutable, nmc_fields_con_active_details_groups[i]->name)) {
+                if (dot)
+                    *dot = '.';
+                g_string_append(str2, s_mutable);
+                g_string_append_c(str2, ',');
+                found = TRUE;
+                break;
+            }
+        }
+        if (!found) {
+            if (dot)
+                *dot = '.';
+            if (!g_ascii_strcasecmp(s_mutable, CON_SHOW_DETAIL_GROUP_PROFILE))
+                group_profile = TRUE;
+            else if (!g_ascii_strcasecmp(s_mutable, CON_SHOW_DETAIL_GROUP_ACTIVE))
+                group_active = TRUE;
+            else {
+                gs_free char *allowed1 = nm_meta_abstract_infos_get_names_str(
+                    (const NMMetaAbstractInfo *const *) nm_meta_setting_infos_editor_p(),
+                    NULL);
+                gs_free char *allowed2 = nm_meta_abstract_infos_get_names_str(
+                    (const NMMetaAbstractInfo *const *) nmc_fields_con_active_details_groups,
+                    NULL);
+
+                g_set_error(error,
+                            NMCLI_ERROR,
+                            0,
+                            _("invalid field '%s'; allowed fields: %s and %s, or %s,%s"),
+                            s_mutable,
+                            allowed1,
+                            allowed2,
+                            CON_SHOW_DETAIL_GROUP_PROFILE,
+                            CON_SHOW_DETAIL_GROUP_ACTIVE);
+                return FALSE;
+            }
+        }
+    }
+
+    /* Handle pseudo groups: profile, active */
+    if (group_profile) {
+        if (str1->len > 0) {
+            g_set_error(error,
+                        NMCLI_ERROR,
+                        0,
+                        _("'%s' has to be alone"),
+                        CON_SHOW_DETAIL_GROUP_PROFILE);
+            return FALSE;
+        }
+        g_string_assign(str1, "all,");
+    }
+    if (group_active) {
+        if (str2->len > 0) {
+            g_set_error(error,
+                        NMCLI_ERROR,
+                        0,
+                        _("'%s' has to be alone"),
+                        CON_SHOW_DETAIL_GROUP_ACTIVE);
+            return FALSE;
+        }
+        g_string_assign(str2, "all,");
+    }
+
+    if (str1->len > 0)
+        g_string_truncate(str1, str1->len - 1);
+    if (str2->len > 0)
+        g_string_truncate(str2, str2->len - 1);
+
+    do_free       = (str1->len == 0);
+    *profile_flds = g_string_free(g_steal_pointer(&str1), do_free);
+    do_free       = (str2->len == 0);
+    *active_flds  = g_string_free(g_steal_pointer(&str2), do_free);
+    return TRUE;
+}
+
+typedef enum {
+    NMC_SORT_ACTIVE     = 1,
+    NMC_SORT_ACTIVE_INV = -1,
+    NMC_SORT_NAME       = 2,
+    NMC_SORT_NAME_INV   = -2,
+    NMC_SORT_TYPE       = 3,
+    NMC_SORT_TYPE_INV   = -3,
+    NMC_SORT_PATH       = 4,
+    NMC_SORT_PATH_INV   = -4,
+} NmcSortOrder;
+
+typedef struct {
+    NmCli *       nmc;
+    const GArray *order;
+    gboolean      show_active_fields;
+} ConShowSortInfo;
+
+static int
+con_show_get_items_cmp(gconstpointer pa, gconstpointer pb, gpointer user_data)
+{
+    const ConShowSortInfo *      sort_info      = user_data;
+    const MetagenConShowRowData *row_data_a     = *((const MetagenConShowRowData *const *) pa);
+    const MetagenConShowRowData *row_data_b     = *((const MetagenConShowRowData *const *) pb);
+    NMConnection *               c_a            = row_data_a->connection;
+    NMConnection *               c_b            = row_data_b->connection;
+    NMActiveConnection *         ac_a           = row_data_a->primary_active;
+    NMActiveConnection *         ac_b           = row_data_b->primary_active;
+    NMActiveConnection *         ac_a_effective = sort_info->show_active_fields ? ac_a : NULL;
+    NMActiveConnection *         ac_b_effective = sort_info->show_active_fields ? ac_b : NULL;
+
+    /* first sort active-connections which are invisible, i.e. that have no connection */
+    if (!c_a && c_b)
+        return -1;
+    if (!c_b && c_a)
+        return 1;
+
+    /* we have two connections... */
+    if (c_a && c_b && c_a != c_b) {
+        const NmcSortOrder *  order_arr;
+        guint                 i, order_len;
+        NMMetaAccessorGetType get_type =
+            nmc_print_output_to_accessor_get_type(sort_info->nmc->nmc_config.print_output);
+
+        if (sort_info->order) {
+            order_arr = &g_array_index(sort_info->order, NmcSortOrder, 0);
+            order_len = sort_info->order->len;
+        } else {
+            static const NmcSortOrder def[] = {NMC_SORT_ACTIVE, NMC_SORT_NAME, NMC_SORT_PATH};
+
+            /* Note: the default order does not consider whether a column is shown.
+             *       That means, the selection of the output fields, does not affect the
+             *       order (although there could be an argument that it should). */
+            order_arr = def;
+            order_len = G_N_ELEMENTS(def);
+        }
+
+        for (i = 0; i < order_len; i++) {
+            NmcSortOrder item = order_arr[i];
+
+            switch (item) {
+            case NMC_SORT_ACTIVE:
+                NM_CMP_RETURN(nmc_active_connection_cmp(ac_b, ac_a));
+                break;
+            case NMC_SORT_ACTIVE_INV:
+                NM_CMP_RETURN(nmc_active_connection_cmp(ac_a, ac_b));
+                break;
+
+            case NMC_SORT_TYPE:
+                NM_CMP_DIRECT_STRCMP0(_con_show_fcn_get_type(c_a, ac_a_effective, get_type),
+                                      _con_show_fcn_get_type(c_b, ac_b_effective, get_type));
+                break;
+            case NMC_SORT_TYPE_INV:
+                NM_CMP_DIRECT_STRCMP0(_con_show_fcn_get_type(c_b, ac_b_effective, get_type),
+                                      _con_show_fcn_get_type(c_a, ac_a_effective, get_type));
+                break;
+
+            case NMC_SORT_NAME:
+                NM_CMP_RETURN(nm_utf8_collate0(_con_show_fcn_get_id(c_a, ac_a_effective),
+                                               _con_show_fcn_get_id(c_b, ac_b_effective)));
+                break;
+            case NMC_SORT_NAME_INV:
+                NM_CMP_RETURN(nm_utf8_collate0(_con_show_fcn_get_id(c_b, ac_b_effective),
+                                               _con_show_fcn_get_id(c_a, ac_a_effective)));
+                break;
+
+            case NMC_SORT_PATH:
+                NM_CMP_RETURN(nm_utils_dbus_path_cmp(nm_connection_get_path(c_a),
+                                                     nm_connection_get_path(c_b)));
+                break;
+
+            case NMC_SORT_PATH_INV:
+                NM_CMP_RETURN(nm_utils_dbus_path_cmp(nm_connection_get_path(c_b),
+                                                     nm_connection_get_path(c_a)));
+                break;
+
+            default:
+                nm_assert_not_reached();
+                break;
+            }
+        }
+
+        NM_CMP_DIRECT_STRCMP0(nm_connection_get_uuid(c_a), nm_connection_get_uuid(c_b));
+        NM_CMP_DIRECT_STRCMP0(nm_connection_get_path(c_a), nm_connection_get_path(c_b));
+    }
+
+    NM_CMP_DIRECT_STRCMP0(nm_object_get_path(NM_OBJECT(ac_a)), nm_object_get_path(NM_OBJECT(ac_b)));
+
+    g_return_val_if_reached(0);
+}
+
+static GPtrArray *
+con_show_get_items(NmCli *nmc, gboolean active_only, gboolean show_active_fields, GArray *order)
+{
+    gs_unref_hashtable GHashTable *row_hash = NULL;
+    GHashTableIter                 hiter;
+    GPtrArray *                    result;
+    const GPtrArray *              arr;
+    NMRemoteConnection *           c;
+    MetagenConShowRowData *        row_data;
+    guint                          i;
+    const ConShowSortInfo          sort_info = {
+        .nmc                = nmc,
+        .order              = order,
+        .show_active_fields = show_active_fields,
+    };
+
+    row_hash = g_hash_table_new(nm_direct_hash, NULL);
+
+    arr = nm_client_get_connections(nmc->client);
+    for (i = 0; i < arr->len; i++) {
+        /* Note: libnm will not expose connection that are invisible
+         * to the user but currently inactive.
+         *
+         * That differs from get-active-connection(). If an invisible connection
+         * is active, we can get its NMActiveConnection. We can even obtain
+         * the corresponding NMRemoteConnection (although, of course it has
+         * no visible settings).
+         *
+         * I think this inconsistency is a bug in libnm. Anyway, the result is,
+         * that we print invisible connections if they are active, but otherwise
+         * we exclude them. */
+        c = arr->pdata[i];
+        g_hash_table_insert(row_hash,
+                            c,
+                            _metagen_con_show_row_data_new_for_connection(c, show_active_fields));
+    }
+
+    arr = nm_client_get_active_connections(nmc->client);
+    for (i = 0; i < arr->len; i++) {
+        NMActiveConnection *ac = arr->pdata[i];
+
+        c = nm_active_connection_get_connection(ac);
+        if (!show_active_fields && !c) {
+            /* the active connection has no connection, and we don't show
+             * any active fields. Skip this row. */
+            continue;
+        }
+
+        row_data = c ? g_hash_table_lookup(row_hash, c) : NULL;
+
+        if (show_active_fields || !c) {
+            /* the active connection either has no connection (in which we create a
+             * connection-less row), or we are interested in showing each active
+             * connection in its own row. Add a row. */
+            if (row_data) {
+                /* we create a rowdata for this connection earlier. We drop it, because this
+                 * connection is tracked via the rowdata of the active connection. */
+                g_hash_table_remove(row_hash, c);
+                _metagen_con_show_row_data_destroy(row_data);
+            }
+            row_data =
+                _metagen_con_show_row_data_new_for_active_connection(c, ac, show_active_fields);
+            g_hash_table_insert(row_hash, ac, row_data);
+            continue;
+        }
+
+        /* we add the active connection to the row for the referenced
+         * connection. We need to group them this way, to print the proper
+         * color (activated or not) based on primary_active. */
+        if (!row_data) {
+            /* this is unexpected. The active connection references a connection that
+             * seemingly no longer exists. It's a bug in libnm. Add a row nonetheless. */
+            row_data = _metagen_con_show_row_data_new_for_connection(c, show_active_fields);
+            g_hash_table_insert(row_hash, c, row_data);
+        }
+        _metagen_con_show_row_data_add_active_connection(row_data, ac);
+    }
+
+    result = g_ptr_array_new_with_free_func(_metagen_con_show_row_data_destroy);
+
+    g_hash_table_iter_init(&hiter, row_hash);
+    while (g_hash_table_iter_next(&hiter, NULL, (gpointer *) &row_data)) {
+        if (active_only && !row_data->primary_active) {
+            /* We only print connections that are active. Skip this row. */
+            _metagen_con_show_row_data_destroy(row_data);
+            continue;
+        }
+        if (!show_active_fields) {
+            NMSettingConnection *s_con;
+
+            nm_assert(NM_IS_REMOTE_CONNECTION(row_data->connection));
+            s_con = nm_connection_get_setting_connection(row_data->connection);
+            if (!s_con || !nm_setting_connection_get_uuid(s_con)) {
+                /* we are in a mode, where we only print rows for connection.
+                 * For that we require that all rows are visible to the user,
+                 * meaning: the have a [connection] setting and a UUID.
+                 *
+                 * Otherwise, this connection is likely invisible to the user.
+                 * Skip it. */
+                _metagen_con_show_row_data_destroy(row_data);
+                continue;
+            }
+            _metagen_con_show_row_data_init_primary_active(row_data);
+        } else
+            nm_assert(!row_data->all_active);
+        g_ptr_array_add(result, row_data);
+    }
+
+    g_ptr_array_sort_with_data(result, con_show_get_items_cmp, (gpointer) &sort_info);
+    return result;
+}
+
+static GArray *
+parse_preferred_connection_order(const char *order, GError **error)
+{
+    gs_free const char **strv = NULL;
+    const char *const *  iter;
+    const char *         str;
+    GArray *             order_arr;
+    NmcSortOrder         val;
+    gboolean             inverse, unique;
+    guint                i;
+
+    strv = nm_utils_strsplit_set(order, ":");
+    if (!strv) {
+        g_set_error(error, NMCLI_ERROR, 0, _("incorrect string '%s' of '--order' option"), order);
+        return NULL;
+    }
+
+    order_arr = g_array_sized_new(FALSE, FALSE, sizeof(NmcSortOrder), 4);
+    for (iter = strv; iter && *iter; iter++) {
+        str     = *iter;
+        inverse = FALSE;
+        if (str[0] == '-')
+            inverse = TRUE;
+        if (str[0] == '+' || str[0] == '-')
+            str++;
+
+        if (matches(str, "active"))
+            val = inverse ? NMC_SORT_ACTIVE_INV : NMC_SORT_ACTIVE;
+        else if (matches(str, "name"))
+            val = inverse ? NMC_SORT_NAME_INV : NMC_SORT_NAME;
+        else if (matches(str, "type"))
+            val = inverse ? NMC_SORT_TYPE_INV : NMC_SORT_TYPE;
+        else if (matches(str, "path"))
+            val = inverse ? NMC_SORT_PATH_INV : NMC_SORT_PATH;
+        else {
+            g_array_unref(order_arr);
+            order_arr = NULL;
+            g_set_error(error, NMCLI_ERROR, 0, _("incorrect item '%s' in '--order' option"), *iter);
+            break;
+        }
+        /* Check for duplicates and ignore them. */
+        unique = TRUE;
+        for (i = 0; i < order_arr->len; i++) {
+            if (abs(g_array_index(order_arr, NmcSortOrder, i)) - abs(val) == 0) {
+                unique = FALSE;
+                break;
+            }
+        }
+
+        /* Value is ok and unique, add it to the array */
+        if (unique)
+            g_array_append_val(order_arr, val);
+    }
+
+    return order_arr;
+}
+
+static NMConnection *
+get_connection(NmCli *             nmc,
+               int *               argc,
+               const char *const **argv,
+               const char **       out_selector,
+               const char **       out_value,
+               GPtrArray **        out_result,
+               GError **           error)
+{
+    const GPtrArray *connections;
+    NMConnection *   connection = NULL;
+    const char *     selector   = NULL;
+
+    NM_SET_OUT(out_selector, NULL);
+    NM_SET_OUT(out_value, NULL);
+
+    if (*argc == 0) {
+        g_set_error_literal(error,
+                            NMCLI_ERROR,
+                            NMC_RESULT_ERROR_USER_INPUT,
+                            _("No connection specified"));
+        return NULL;
+    }
+
+    if (*argc == 1 && nmc->complete)
+        nmc_complete_strings(**argv, "id", "uuid", "path", "filename");
+
+    if (NM_IN_STRSET(**argv, "id", "uuid", "path", "filename")) {
+        if (*argc == 1) {
+            if (!nmc->complete) {
+                g_set_error(error,
+                            NMCLI_ERROR,
+                            NMC_RESULT_ERROR_USER_INPUT,
+                            _("%s argument is missing"),
+                            selector);
+                return NULL;
+            }
+        } else {
+            selector = **argv;
+            (*argv)++;
+            (*argc)--;
+        }
+    }
+
+    NM_SET_OUT(out_selector, selector);
+    NM_SET_OUT(out_value, **argv);
+
+    connections = nm_client_get_connections(nmc->client);
+    connection =
+        nmc_find_connection(connections, selector, **argv, out_result, *argc == 1 && nmc->complete);
+    if (!connection) {
+        g_set_error(error,
+                    NMCLI_ERROR,
+                    NMC_RESULT_ERROR_NOT_FOUND,
+                    _("unknown connection '%s'"),
+                    **argv);
+    }
+
+    next_arg(nmc, argc, argv, NULL);
+    return connection;
+}
+
+static void
+do_connections_show(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    gs_free_error GError *err          = NULL;
+    gs_free char *        profile_flds = NULL;
+    gs_free char *        active_flds  = NULL;
+    gboolean              active_only  = FALSE;
+    gs_unref_array GArray *order       = NULL;
+    guint                  i;
+    int                    option;
+
+    /* check connection show options [--active] [--order <order spec>] */
+    while ((option = next_arg(nmc, &argc, &argv, "--active", "--order", NULL)) > 0) {
+        switch (option) {
+        case 1: /* --active */
+            active_only = TRUE;
+            break;
+        case 2: /* --order */
+            argc--;
+            argv++;
+            if (!argc) {
+                g_set_error_literal(&err, NMCLI_ERROR, 0, _("'--order' argument is missing"));
+                goto finish;
+            }
+            order = parse_preferred_connection_order(*argv, &err);
+            if (err)
+                goto finish;
+            break;
+        default:
+            g_assert_not_reached();
+            break;
+        }
+    }
+
+    if (argc == 0) {
+        const char *      fields_str                          = NULL;
+        gs_unref_ptrarray GPtrArray *items                    = NULL;
+        gs_free NMMetaSelectionResultList *selection          = NULL;
+        gboolean                           show_active_fields = TRUE;
+
+        if (nmc->complete)
+            goto finish;
+
+        if (!nmc->required_fields || g_ascii_strcasecmp(nmc->required_fields, "common") == 0)
+            fields_str = NMC_FIELDS_CON_SHOW_COMMON;
+        else if (!nmc->required_fields || g_ascii_strcasecmp(nmc->required_fields, "all") == 0) {
+            /* pass */
+        } else
+            fields_str = nmc->required_fields;
+
+        /* determine whether the user wants to see any fields that are related to active-connections
+         * (e.g. the apath, the current state, or the device where the profile is active).
+         *
+         * If that's the case, then we will show one line for each active connection. In case
+         * a profile has multiple active connections, it will be listed multiple times.
+         * If that's not the case, we filter out these duplicate lines. */
+        selection = nm_meta_selection_create_parse_list(
+            (const NMMetaAbstractInfo *const *) metagen_con_show,
+            fields_str,
+            FALSE,
+            NULL);
+        if (selection && selection->num > 0) {
+            show_active_fields = FALSE;
+            for (i = 0; i < selection->num; i++) {
+                const NmcMetaGenericInfo *info =
+                    (const NmcMetaGenericInfo *) selection->items[i].info;
+
+                if (NM_IN_SET(info->info_type,
+                              NMC_GENERIC_INFO_TYPE_CON_SHOW_DEVICE,
+                              NMC_GENERIC_INFO_TYPE_CON_SHOW_STATE,
+                              NMC_GENERIC_INFO_TYPE_CON_SHOW_ACTIVE,
+                              NMC_GENERIC_INFO_TYPE_CON_SHOW_ACTIVE_PATH)) {
+                    show_active_fields = TRUE;
+                    break;
+                }
+            }
+        }
+
+        nm_cli_spawn_pager(&nmc->nmc_config, &nmc->pager_data);
+
+        items = con_show_get_items(nmc, active_only, show_active_fields, order);
+        g_ptr_array_add(items, NULL);
+        if (!nmc_print(&nmc->nmc_config,
+                       items->pdata,
+                       NULL,
+                       active_only ? _("NetworkManager active profiles")
+                                   : _("NetworkManager connection profiles"),
+                       (const NMMetaAbstractInfo *const *) metagen_con_show,
+                       fields_str,
+                       &err))
+            goto finish;
+    } else {
+        gboolean         new_line       = FALSE;
+        gboolean         without_fields = (nmc->required_fields == NULL);
+        const GPtrArray *active_cons    = nm_client_get_active_connections(nmc->client);
+
+        /* multiline mode is default for 'connection show <ID>' */
+        if (!nmc->mode_specified)
+            nmc->nmc_config_mutable.multiline_output = TRUE;
+
+        /* Split required fields into the settings and active ones. */
+        if (!split_required_fields_for_con_show(nmc->required_fields,
+                                                &profile_flds,
+                                                &active_flds,
+                                                &err))
+            goto finish;
+
+        nm_clear_g_free(&nmc->required_fields);
+
+        /* Before printing the connections check if we have a "--show-secret"
+         * option after the connection ids */
+        if (!nmc->nmc_config.show_secrets && !nmc->complete) {
+            int                argc_cp = argc;
+            const char *const *argv_cp = argv;
+
+            do {
+                if (NM_IN_STRSET(*argv_cp, "id", "uuid", "path", "filename", "apath")) {
+                    argc_cp--;
+                    argv_cp++;
+                }
+            } while (next_arg(nmc, &argc_cp, &argv_cp, NULL) != -1);
+        }
+
+        while (argc > 0) {
+            const GPtrArray *connections;
+            gboolean         res;
+            NMConnection *   con;
+            gs_unref_object NMActiveConnection *explicit_acon  = NULL;
+            const char *                        selector       = NULL;
+            gs_unref_ptrarray GPtrArray *found_cons            = NULL;
+            gboolean                     explicit_acon_handled = FALSE;
+            guint                        i_found_cons;
+
+            if (argc == 1 && nmc->complete)
+                nmc_complete_strings(*argv, "id", "uuid", "path", "filename", "apath");
+
+            if (NM_IN_STRSET(*argv, "id", "uuid", "path", "filename", "apath")) {
+                selector = *argv;
+                argc--;
+                argv++;
+                if (!argc) {
+                    g_string_printf(nmc->return_text,
+                                    _("Error: %s argument is missing."),
+                                    *(argv - 1));
+                    nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                    goto finish;
+                }
+            }
+
+            /* Try to find connection by id, uuid or path first */
+            connections = nm_client_get_connections(nmc->client);
+            con         = nmc_find_connection(connections,
+                                      selector,
+                                      *argv,
+                                      &found_cons,
+                                      argc == 1 && nmc->complete);
+            if (!con && NM_IN_STRSET(selector, NULL, "apath")) {
+                /* Try apath too */
+                explicit_acon = nmc_find_active_connection(active_cons,
+                                                           "apath",
+                                                           *argv,
+                                                           NULL,
+                                                           argc == 1 && nmc->complete);
+                if (explicit_acon) {
+                    if (!selector
+                        && !nm_streq0(*argv, nm_object_get_path(NM_OBJECT(explicit_acon)))) {
+                        /* we matched the apath based on the last component alone (note the full D-Bus path).
+                         * That is how nmc_find_active_connection() works, if you pass in a selector.
+                         * Reject it. */
+                        explicit_acon = NULL;
+                    }
+                    nm_g_object_ref(explicit_acon);
+                }
+            }
+
+            if (!con && !explicit_acon) {
+                g_string_printf(nmc->return_text,
+                                _("Error: %s - no such connection profile."),
+                                *argv);
+                nmc->return_value = NMC_RESULT_ERROR_NOT_FOUND;
+                goto finish;
+            }
+
+            /* Print connection details:
+             * Usually we have both static and active connection.
+             * But when a connection is private to a user, another user
+             * may see only the active connection.
+             */
+
+            if (nmc->complete) {
+                next_arg(nmc, &argc, &argv, NULL);
+                continue;
+            }
+
+            explicit_acon_handled = FALSE;
+            i_found_cons          = 0;
+            for (;;) {
+                gs_unref_ptrarray GPtrArray *found_acons = NULL;
+
+                if (explicit_acon) {
+                    if (explicit_acon_handled)
+                        break;
+                    explicit_acon_handled = TRUE;
+                    /* the user referenced an "apath". In this case, we can only have at most one connection
+                     * and one apath. */
+                    con = NM_CONNECTION(nm_active_connection_get_connection(explicit_acon));
+                } else {
+                    if (i_found_cons >= found_cons->len)
+                        break;
+                    con = found_cons->pdata[i_found_cons++];
+                    get_ac_for_connection(active_cons, con, &found_acons);
+                }
+
+                if (active_only && !explicit_acon && !found_acons) {
+                    /* this connection is not interesting, we only print active ones. */
+                    continue;
+                }
+
+                nm_assert(explicit_acon || con);
+
+                if (new_line)
+                    g_print("\n");
+                new_line = TRUE;
+
+                if (without_fields || profile_flds) {
+                    if (con) {
+                        nmc->required_fields = profile_flds;
+                        if (nmc->nmc_config.show_secrets)
+                            update_secrets_in_connection(NM_REMOTE_CONNECTION(con), con);
+                        res                  = nmc_connection_profile_details(con, nmc);
+                        nmc->required_fields = NULL;
+                        if (!res)
+                            goto finish;
+                    }
+                }
+
+                if (without_fields || active_flds) {
+                    guint l = explicit_acon ? 1 : (found_acons ? found_acons->len : 0);
+
+                    for (i = 0; i < l; i++) {
+                        NMActiveConnection *acon;
+
+                        if (i > 0) {
+                            /* if there are multiple active connections, separate them with newline.
+                             * that is a bit odd, because we already separate connections with newlines,
+                             * and commonly don't separate the connection from the first active connection. */
+                            g_print("\n");
+                        }
+
+                        if (explicit_acon)
+                            acon = explicit_acon;
+                        else
+                            acon = found_acons->pdata[i];
+
+                        nmc->required_fields = active_flds;
+                        res                  = nmc_active_connection_details(acon, nmc);
+                        nmc->required_fields = NULL;
+                        if (!res)
+                            goto finish;
+                    }
+                }
+            }
+
+            next_arg(nmc, &argc, &argv, NULL);
+        }
+    }
+
+finish:
+    if (err) {
+        g_string_printf(nmc->return_text, _("Error: %s."), err->message);
+        nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+    }
+}
+
+static NMActiveConnection *
+get_default_active_connection(NmCli *nmc, NMDevice **device)
+{
+    NMActiveConnection *default_ac         = NULL;
+    NMDevice *          non_default_device = NULL;
+    NMActiveConnection *non_default_ac     = NULL;
+    const GPtrArray *   connections;
+    guint               i;
+
+    g_return_val_if_fail(nmc, NULL);
+    g_return_val_if_fail(device, NULL);
+    g_return_val_if_fail(*device == NULL, NULL);
+
+    connections = nm_client_get_active_connections(nmc->client);
+    for (i = 0; i < connections->len; i++) {
+        NMActiveConnection *candidate = g_ptr_array_index(connections, i);
+        const GPtrArray *   devices;
+
+        devices = nm_active_connection_get_devices(candidate);
+        if (!devices->len)
+            continue;
+
+        if (nm_active_connection_get_default(candidate)) {
+            if (!default_ac) {
+                *device    = g_ptr_array_index(devices, 0);
+                default_ac = candidate;
+            }
+        } else {
+            if (!non_default_ac) {
+                non_default_device = g_ptr_array_index(devices, 0);
+                non_default_ac     = candidate;
+            }
+        }
+    }
+
+    /* Prefer the default connection if one exists, otherwise return the first
+     * non-default connection.
+     */
+    if (!default_ac && non_default_ac) {
+        default_ac = non_default_ac;
+        *device    = non_default_device;
+    }
+    return default_ac;
+}
+
+/* Find a device to activate the connection on.
+ * IN:  connection:  connection to activate
+ *      iface:       device interface name to use (optional)
+ *      ap:          access point to use (optional; valid just for 802-11-wireless)
+ *      nsp:         Network Service Provider to use (option; valid only for wimax)
+ * OUT: device:      found device
+ *      spec_object: specific_object path of NMAccessPoint
+ * RETURNS: TRUE when a device is found, FALSE otherwise.
+ */
+static gboolean
+find_device_for_connection(NmCli *       nmc,
+                           NMConnection *connection,
+                           const char *  iface,
+                           const char *  ap,
+                           const char *  nsp,
+                           NMDevice **   device,
+                           const char ** spec_object,
+                           GError **     error)
+{
+    NMSettingConnection *s_con;
+    const char *         con_type;
+    guint                i, j;
+
+    g_return_val_if_fail(nmc, FALSE);
+    g_return_val_if_fail(iface || ap || nsp, FALSE);
+    g_return_val_if_fail(device && *device == NULL, FALSE);
+    g_return_val_if_fail(spec_object && *spec_object == NULL, FALSE);
+    g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
+
+    s_con = nm_connection_get_setting_connection(connection);
+    g_assert(s_con);
+    con_type = nm_setting_connection_get_connection_type(s_con);
+
+    if (strcmp(con_type, NM_SETTING_VPN_SETTING_NAME) == 0) {
+        /* VPN connections */
+        NMActiveConnection *active = NULL;
+        if (iface) {
+            *device = nm_client_get_device_by_iface(nmc->client, iface);
+            if (*device)
+                active = nm_device_get_active_connection(*device);
+
+            if (!active) {
+                g_set_error(error, NMCLI_ERROR, 0, _("no active connection on device '%s'"), iface);
+                return FALSE;
+            }
+            *spec_object = nm_object_get_path(NM_OBJECT(active));
+            return TRUE;
+        } else {
+            active = get_default_active_connection(nmc, device);
+            if (!active) {
+                g_set_error_literal(error, NMCLI_ERROR, 0, _("no active connection or device"));
+                return FALSE;
+            }
+            *spec_object = nm_object_get_path(NM_OBJECT(active));
+            return TRUE;
+        }
+    } else {
+        /* Other connections */
+        NMDevice *       found_device = NULL;
+        const GPtrArray *devices      = nm_client_get_devices(nmc->client);
+
+        for (i = 0; i < devices->len && !found_device; i++) {
+            NMDevice *dev = g_ptr_array_index(devices, i);
+
+            if (iface) {
+                const char *dev_iface = nm_device_get_iface(dev);
+                if (!nm_streq0(dev_iface, iface))
+                    continue;
+
+                if (!nm_device_connection_compatible(dev, connection, error)) {
+                    g_prefix_error(error,
+                                   _("device '%s' not compatible with connection '%s': "),
+                                   iface,
+                                   nm_setting_connection_get_id(s_con));
+                    return FALSE;
+                }
+
+            } else {
+                if (!nm_device_connection_compatible(dev, connection, NULL))
+                    continue;
+            }
+
+            found_device = dev;
+            if (ap && nm_streq(con_type, NM_SETTING_WIRELESS_SETTING_NAME)
+                && NM_IS_DEVICE_WIFI(dev)) {
+                gs_free char *   bssid_up = g_ascii_strup(ap, -1);
+                const GPtrArray *aps      = nm_device_wifi_get_access_points(NM_DEVICE_WIFI(dev));
+                found_device =
+                    NULL; /* Mark as not found; set to the device again later, only if AP matches */
+
+                for (j = 0; j < aps->len; j++) {
+                    NMAccessPoint *candidate_ap    = g_ptr_array_index(aps, j);
+                    const char *   candidate_bssid = nm_access_point_get_bssid(candidate_ap);
+
+                    if (nm_streq0(bssid_up, candidate_bssid)) {
+                        found_device = dev;
+                        *spec_object = nm_object_get_path(NM_OBJECT(candidate_ap));
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (!found_device) {
+            if (iface) {
+                g_set_error(error,
+                            NMCLI_ERROR,
+                            0,
+                            _("device '%s' not compatible with connection '%s'"),
+                            iface,
+                            nm_setting_connection_get_id(s_con));
+            } else {
+                g_set_error(error,
+                            NMCLI_ERROR,
+                            0,
+                            _("no device found for connection '%s'"),
+                            nm_setting_connection_get_id(s_con));
+            }
+            return FALSE;
+        }
+
+        *device = found_device;
+        return TRUE;
+    }
+}
+
+typedef struct {
+    NmCli *             nmc;
+    NMDevice *          device;
+    NMActiveConnection *active;
+} ActivateConnectionInfo;
+
+static void
+active_connection_hint(GString *return_text, NMActiveConnection *active, NMDevice *device)
+{
+    NMRemoteConnection * connection;
+    nm_auto_free_gstring GString *hint = NULL;
+    const GPtrArray *             devices;
+    guint                         i;
+
+    if (!active)
+        return;
+
+    if (!nm_streq(NM_CONFIG_DEFAULT_LOGGING_BACKEND, "journal"))
+        return;
+
+    connection = nm_active_connection_get_connection(active);
+    g_return_if_fail(connection);
+
+    hint = g_string_new("journalctl -xe ");
+    g_string_append_printf(hint,
+                           "NM_CONNECTION=%s",
+                           nm_connection_get_uuid(NM_CONNECTION(connection)));
+
+    if (device)
+        g_string_append_printf(hint, " + NM_DEVICE=%s", nm_device_get_iface(device));
+    else {
+        devices = nm_active_connection_get_devices(active);
+        for (i = 0; i < devices->len; i++) {
+            g_string_append_printf(hint,
+                                   " + NM_DEVICE=%s",
+                                   nm_device_get_iface(NM_DEVICE(g_ptr_array_index(devices, i))));
+        }
+    }
+
+    g_string_append(return_text, "\n");
+    g_string_append_printf(return_text, _("Hint: use '%s' to get more details."), hint->str);
+}
+
+static void activate_connection_info_finish(ActivateConnectionInfo *info);
+
+static void
+check_activated(ActivateConnectionInfo *info)
+{
+    NMActiveConnectionState ac_state;
+    NmCli *                 nmc    = info->nmc;
+    const char *            reason = NULL;
+
+    ac_state = nmc_activation_get_effective_state(info->active, info->device, &reason);
+    switch (ac_state) {
+    case NM_ACTIVE_CONNECTION_STATE_ACTIVATED:
+        if (nmc->nmc_config.print_output == NMC_PRINT_PRETTY)
+            nmc_terminal_erase_line();
+        if (reason) {
+            g_print(_("Connection successfully activated (%s) (D-Bus active path: %s)\n"),
+                    reason,
+                    nm_object_get_path(NM_OBJECT(info->active)));
+        } else {
+            g_print(_("Connection successfully activated (D-Bus active path: %s)\n"),
+                    nm_object_get_path(NM_OBJECT(info->active)));
+        }
+        activate_connection_info_finish(info);
+        break;
+    case NM_ACTIVE_CONNECTION_STATE_DEACTIVATED:
+        nm_assert(reason);
+        g_string_printf(nmc->return_text, _("Error: Connection activation failed: %s"), reason);
+        active_connection_hint(nmc->return_text, info->active, info->device);
+        nmc->return_value = NMC_RESULT_ERROR_CON_ACTIVATION;
+        activate_connection_info_finish(info);
+        break;
+    case NM_ACTIVE_CONNECTION_STATE_ACTIVATING:
+        if (nmc->secret_agent) {
+            NMRemoteConnection *connection = nm_active_connection_get_connection(info->active);
+
+            nm_secret_agent_simple_enable(nmc->secret_agent,
+                                          nm_connection_get_path(NM_CONNECTION(connection)));
+        }
+        break;
+    default:
+        break;
+    }
+}
+
+static void
+device_state_cb(NMDevice *device, GParamSpec *pspec, ActivateConnectionInfo *info)
+{
+    check_activated(info);
+}
+
+static void
+active_connection_state_cb(NMActiveConnection *          active,
+                           NMActiveConnectionState       state,
+                           NMActiveConnectionStateReason reason,
+                           ActivateConnectionInfo *      info)
+{
+    check_activated(info);
+}
+
+static void
+set_nmc_error_timeout(NmCli *nmc)
+{
+    g_string_printf(nmc->return_text, _("Error: Timeout expired (%d seconds)"), nmc->timeout);
+    nmc->return_value = NMC_RESULT_ERROR_TIMEOUT_EXPIRED;
+}
+
+static gboolean
+activate_connection_timeout_cb(gpointer user_data)
+{
+    ActivateConnectionInfo *info = user_data;
+
+    /* Time expired -> exit nmcli */
+    set_nmc_error_timeout(info->nmc);
+    activate_connection_info_finish(info);
+    return FALSE;
+}
+
+static gboolean
+progress_cb(gpointer user_data)
+{
+    const char *str = (const char *) user_data;
+
+    nmc_terminal_show_progress(str);
+
+    return TRUE;
+}
+
+static gboolean
+progress_active_connection_cb(gpointer user_data)
+{
+    NMActiveConnection *    active = user_data;
+    const char *            str;
+    NMDevice *              device;
+    NMActiveConnectionState ac_state;
+    const GPtrArray *       ac_devs;
+
+    ac_state = nm_active_connection_get_state(active);
+
+    if (ac_state == NM_ACTIVE_CONNECTION_STATE_ACTIVATING) {
+        /* If the connection is activating, the device state
+         * is more interesting. */
+        ac_devs = nm_active_connection_get_devices(active);
+        device  = ac_devs->len > 0 ? g_ptr_array_index(ac_devs, 0) : NULL;
+    } else {
+        device = NULL;
+    }
+
+    str = device ? gettext(nmc_device_state_to_string_with_external(device))
+                 : active_connection_state_to_string(ac_state);
+
+    nmc_terminal_show_progress(str);
+
+    return TRUE;
+}
+
+static void
+activate_connection_info_finish(ActivateConnectionInfo *info)
+{
+    if (info->device) {
+        g_signal_handlers_disconnect_by_func(info->device, G_CALLBACK(device_state_cb), info);
+        g_object_unref(info->device);
+    }
+
+    if (info->active) {
+        g_signal_handlers_disconnect_by_func(info->active,
+                                             G_CALLBACK(active_connection_state_cb),
+                                             info);
+        g_object_unref(info->active);
+    }
+
+    g_free(info);
+    quit();
+}
+
+static void
+activate_connection_cb(GObject *client, GAsyncResult *result, gpointer user_data)
+{
+    ActivateConnectionInfo *info   = (ActivateConnectionInfo *) user_data;
+    NmCli *                 nmc    = info->nmc;
+    NMDevice *              device = info->device;
+    NMActiveConnection *    active;
+    NMActiveConnectionState state;
+    const GPtrArray *       ac_devs;
+    GError *                error = NULL;
+
+    info->active = active = nm_client_activate_connection_finish(NM_CLIENT(client), result, &error);
+
+    if (error) {
+        g_string_printf(nmc->return_text,
+                        _("Error: Connection activation failed: %s"),
+                        error->message);
+        g_error_free(error);
+        active_connection_hint(nmc->return_text, info->active, info->device);
+        nmc->return_value = NMC_RESULT_ERROR_CON_ACTIVATION;
+        activate_connection_info_finish(info);
+    } else {
+        state = nm_active_connection_get_state(active);
+        if (!device && !nm_active_connection_get_vpn(active)) {
+            /* device could be NULL for virtual devices. Fill it here. */
+            ac_devs = nm_active_connection_get_devices(active);
+            device  = ac_devs->len > 0 ? g_ptr_array_index(ac_devs, 0) : NULL;
+            if (device)
+                info->device = g_object_ref(device);
+        }
+
+        if (nmc->nowait_flag || state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED) {
+            /* User doesn't want to wait or already activated */
+            if (state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED) {
+                if (nmc->nmc_config.print_output == NMC_PRINT_PRETTY)
+                    nmc_terminal_erase_line();
+                g_print(_("Connection successfully activated (D-Bus active path: %s)\n"),
+                        nm_object_get_path(NM_OBJECT(active)));
+            }
+            activate_connection_info_finish(info);
+        } else {
+            /* Monitor the active connection and device (if available) states */
+            g_signal_connect(active, "state-changed", G_CALLBACK(active_connection_state_cb), info);
+            if (device)
+                g_signal_connect(device,
+                                 "notify::" NM_DEVICE_STATE,
+                                 G_CALLBACK(device_state_cb),
+                                 info);
+            /* Both active_connection_state_cb () and device_state_cb () will just
+             * call check_activated (info). So, just call it once directly after
+             * connecting on both the signals of the objects and skip the call to
+             * the callbacks.
+             */
+            check_activated(info);
+
+            /* Start progress indication showing VPN states */
+            if (nmc->nmc_config.print_output == NMC_PRINT_PRETTY) {
+                if (progress_id)
+                    g_source_remove(progress_id);
+                progress_id = g_timeout_add(120, progress_active_connection_cb, active);
+            }
+
+            /* Start timer not to loop forever when signals are not emitted */
+            g_timeout_add_seconds(nmc->timeout, activate_connection_timeout_cb, info);
+        }
+    }
+}
+
+static gboolean
+nmc_activate_connection(NmCli *             nmc,
+                        NMConnection *      connection,
+                        const char *        ifname,
+                        const char *        ap,
+                        const char *        nsp,
+                        const char *        pwds,
+                        GAsyncReadyCallback callback,
+                        GError **           error)
+{
+    ActivateConnectionInfo *info;
+
+    GHashTable *pwds_hash;
+    NMDevice *  device      = NULL;
+    const char *spec_object = NULL;
+    gboolean    device_found;
+
+    g_return_val_if_fail(nmc, FALSE);
+    g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
+
+    if (connection && (ifname || ap || nsp)) {
+        gs_free_error GError *local = NULL;
+
+        device_found = find_device_for_connection(nmc,
+                                                  connection,
+                                                  ifname,
+                                                  ap,
+                                                  nsp,
+                                                  &device,
+                                                  &spec_object,
+                                                  &local);
+
+        /* Virtual connection may not have their interfaces created yet */
+        if (!device_found && !nm_connection_is_virtual(connection)) {
+            g_set_error(error, NMCLI_ERROR, NMC_RESULT_ERROR_CON_ACTIVATION, "%s", local->message);
+            return FALSE;
+        }
+    } else if (ifname) {
+        device = nm_client_get_device_by_iface(nmc->client, ifname);
+        if (!device) {
+            g_set_error(error,
+                        NMCLI_ERROR,
+                        NMC_RESULT_ERROR_NOT_FOUND,
+                        _("unknown device '%s'."),
+                        ifname);
+            return FALSE;
+        }
+    } else if (!connection) {
+        g_set_error_literal(error,
+                            NMCLI_ERROR,
+                            NMC_RESULT_ERROR_NOT_FOUND,
+                            _("neither a valid connection nor device given"));
+        return FALSE;
+    }
+
+    /* Parse passwords given in passwords file */
+    {
+        gs_free_error GError *local = NULL;
+        gssize                error_line;
+
+        pwds_hash = nmc_utils_read_passwd_file(pwds, &error_line, &local);
+        if (!pwds_hash) {
+            if (error_line >= 0) {
+                g_set_error(error,
+                            NMCLI_ERROR,
+                            NMC_RESULT_ERROR_USER_INPUT,
+                            _("invalid passwd-file '%s' at line %zd: %s"),
+                            pwds,
+                            error_line,
+                            local->message);
+            } else {
+                g_set_error(error,
+                            NMCLI_ERROR,
+                            NMC_RESULT_ERROR_USER_INPUT,
+                            _("invalid passwd-file '%s': %s"),
+                            pwds,
+                            local->message);
+            }
+            return FALSE;
+        }
+    }
+
+    if (nmc->pwds_hash)
+        g_hash_table_destroy(nmc->pwds_hash);
+    nmc->pwds_hash = pwds_hash;
+
+    nmc->secret_agent = nm_secret_agent_simple_new("nmcli-connect");
+    if (nmc->secret_agent) {
+        g_signal_connect(nmc->secret_agent,
+                         NM_SECRET_AGENT_SIMPLE_REQUEST_SECRETS,
+                         G_CALLBACK(nmc_secrets_requested),
+                         nmc);
+    }
+
+    info      = g_malloc0(sizeof(ActivateConnectionInfo));
+    info->nmc = nmc;
+    if (device)
+        info->device = g_object_ref(device);
+
+    nm_client_activate_connection_async(nmc->client,
+                                        connection,
+                                        device,
+                                        spec_object,
+                                        NULL,
+                                        callback,
+                                        info);
+    return TRUE;
+}
+
+static void
+do_connection_up(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    NMConnection *connection      = NULL;
+    const char *  ifname          = NULL;
+    const char *  ap              = NULL;
+    const char *  nsp             = NULL;
+    const char *  pwds            = NULL;
+    gs_free_error GError *error   = NULL;
+    gs_strfreev char **   arg_arr = NULL;
+    int                   arg_num;
+    const char *const **  argv_ptr;
+    int *                 argc_ptr;
+
+    /*
+     * Set default timeout for connection activation.
+     * Activation can take quite a long time, use 90 seconds.
+     */
+    if (nmc->timeout == -1)
+        nmc->timeout = 90;
+
+    next_arg(nmc, &argc, &argv, NULL);
+    argv_ptr = &argv;
+    argc_ptr = &argc;
+
+    if (argc == 0 && nmc->ask) {
+        gs_free char *line = NULL;
+
+        /* nmc_do_cmd() should not call this with argc=0. */
+        g_assert(!nmc->complete);
+
+        line = nmc_readline(&nmc->nmc_config, PROMPT_CONNECTION);
+        nmc_string_to_arg_array(line, NULL, TRUE, &arg_arr, &arg_num);
+        argv_ptr = (const char *const **) &arg_arr;
+        argc_ptr = &arg_num;
+    }
+
+    if (argc > 0 && strcmp(*argv, "ifname") != 0) {
+        connection = get_connection(nmc, argc_ptr, argv_ptr, NULL, NULL, NULL, &error);
+        if (!connection) {
+            g_string_printf(nmc->return_text, _("Error: %s."), error->message);
+            nmc->return_value = error->code;
+            return;
+        }
+    }
+
+    while (argc > 0) {
+        if (argc == 1 && nmc->complete)
+            nmc_complete_strings(*argv, "ifname", "ap", "passwd-file");
+
+        if (strcmp(*argv, "ifname") == 0) {
+            argc--;
+            argv++;
+            if (!argc) {
+                g_string_printf(nmc->return_text, _("Error: %s argument is missing."), *(argv - 1));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return;
+            }
+
+            ifname = *argv;
+            if (argc == 1 && nmc->complete)
+                nmc_complete_device(nmc->client, ifname, ap != NULL);
+        } else if (strcmp(*argv, "ap") == 0) {
+            argc--;
+            argv++;
+            if (!argc) {
+                g_string_printf(nmc->return_text, _("Error: %s argument is missing."), *(argv - 1));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return;
+            }
+
+            ap = *argv;
+            if (argc == 1 && nmc->complete)
+                nmc_complete_bssid(nmc->client, ifname, ap);
+        } else if (strcmp(*argv, "passwd-file") == 0) {
+            argc--;
+            argv++;
+            if (!argc) {
+                g_string_printf(nmc->return_text, _("Error: %s argument is missing."), *(argv - 1));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return;
+            }
+
+            if (argc == 1 && nmc->complete)
+                nmc->return_value = NMC_RESULT_COMPLETE_FILE;
+
+            pwds = *argv;
+        } else if (!nmc->complete) {
+            g_string_printf(nmc->return_text, _("Error: invalid extra argument '%s'."), *argv);
+            nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+            return;
+        }
+
+        next_arg(nmc, &argc, &argv, NULL);
+    }
+
+    if (nmc->complete)
+        return;
+
+    /* Use nowait_flag instead of should_wait because exiting has to be postponed till
+     * active_connection_state_cb() is called. That gives NM time to check our permissions
+     * and we can follow activation progress.
+     */
+    nmc->nowait_flag = (nmc->timeout == 0);
+    nmc->should_wait++;
+
+    if (!nmc_activate_connection(nmc,
+                                 connection,
+                                 ifname,
+                                 ap,
+                                 nsp,
+                                 pwds,
+                                 activate_connection_cb,
+                                 &error)) {
+        g_string_printf(nmc->return_text, _("Error: %s."), error->message);
+        nmc->should_wait--;
+        nmc->return_value = error->code;
+        return;
+    }
+
+    /* Start progress indication */
+    if (nmc->nmc_config.print_output == NMC_PRINT_PRETTY)
+        progress_id = g_timeout_add(120, progress_cb, _("preparing"));
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    NmCli *nmc;
+    /* a list of object that is relevant for the callback. The object
+     * type differs, and depends on the type of callback. */
+    GPtrArray *   obj_list;
+    guint         timeout_id;
+    GCancellable *cancellable;
+} ConnectionCbInfo;
+
+static void
+connection_removed_cb(NMClient *client, NMConnection *connection, ConnectionCbInfo *info);
+
+static void down_active_connection_state_cb(NMActiveConnection *active,
+                                            GParamSpec *        pspec,
+                                            ConnectionCbInfo *  info);
+
+static void
+connection_cb_info_obj_list_destroy(ConnectionCbInfo *info, gpointer obj)
+{
+    nm_assert(info);
+    nm_assert(info->obj_list);
+    nm_assert(G_IS_OBJECT(obj));
+
+    g_signal_handlers_disconnect_by_func(obj, down_active_connection_state_cb, info);
+    g_object_unref(obj);
+}
+
+static gssize
+connection_cb_info_obj_list_idx(ConnectionCbInfo *info, gpointer obj)
+{
+    guint i;
+
+    nm_assert(info);
+    nm_assert(info->obj_list);
+    nm_assert(G_IS_OBJECT(obj));
+
+    for (i = 0; i < info->obj_list->len; i++) {
+        if (info->obj_list->pdata[i] == obj)
+            return i;
+    }
+    return -1;
+}
+
+static gpointer
+connection_cb_info_obj_list_has(ConnectionCbInfo *info, gpointer obj)
+{
+    gssize idx;
+
+    idx = connection_cb_info_obj_list_idx(info, obj);
+    if (idx >= 0)
+        return info->obj_list->pdata[idx];
+    return NULL;
+}
+
+static gpointer
+connection_cb_info_obj_list_steal(ConnectionCbInfo *info, gpointer obj)
+{
+    gssize idx;
+
+    idx = connection_cb_info_obj_list_idx(info, obj);
+    if (idx >= 0) {
+        g_ptr_array_remove_index(info->obj_list, idx);
+        return obj;
+    }
+    return NULL;
+}
+
+static void
+connection_cb_info_finish(ConnectionCbInfo *info, gpointer obj)
+{
+    if (obj) {
+        obj = connection_cb_info_obj_list_steal(info, obj);
+        if (obj)
+            connection_cb_info_obj_list_destroy(info, obj);
+    } else {
+        while (info->obj_list->len > 0) {
+            obj = info->obj_list->pdata[info->obj_list->len - 1];
+            g_ptr_array_remove_index(info->obj_list, info->obj_list->len - 1);
+            connection_cb_info_obj_list_destroy(info, obj);
+        }
+    }
+
+    if (info->obj_list->len > 0)
+        return;
+
+    nm_clear_g_source(&info->timeout_id);
+    nm_clear_g_cancellable(&info->cancellable);
+    g_ptr_array_free(info->obj_list, TRUE);
+
+    g_signal_handlers_disconnect_by_func(info->nmc->client, connection_removed_cb, info);
+
+    g_slice_free(ConnectionCbInfo, info);
+
+    quit();
+}
+
+/*****************************************************************************/
+
+static void
+connection_removed_cb(NMClient *client, NMConnection *connection, ConnectionCbInfo *info)
+{
+    if (!connection_cb_info_obj_list_has(info, connection))
+        return;
+    g_print(_("Connection '%s' (%s) successfully deleted.\n"),
+            nm_connection_get_id(connection),
+            nm_connection_get_uuid(connection));
+    connection_cb_info_finish(info, connection);
+}
+
+static void
+down_active_connection_state_cb(NMActiveConnection *active,
+                                GParamSpec *        pspec,
+                                ConnectionCbInfo *  info)
+{
+    if (nm_active_connection_get_state(active) < NM_ACTIVE_CONNECTION_STATE_DEACTIVATED)
+        return;
+
+    if (info->nmc->nmc_config.print_output == NMC_PRINT_PRETTY)
+        nmc_terminal_erase_line();
+    g_print(_("Connection '%s' successfully deactivated (D-Bus active path: %s)\n"),
+            nm_active_connection_get_id(active),
+            nm_object_get_path(NM_OBJECT(active)));
+
+    g_signal_handlers_disconnect_by_func(G_OBJECT(active), down_active_connection_state_cb, info);
+    connection_cb_info_finish(info, active);
+}
+
+static gboolean
+connection_op_timeout_cb(gpointer user_data)
+{
+    ConnectionCbInfo *info = user_data;
+
+    set_nmc_error_timeout(info->nmc);
+    connection_cb_info_finish(info, NULL);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+do_connection_down(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    NMActiveConnection *active;
+    ConnectionCbInfo *  info = NULL;
+    const GPtrArray *   active_cons;
+    gs_strfreev char ** arg_arr = NULL;
+    const char *const * arg_ptr;
+    int                 arg_num;
+    guint               i;
+    gs_unref_ptrarray GPtrArray *found_active_cons = NULL;
+
+    if (nmc->timeout == -1)
+        nmc->timeout = 10;
+
+    next_arg(nmc, &argc, &argv, NULL);
+    arg_ptr = argv;
+    arg_num = argc;
+
+    if (argc == 0) {
+        /* nmc_do_cmd() should not call this with argc=0. */
+        g_assert(!nmc->complete);
+
+        if (nmc->ask) {
+            gs_free char *line = NULL;
+
+            line = nmc_readline(&nmc->nmc_config, PROMPT_ACTIVE_CONNECTIONS);
+            nmc_string_to_arg_array(line, NULL, TRUE, &arg_arr, &arg_num);
+            arg_ptr = (const char *const *) arg_arr;
+        }
+        if (arg_num == 0) {
+            g_string_printf(nmc->return_text, _("Error: No connection specified."));
+            nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+            return;
+        }
+    }
+
+    /* Get active connections */
+    active_cons = nm_client_get_active_connections(nmc->client);
+    while (arg_num > 0) {
+        const char *selector = NULL;
+
+        if (arg_num == 1 && nmc->complete)
+            nmc_complete_strings(*arg_ptr, "id", "uuid", "path", "filename", "apath");
+
+        if (NM_IN_STRSET(*arg_ptr, "id", "uuid", "path", "filename", "apath")) {
+            selector = *arg_ptr;
+            arg_num--;
+            arg_ptr++;
+            if (!arg_num) {
+                g_string_printf(nmc->return_text, _("Error: %s argument is missing."), selector);
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return;
+            }
+        }
+
+        active = nmc_find_active_connection(active_cons,
+                                            selector,
+                                            *arg_ptr,
+                                            &found_active_cons,
+                                            arg_num == 1 && nmc->complete);
+        if (!active) {
+            if (!nmc->complete)
+                g_printerr(_("Error: '%s' is not an active connection.\n"), *arg_ptr);
+            g_string_printf(nmc->return_text, _("Error: not all active connections found."));
+            nmc->return_value = NMC_RESULT_ERROR_NOT_FOUND;
+        }
+
+        next_arg(nmc->ask ? NULL : nmc, &arg_num, &arg_ptr, NULL);
+    }
+
+    if (!found_active_cons) {
+        g_string_printf(nmc->return_text, _("Error: no active connection provided."));
+        nmc->return_value = NMC_RESULT_ERROR_NOT_FOUND;
+        return;
+    }
+    nm_assert(found_active_cons->len > 0);
+
+    if (nmc->complete)
+        return;
+
+    if (nmc->timeout > 0) {
+        nmc->should_wait++;
+
+        info           = g_slice_new0(ConnectionCbInfo);
+        info->nmc      = nmc;
+        info->obj_list = g_ptr_array_sized_new(found_active_cons->len);
+        for (i = 0; i < found_active_cons->len; i++) {
+            active = found_active_cons->pdata[i];
+            g_ptr_array_add(info->obj_list, g_object_ref(active));
+            g_signal_connect(active,
+                             "notify::" NM_ACTIVE_CONNECTION_STATE,
+                             G_CALLBACK(down_active_connection_state_cb),
+                             info);
+        }
+        info->timeout_id = g_timeout_add_seconds(nmc->timeout, connection_op_timeout_cb, info);
+    }
+
+    for (i = 0; i < found_active_cons->len; i++) {
+        GError *error = NULL;
+
+        active = found_active_cons->pdata[i];
+
+        if (!nm_client_deactivate_connection(nmc->client, active, NULL, &error)) {
+            g_print(_("Connection '%s' deactivation failed: %s\n"),
+                    nm_active_connection_get_id(active),
+                    error->message);
+            g_clear_error(&error);
+
+            if (info) {
+                /* coverity thinks that info might be freed already while we still iterate
+                 * the loop. But it cannot, because connection_cb_info_finish() only does some
+                 * kind of ref-counting that ensures info stays alive long enough. */
+
+                /* coverity[pass_freed_arg] */
+                g_signal_handlers_disconnect_by_func(active, down_active_connection_state_cb, info);
+
+                connection_cb_info_finish(info, active);
+            }
+        }
+    }
+}
+
+/*****************************************************************************/
+
+/*
+ * Return the most appropriate name for the connection of a type 'name' possibly with given 'slave_type'
+ * if exists, else return the 'name'. The returned string must not be freed.
+ */
+static const char *
+get_name_alias_toplevel(const char *name, const char *slave_type)
+{
+    const NMMetaSettingInfoEditor *setting_info;
+
+    if (slave_type) {
+        const char *slave_name;
+
+        if (nm_meta_setting_info_valid_parts_for_slave_type(slave_type, &slave_name))
+            return slave_name ?: name;
+        return name;
+    }
+
+    setting_info = nm_meta_setting_info_editor_find_by_name(name, FALSE);
+    if (setting_info)
+        return setting_info->alias ?: setting_info->general->setting_name;
+
+    return name;
+}
+
+/*
+ * Construct a string with names and aliases from the arrays formatted as:
+ * "name (alias), name, name (alias), name, name"
+ *
+ * Returns: string; the caller is responsible for freeing it.
+ */
+static char *
+get_valid_options_string(const NMMetaSettingValidPartItem *const *array,
+                         const NMMetaSettingValidPartItem *const *array_slv)
+{
+    const NMMetaSettingValidPartItem *const *iter = array;
+    GString *                                str;
+    int                                      i;
+
+    str = g_string_sized_new(150);
+
+    for (i = 0; i < 2; i++, iter = array_slv) {
+        for (; iter && *iter; iter++) {
+            const NMMetaSettingInfoEditor *setting_info = (*iter)->setting_info;
+
+            if (str->len)
+                g_string_append(str, ", ");
+            if (setting_info->alias)
+                g_string_append_printf(str,
+                                       "%s (%s)",
+                                       setting_info->general->setting_name,
+                                       setting_info->alias);
+            else
+                g_string_append(str, setting_info->general->setting_name);
+        }
+    }
+    return g_string_free(str, FALSE);
+}
+
+static char *
+get_valid_options_string_toplevel(void)
+{
+    GString *str;
+    int      i;
+
+    str = g_string_sized_new(150);
+    for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++) {
+        const NMMetaSettingInfoEditor *setting_info = &nm_meta_setting_infos_editor[i];
+
+        if (!setting_info->valid_parts)
+            continue;
+
+        if (str->len)
+            g_string_append(str, ", ");
+        if (setting_info->alias)
+            g_string_append_printf(str,
+                                   "%s (%s)",
+                                   setting_info->general->setting_name,
+                                   setting_info->alias);
+        else
+            g_string_append(str, setting_info->general->setting_name);
+    }
+
+    if (str->len)
+        g_string_append(str, ", ");
+    g_string_append(str, "bond-slave, bridge-slave, team-slave");
+
+    return g_string_free(str, FALSE);
+}
+
+static const NMMetaSettingValidPartItem *const *
+get_valid_settings_array(const char *con_type)
+{
+    const NMMetaSettingInfoEditor *setting_info;
+
+    /* No connection type yet? Return settings for a generic connection
+     * (just the "connection" setting), which always makes sense. */
+    if (!con_type)
+        return nm_meta_setting_info_valid_parts_default;
+
+    setting_info = nm_meta_setting_info_editor_find_by_name(con_type, FALSE);
+    if (setting_info)
+        return setting_info->valid_parts ?: NM_PTRARRAY_EMPTY(const NMMetaSettingValidPartItem *);
+    return NULL;
+}
+
+static char *
+_construct_property_name(const char *           setting_name,
+                         const char *           property_name,
+                         NMMetaAccessorModifier modifier)
+{
+    return g_strdup_printf("%s%s.%s\n",
+                           (modifier == NM_META_ACCESSOR_MODIFIER_ADD
+                                ? "+"
+                                : (modifier == NM_META_ACCESSOR_MODIFIER_DEL ? "-" : "")),
+                           setting_name,
+                           property_name);
+}
+
+/* get_valid_properties_string:
+ * @array: base properties for the current connection type
+ * @array_slv: slave properties (or ipv4/ipv6 ones) for the current connection type
+ * @modifier: to prepend to each element of the returned list
+ * @prefix: only properties matching the prefix will be returned
+ * @postfix: required prefix on the property args; if a empty string is passed, is
+ *           assumed that the @prefix is a shortcut, so it should not be completed
+ *           but left as is (and an additional check for shortcut ambiguity is performed)
+ *
+ * Returns a list of properties compatible with the current connection type
+ * for the shell autocompletion functionality.
+ *
+ * Returns: list of property.arg elements
+ */
+static char *
+get_valid_properties_string(const NMMetaSettingValidPartItem *const *array,
+                            const NMMetaSettingValidPartItem *const *array_slv,
+                            NMMetaAccessorModifier                   modifier,
+                            const char *                             prefix,
+                            const char *                             postfix)
+{
+    const NMMetaSettingValidPartItem *const *iter      = array;
+    const char *                             prop_name = NULL;
+    GString *                                str;
+    guint                                    i, j;
+    gboolean                                 full_match = FALSE;
+
+    g_return_val_if_fail(prefix, NULL);
+
+    str = g_string_sized_new(1024);
+
+    for (i = 0; i < 2; i++, iter = array_slv) {
+        for (; !full_match && iter && *iter; iter++) {
+            const NMMetaSettingInfoEditor *setting_info = (*iter)->setting_info;
+
+            if (!(g_str_has_prefix(setting_info->general->setting_name, prefix))
+                && (!setting_info->alias || !g_str_has_prefix(setting_info->alias, prefix))) {
+                continue;
+            }
+
+            /* If postix (so prefix is terminated by a dot), check
+             * that prefix is not ambiguous */
+            if (postfix) {
+                /* If we have a perfect match, no need to look for others
+                 * prefix and no check on ambiguity should be performed.
+                 * Moreover, erase previous matches from output string */
+                if (nm_streq(prefix, setting_info->general->setting_name)
+                    || nm_streq0(prefix, setting_info->alias)) {
+                    g_string_erase(str, 0, -1);
+                    full_match = TRUE;
+                } else if (prop_name)
+                    return g_string_free(str, TRUE);
+                prop_name = prefix;
+            } else
+                prop_name = setting_info->general->setting_name;
+
+            /* Search the array with the arguments of the current property */
+            for (j = 0; j < setting_info->properties_num; j++) {
+                gs_free char *ss1 = NULL;
+                const char *  arg_name;
+
+                arg_name = setting_info->properties[j]->property_name;
+
+                /* If required, expand the alias too */
+                if (!postfix && setting_info->alias) {
+                    gs_free char *ss2 = NULL;
+
+                    ss2 = _construct_property_name(setting_info->alias, arg_name, modifier);
+                    g_string_append(str, ss2);
+                }
+
+                if (postfix && !g_str_has_prefix(arg_name, postfix))
+                    continue;
+
+                ss1 = _construct_property_name(prop_name, arg_name, modifier);
+                g_string_append(str, ss1);
+            }
+        }
+    }
+    return g_string_free(str, FALSE);
+}
+
+/*
+ * Check if 'val' is valid string in either array->name or array->alias for
+ * both array parameters (array & array_slv).
+ * It accepts shorter string provided they are not ambiguous.
+ * 'val' == NULL doesn't hurt.
+ *
+ * Returns: pointer to array->name string or NULL on failure.
+ * The returned string must not be freed.
+ */
+static const char *
+check_valid_name(const char *                             val,
+                 const NMMetaSettingValidPartItem *const *array,
+                 const NMMetaSettingValidPartItem *const *array_slv,
+                 GError **                                error)
+{
+    const NMMetaSettingValidPartItem *const *iter;
+    gs_unref_ptrarray GPtrArray *tmp_arr = NULL;
+    const char *                 str;
+    GError *                     tmp_err = NULL;
+    int                          i;
+
+    g_return_val_if_fail(array, NULL);
+
+    /* Create a temporary array that can be used in nmc_string_is_valid() */
+    tmp_arr = g_ptr_array_sized_new(32);
+    iter    = array;
+    for (i = 0; i < 2; i++, iter = array_slv) {
+        for (; iter && *iter; iter++) {
+            const NMMetaSettingInfoEditor *setting_info = (*iter)->setting_info;
+
+            g_ptr_array_add(tmp_arr, (gpointer) setting_info->general->setting_name);
+            if (setting_info->alias)
+                g_ptr_array_add(tmp_arr, (gpointer) setting_info->alias);
+        }
+    }
+    g_ptr_array_add(tmp_arr, (gpointer) NULL);
+
+    /* Check string validity */
+    str = nmc_string_is_valid(val, (const char **) tmp_arr->pdata, &tmp_err);
+    if (!str) {
+        if (nm_g_error_matches(tmp_err, NM_UTILS_ERROR, NM_UTILS_ERROR_AMBIGUOUS))
+            g_propagate_error(error, tmp_err);
+        else {
+            /* We want to handle aliases, so construct own error message */
+            gs_free char *err_str = NULL;
+
+            err_str = get_valid_options_string(array, array_slv);
+            g_set_error(error, 1, 0, _("'%s' not among [%s]"), val, err_str);
+            g_clear_error(&tmp_err);
+        }
+        return NULL;
+    }
+
+    /* Return a pointer to the found string in passed 'array' */
+    iter = array;
+    for (i = 0; i < 2; i++, iter = array_slv) {
+        for (; iter && *iter; iter++) {
+            const NMMetaSettingInfoEditor *setting_info = (*iter)->setting_info;
+
+            if (nm_streq(setting_info->general->setting_name, str)
+                || nm_streq0(setting_info->alias, str)) {
+                return setting_info->general->setting_name;
+            }
+        }
+    }
+
+    /* We should not really come here */
+    g_set_error(error, 1, 0, _("Unknown error"));
+    return NULL;
+}
+
+static const char *
+check_valid_name_toplevel(const char *val, const char **slave_type, GError **error)
+{
+    gs_unref_ptrarray GPtrArray *  tmp_arr = NULL;
+    const NMMetaSettingInfoEditor *setting_info;
+    gs_free_error GError *tmp_err = NULL;
+    const char *          str;
+    int                   i;
+
+    NM_SET_OUT(slave_type, NULL);
+
+    /* Create a temporary array that can be used in nmc_string_is_valid() */
+    tmp_arr = g_ptr_array_sized_new(32);
+    for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++) {
+        setting_info = &nm_meta_setting_infos_editor[i];
+        g_ptr_array_add(tmp_arr, (gpointer) setting_info->general->setting_name);
+        if (setting_info->alias)
+            g_ptr_array_add(tmp_arr, (gpointer) setting_info->alias);
+    }
+    g_ptr_array_add(tmp_arr, "bond-slave");
+    g_ptr_array_add(tmp_arr, "bridge-slave");
+    g_ptr_array_add(tmp_arr, "team-slave");
+    g_ptr_array_add(tmp_arr, (gpointer) NULL);
+
+    /* Check string validity */
+    str = nmc_string_is_valid(val, (const char **) tmp_arr->pdata, &tmp_err);
+    if (!str) {
+        if (nm_g_error_matches(tmp_err, NM_UTILS_ERROR, NM_UTILS_ERROR_AMBIGUOUS))
+            g_propagate_error(error, g_steal_pointer(&tmp_err));
+        else {
+            /* We want to handle aliases, so construct own error message */
+            gs_free char *err_str = NULL;
+
+            err_str = get_valid_options_string_toplevel();
+            g_set_error(error, 1, 0, _("'%s' not among [%s]"), val, err_str);
+        }
+        return NULL;
+    }
+
+    if (nm_streq(str, "bond-slave")) {
+        NM_SET_OUT(slave_type, NM_SETTING_BOND_SETTING_NAME);
+        return NM_SETTING_WIRED_SETTING_NAME;
+    } else if (nm_streq(str, "bridge-slave")) {
+        NM_SET_OUT(slave_type, NM_SETTING_BRIDGE_SETTING_NAME);
+        return NM_SETTING_WIRED_SETTING_NAME;
+    } else if (nm_streq(str, "team-slave")) {
+        NM_SET_OUT(slave_type, NM_SETTING_TEAM_SETTING_NAME);
+        return NM_SETTING_WIRED_SETTING_NAME;
+    }
+
+    setting_info = nm_meta_setting_info_editor_find_by_name(str, TRUE);
+    if (setting_info)
+        return setting_info->general->setting_name;
+
+    /* We should not really come here */
+    g_set_error(error, 1, 0, _("Unknown error"));
+    return NULL;
+}
+
+static gboolean
+is_setting_mandatory(NMConnection *connection, NMSetting *setting)
+{
+    NMSettingConnection *                    s_con;
+    const char *                             c_type;
+    const NMMetaSettingValidPartItem *const *item;
+    const char *                             name;
+    const char *                             s_type;
+    guint                                    i;
+
+    s_con = nm_connection_get_setting_connection(connection);
+    g_assert(s_con);
+    c_type = nm_setting_connection_get_connection_type(s_con);
+    s_type = nm_setting_connection_get_slave_type(s_con);
+
+    name = nm_setting_get_name(setting);
+
+    for (i = 0; i < 2; i++) {
+        if (i == 0)
+            item = get_valid_settings_array(c_type);
+        else
+            item = nm_meta_setting_info_valid_parts_for_slave_type(s_type, NULL);
+        for (; item && *item; item++) {
+            if (!strcmp(name, (*item)->setting_info->general->setting_name))
+                return (*item)->mandatory;
+        }
+    }
+
+    return FALSE;
+}
+
+/*****************************************************************************/
+
+static const char *
+_strip_master_prefix(const char *master, const char *(**func)(NMConnection *) )
+{
+    if (!master)
+        return NULL;
+
+    if (g_str_has_prefix(master, "ifname/")) {
+        master = master + strlen("ifname/");
+        if (func)
+            *func = nm_connection_get_interface_name;
+    } else if (g_str_has_prefix(master, "uuid/")) {
+        master = master + strlen("uuid/");
+        if (func)
+            *func = nm_connection_get_uuid;
+    } else if (g_str_has_prefix(master, "id/")) {
+        master = master + strlen("id/");
+        if (func)
+            *func = nm_connection_get_id;
+    }
+    return master;
+}
+
+/* normalized_master_for_slave:
+ * @connections: list af all connections
+ * @master: UUID, ifname or ID of the master connection
+ * @type: virtual connection type (bond, team, bridge, ...) or %NULL
+ * @out_type: type of the connection that matched
+ *
+ * Check whether master is a valid interface name, UUID or ID of some connection,
+ * possibly of a specified @type.
+ * First UUID and ifname are checked. If they don't match, ID is checked
+ * and replaced by UUID on a match.
+ *
+ * Returns: identifier of master connection if found, %NULL otherwise
+ */
+static const char *
+normalized_master_for_slave(const GPtrArray *connections,
+                            const char *     master,
+                            const char *     type,
+                            const char **    out_type)
+{
+    NMConnection *       connection;
+    NMSettingConnection *s_con;
+    const char *         con_type = NULL, *id, *uuid, *ifname;
+    guint                i;
+    const char *         found_by_id    = NULL;
+    const char *         out_type_by_id = NULL;
+    const char *         out_master     = NULL;
+    const char *(*func)(NMConnection *) = NULL;
+
+    if (!master)
+        return NULL;
+
+    master = _strip_master_prefix(master, &func);
+    for (i = 0; i < connections->len; i++) {
+        connection = NM_CONNECTION(connections->pdata[i]);
+        s_con      = nm_connection_get_setting_connection(connection);
+        g_assert(s_con);
+        con_type = nm_setting_connection_get_connection_type(s_con);
+        if (type && g_strcmp0(con_type, type) != 0)
+            continue;
+        if (func) {
+            /* There was a prefix; only compare to that type. */
+            if (g_strcmp0(master, func(connection)) == 0) {
+                if (out_type)
+                    *out_type = con_type;
+                if (func == nm_connection_get_id)
+                    out_master = nm_connection_get_uuid(connection);
+                else
+                    out_master = master;
+                break;
+            }
+        } else {
+            id     = nm_connection_get_id(connection);
+            uuid   = nm_connection_get_uuid(connection);
+            ifname = nm_connection_get_interface_name(connection);
+            if (g_strcmp0(master, uuid) == 0 || g_strcmp0(master, ifname) == 0) {
+                out_master = master;
+                if (out_type)
+                    *out_type = con_type;
+                break;
+            }
+            if (!found_by_id && g_strcmp0(master, id) == 0) {
+                out_type_by_id = con_type;
+                found_by_id    = uuid;
+            }
+        }
+    }
+
+    if (!out_master) {
+        out_master = found_by_id;
+        if (out_type)
+            *out_type = out_type_by_id;
+    }
+
+    if (!out_master) {
+        g_print(_("Warning: master='%s' doesn't refer to any existing profile.\n"), master);
+        out_master = master;
+        if (out_type)
+            *out_type = type;
+    }
+
+    return out_master;
+}
+
+#define WORD_YES "yes"
+#define WORD_NO  "no"
+static const char *
+prompt_yes_no(gboolean default_yes, char *delim)
+{
+    static char prompt[128] = {0};
+
+    if (!delim)
+        delim = "";
+
+    snprintf(prompt,
+             sizeof(prompt),
+             "(%s/%s) [%s]%s ",
+             WORD_YES,
+             WORD_NO,
+             default_yes ? WORD_YES : WORD_NO,
+             delim);
+
+    return prompt;
+}
+
+static NMSetting *
+is_setting_valid(NMConnection *                           connection,
+                 const NMMetaSettingValidPartItem *const *valid_settings_main,
+                 const NMMetaSettingValidPartItem *const *valid_settings_slave,
+                 const char *                             setting)
+{
+    const char *setting_name;
+
+    if (!(setting_name =
+              check_valid_name(setting, valid_settings_main, valid_settings_slave, NULL)))
+        return NULL;
+    return nm_connection_get_setting_by_name(connection, setting_name);
+}
+
+static char *
+is_property_valid(NMSetting *setting, const char *property, GError **error)
+{
+    gs_strfreev char **valid_props = NULL;
+    const char *       prop_name;
+
+    valid_props = nmc_setting_get_valid_properties(setting);
+    prop_name   = nmc_string_is_valid(property, (const char **) valid_props, error);
+    return g_strdup(prop_name);
+}
+
+static char *
+unique_master_iface_ifname(const GPtrArray *connections, const char *try_name)
+{
+    char *new_name;
+    guint num = 0;
+    guint i;
+
+    new_name = g_strdup(try_name);
+
+again:
+    for (i = 0; i < connections->len; i++) {
+        NMConnection *connection = connections->pdata[i];
+
+        if (nm_streq0(new_name, nm_connection_get_interface_name(connection))) {
+            num++;
+            g_free(new_name);
+            new_name = g_strdup_printf("%s%u", try_name, num);
+            goto again;
+        }
+    }
+    return new_name;
+}
+
+static void
+set_default_interface_name(NmCli *nmc, NMSettingConnection *s_con)
+{
+    const char *default_name;
+    const char *con_type;
+
+    if (nm_setting_connection_get_interface_name(s_con))
+        return;
+
+    con_type = nm_setting_connection_get_connection_type(s_con);
+
+    /* Set a sensible bond/team/bridge interface name by default */
+    if (nm_streq0(con_type, NM_SETTING_BOND_SETTING_NAME))
+        default_name = "nm-bond";
+    else if (nm_streq0(con_type, NM_SETTING_TEAM_SETTING_NAME))
+        default_name = "nm-team";
+    else if (nm_streq0(con_type, NM_SETTING_BRIDGE_SETTING_NAME))
+        default_name = "nm-bridge";
+    else
+        default_name = NULL;
+
+    if (default_name) {
+        const GPtrArray *connections;
+        gs_free char *   ifname = NULL;
+
+        connections = nm_client_get_connections(nmc->client);
+        ifname      = unique_master_iface_ifname(connections, default_name);
+        g_object_set(s_con, NM_SETTING_CONNECTION_INTERFACE_NAME, ifname, NULL);
+    }
+}
+
+/*****************************************************************************/
+
+static PropertyInfFlags
+_dynamic_options_set(const NMMetaAbstractInfo *abstract_info,
+                     PropertyInfFlags          mask,
+                     PropertyInfFlags          set)
+{
+    static GHashTable *cache = NULL;
+    gpointer           p;
+    PropertyInfFlags   v, v2;
+
+    if (G_UNLIKELY(!cache))
+        cache = g_hash_table_new(nm_direct_hash, NULL);
+
+    if (g_hash_table_lookup_extended(cache, (gpointer) abstract_info, NULL, &p))
+        v = GPOINTER_TO_UINT(p);
+    else
+        v = 0;
+
+    v2 = (v & ~mask) | (mask & set);
+    if (v != v2)
+        g_hash_table_insert(cache, (gpointer) abstract_info, GUINT_TO_POINTER(v2));
+
+    return v2;
+}
+
+static PropertyInfFlags
+_dynamic_options_get(const NMMetaAbstractInfo *abstract_info)
+{
+    return _dynamic_options_set(abstract_info, 0, 0);
+}
+
+/*****************************************************************************/
+
+static gboolean
+_meta_property_needs_bond_hack(const NMMetaPropertyInfo *property_info)
+{
+    /* hack: the bond property data is handled special and not generically.
+     * Eventually, get rid of explicitly checking whether we handle a bond. */
+    if (!property_info)
+        g_return_val_if_reached(FALSE);
+    return property_info->property_typ_data
+           && property_info->property_typ_data->nested == &nm_meta_property_typ_data_bond;
+}
+
+static char **
+_meta_abstract_complete(const NMMetaAbstractInfo *abstract_info, const char *text)
+{
+    const char *const *          values;
+    char **                      values_to_free = NULL;
+    const NMMetaOperationContext ctx            = {
+        .connection = nmc_tab_completion.connection,
+    };
+
+    values = nm_meta_abstract_info_complete(abstract_info,
+                                            nmc_meta_environment,
+                                            (gpointer) nmc_meta_environment_arg,
+                                            &ctx,
+                                            text,
+                                            NULL,
+                                            &values_to_free);
+    if (values)
+        return values_to_free ?: g_strdupv((char **) values);
+    return NULL;
+}
+
+static char *
+_meta_abstract_generator(const char *text, int state)
+{
+    if (nmc_tab_completion.words) {
+        return nmc_rl_gen_func_basic(text, state, (const char *const *) nmc_tab_completion.words);
+    }
+
+    return NULL;
+}
+
+static void
+_meta_abstract_get(const NMMetaAbstractInfo *      abstract_info,
+                   const NMMetaSettingInfoEditor **out_setting_info,
+                   const char **                   out_setting_name,
+                   const char **                   out_property_name,
+                   const char **                   out_option,
+                   NMMetaPropertyInfFlags *        out_inf_flags,
+                   const char **                   out_prompt,
+                   const char **                   out_def_hint)
+{
+    const NMMetaPropertyInfo *info = (const NMMetaPropertyInfo *) abstract_info;
+
+    NM_SET_OUT(out_option, info->property_alias);
+    NM_SET_OUT(out_setting_info, info->setting_info);
+    NM_SET_OUT(out_setting_name, info->setting_info->general->setting_name);
+    NM_SET_OUT(out_property_name, info->property_name);
+    NM_SET_OUT(out_option, info->property_alias);
+    NM_SET_OUT(out_inf_flags, info->inf_flags);
+    NM_SET_OUT(out_prompt, info->prompt);
+    NM_SET_OUT(out_def_hint, info->def_hint);
+}
+
+static const OptionInfo *_meta_abstract_get_option_info(const NMMetaAbstractInfo *abstract_info);
+
+/*
+ * Mark options in option_info as relevant.
+ * The questionnaire (for --ask) will ask for them.
+ */
+static void
+enable_options(const char *setting_name, const char *property, const char *const *opts)
+{
+    const NMMetaPropertyInfo *property_info;
+
+    property_info = nm_meta_property_info_find_by_name(setting_name, property);
+
+    if (!property_info)
+        g_return_if_reached();
+
+    if (_meta_property_needs_bond_hack(property_info)) {
+        guint i;
+
+        for (i = 0; i < nm_meta_property_typ_data_bond.nested_len; i++) {
+            const NMMetaNestedPropertyInfo *bi = &nm_meta_property_typ_data_bond.nested[i];
+
+            if (bi->base.inf_flags & NM_META_PROPERTY_INF_FLAG_DONT_ASK && bi->base.property_alias
+                && g_strv_contains(opts, bi->base.property_alias))
+                _dynamic_options_set((const NMMetaAbstractInfo *) bi,
+                                     PROPERTY_INF_FLAG_ENABLED,
+                                     PROPERTY_INF_FLAG_ENABLED);
+        }
+        return;
+    }
+
+    if (!property_info->is_cli_option)
+        g_return_if_reached();
+
+    if (property_info->inf_flags & NM_META_PROPERTY_INF_FLAG_DONT_ASK
+        && property_info->property_alias && g_strv_contains(opts, property_info->property_alias))
+        _dynamic_options_set((const NMMetaAbstractInfo *) property_info,
+                             PROPERTY_INF_FLAG_ENABLED,
+                             PROPERTY_INF_FLAG_ENABLED);
+}
+
+/*
+ * Mark options in option_info as irrelevant (because we learned they make no sense
+ * or they have been set via different means).
+ * The questionnaire (for --ask) will not ask for them.
+ */
+static void
+disable_options(const char *setting_name, const char *property)
+{
+    const NMMetaPropertyInfo *       property_infos_local[2];
+    const NMMetaPropertyInfo *const *property_infos;
+    guint                            p;
+
+    if (property) {
+        const NMMetaPropertyInfo *pi;
+
+        pi = nm_meta_property_info_find_by_name(setting_name, property);
+        if (!pi)
+            g_return_if_reached();
+        if (!_meta_property_needs_bond_hack(pi) && !pi->is_cli_option)
+            return;
+        property_infos_local[0] = pi;
+        property_infos_local[1] = NULL;
+        property_infos          = property_infos_local;
+    } else {
+        const NMMetaSettingInfoEditor *setting_info;
+
+        setting_info = nm_meta_setting_info_editor_find_by_name(setting_name, FALSE);
+        if (!setting_info)
+            g_return_if_reached();
+        property_infos = setting_info->properties;
+        if (!property_infos)
+            return;
+    }
+
+    for (p = 0; property_infos[p]; p++) {
+        const NMMetaPropertyInfo *property_info = property_infos[p];
+
+        if (_meta_property_needs_bond_hack(property_info)) {
+            guint i;
+
+            for (i = 0; i < nm_meta_property_typ_data_bond.nested_len; i++) {
+                const NMMetaNestedPropertyInfo *bi = &nm_meta_property_typ_data_bond.nested[i];
+
+                _dynamic_options_set((const NMMetaAbstractInfo *) bi,
+                                     PROPERTY_INF_FLAG_DISABLED,
+                                     PROPERTY_INF_FLAG_DISABLED);
+            }
+            nm_assert(p == 0 && !property_infos[1]);
+        } else {
+            if (property_info->is_cli_option)
+                _dynamic_options_set((const NMMetaAbstractInfo *) property_info,
+                                     PROPERTY_INF_FLAG_DISABLED,
+                                     PROPERTY_INF_FLAG_DISABLED);
+        }
+    }
+}
+
+/*
+ * Reset marks done with enable_options() and disable_options().
+ * Ensures correct operation in case more than one connection is added in a single
+ * nmcli session.
+ */
+static void
+reset_options(void)
+{
+    NMMetaSettingType s;
+
+    for (s = 0; s < _NM_META_SETTING_TYPE_NUM; s++) {
+        const NMMetaPropertyInfo *const *property_infos;
+        guint                            p;
+
+        property_infos = nm_meta_setting_infos_editor[s].properties;
+        if (!property_infos)
+            continue;
+        for (p = 0; property_infos[p]; p++) {
+            const NMMetaPropertyInfo *property_info = property_infos[p];
+
+            if (_meta_property_needs_bond_hack(property_info)) {
+                guint i;
+
+                for (i = 0; i < nm_meta_property_typ_data_bond.nested_len; i++) {
+                    const NMMetaNestedPropertyInfo *bi = &nm_meta_property_typ_data_bond.nested[i];
+
+                    _dynamic_options_set((const NMMetaAbstractInfo *) bi, PROPERTY_INF_FLAG_ALL, 0);
+                }
+            } else {
+                if (property_info->is_cli_option)
+                    _dynamic_options_set((const NMMetaAbstractInfo *) property_info,
+                                         PROPERTY_INF_FLAG_ALL,
+                                         0);
+            }
+        }
+    }
+}
+
+static gboolean
+set_property(NMClient *             client,
+             NMConnection *         connection,
+             const char *           setting_name,
+             const char *           property,
+             const char *           value,
+             NMMetaAccessorModifier modifier,
+             GError **              error)
+{
+    gs_free char *property_name = NULL;
+    gs_free_error GError *local = NULL;
+    NMSetting *           setting;
+
+    nm_assert(setting_name && setting_name[0]);
+    nm_assert(NM_IN_SET(modifier,
+                        NM_META_ACCESSOR_MODIFIER_SET,
+                        NM_META_ACCESSOR_MODIFIER_ADD,
+                        NM_META_ACCESSOR_MODIFIER_DEL));
+
+    setting = nm_connection_get_setting_by_name(connection, setting_name);
+    if (!setting) {
+        setting = nm_meta_setting_info_editor_new_setting(
+            nm_meta_setting_info_editor_find_by_name(setting_name, FALSE),
+            NM_META_ACCESSOR_SETTING_INIT_TYPE_CLI);
+        nm_connection_add_setting(connection, setting);
+    }
+
+    property_name = is_property_valid(setting, property, &local);
+    if (!property_name) {
+        g_set_error(error,
+                    NMCLI_ERROR,
+                    NMC_RESULT_ERROR_USER_INPUT,
+                    _("Error: invalid property '%s': %s."),
+                    property,
+                    local->message);
+        return FALSE;
+    }
+
+    if (!nmc_setting_set_property(client,
+                                  setting,
+                                  property_name,
+                                  ((modifier == NM_META_ACCESSOR_MODIFIER_DEL && !value)
+                                       ? NM_META_ACCESSOR_MODIFIER_SET
+                                       : modifier),
+                                  value,
+                                  &local)) {
+        g_set_error(error,
+                    NMCLI_ERROR,
+                    NMC_RESULT_ERROR_USER_INPUT,
+                    _("Error: failed to %s %s.%s: %s."),
+                    (modifier != NM_META_ACCESSOR_MODIFIER_DEL ? "modify" : "remove a value from"),
+                    setting_name,
+                    property,
+                    local->message);
+        return FALSE;
+    }
+
+    /* Don't ask for this property in interactive mode. */
+    disable_options(setting_name, property_name);
+
+    return TRUE;
+}
+
+static gboolean
+set_option(NmCli *                   nmc,
+           NMConnection *            connection,
+           const NMMetaAbstractInfo *abstract_info,
+           const char *              value,
+           GError **                 error)
+{
+    const char *           setting_name, *property_name, *option_name;
+    NMMetaPropertyInfFlags inf_flags;
+    const OptionInfo *     option;
+
+    option = _meta_abstract_get_option_info(abstract_info);
+
+    _dynamic_options_set(abstract_info, PROPERTY_INF_FLAG_DISABLED, PROPERTY_INF_FLAG_DISABLED);
+
+    _meta_abstract_get(abstract_info,
+                       NULL,
+                       &setting_name,
+                       &property_name,
+                       &option_name,
+                       &inf_flags,
+                       NULL,
+                       NULL);
+    if (option && option->check_and_set) {
+        return option->check_and_set(nmc, connection, option, value, error);
+    } else {
+        set_property(nmc->client,
+                     connection,
+                     setting_name,
+                     property_name,
+                     value,
+                     !value ? NM_META_ACCESSOR_MODIFIER_DEL
+                            : (inf_flags & NM_META_PROPERTY_INF_FLAG_MULTI
+                                   ? NM_META_ACCESSOR_MODIFIER_ADD
+                                   : NM_META_ACCESSOR_MODIFIER_SET),
+                     error);
+    }
+
+    return TRUE;
+}
+
+/*
+ * Return relevant NameItem[] tables for given connection (based on connection type
+ * and slave type.
+ */
+static gboolean
+con_settings(NMConnection *                            connection,
+             const NMMetaSettingValidPartItem *const **type_settings,
+             const NMMetaSettingValidPartItem *const **slv_settings,
+             GError **                                 error)
+{
+    const char *         con_type;
+    NMSettingConnection *s_con;
+
+    g_return_val_if_fail(type_settings, FALSE);
+    g_return_val_if_fail(slv_settings, FALSE);
+
+    s_con = nm_connection_get_setting_connection(connection);
+    g_assert(s_con);
+
+    con_type      = nm_setting_connection_get_slave_type(s_con);
+    *slv_settings = nm_meta_setting_info_valid_parts_for_slave_type(con_type, NULL);
+    if (!*slv_settings) {
+        g_set_error(error,
+                    NMCLI_ERROR,
+                    NMC_RESULT_ERROR_USER_INPUT,
+                    _("Error: invalid slave type; %s."),
+                    con_type);
+        return FALSE;
+    }
+
+    con_type       = nm_setting_connection_get_connection_type(s_con);
+    *type_settings = get_valid_settings_array(con_type);
+    if (!*type_settings) {
+        g_set_error(error,
+                    NMCLI_ERROR,
+                    NMC_RESULT_ERROR_USER_INPUT,
+                    _("Error: invalid connection type; %s."),
+                    con_type);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+/*
+ * Make sure all required settings are in place (should be called when
+ * it's possible that a type is already set).
+ */
+static void
+ensure_settings(NMConnection *connection, const NMMetaSettingValidPartItem *const *item)
+{
+    NMSetting *setting;
+
+    for (; item && *item; item++) {
+        if (!(*item)->mandatory)
+            continue;
+        if (nm_connection_get_setting_by_name(connection,
+                                              (*item)->setting_info->general->setting_name))
+            continue;
+        setting = nm_meta_setting_info_editor_new_setting((*item)->setting_info,
+                                                          NM_META_ACCESSOR_SETTING_INIT_TYPE_CLI);
+        nm_connection_add_setting(connection, setting);
+    }
+}
+
+/*****************************************************************************/
+
+static char *
+gen_func_bool_values_l10n(const char *text, int state)
+{
+    const char *words[] = {WORD_YES, WORD_NO, NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+
+static char *
+gen_func_bt_type(const char *text, int state)
+{
+    const char *words[] = {"panu", "nap", "dun-gsm", "dun-cdma", NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+
+static char *
+gen_func_bond_mode(const char *text, int state)
+{
+    const char *words[] = {"balance-rr",
+                           "active-backup",
+                           "balance-xor",
+                           "broadcast",
+                           "802.3ad",
+                           "balance-tlb",
+                           "balance-alb",
+                           NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+static char *
+gen_func_bond_mon_mode(const char *text, int state)
+{
+    const char *words[] = {"miimon", "arp", NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+static char *
+gen_func_bond_lacp_rate(const char *text, int state)
+{
+    const char *words[] = {"slow", "fast", NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+
+/*****************************************************************************/
+
+static gboolean
+set_connection_type(NmCli *           nmc,
+                    NMConnection *    con,
+                    const OptionInfo *option,
+                    const char *      value,
+                    GError **         error)
+{
+    const NMMetaSettingValidPartItem *const *type_settings;
+    const NMMetaSettingValidPartItem *const *slv_settings;
+    GError *                                 local      = NULL;
+    const char *                             master[]   = {"master", NULL};
+    const char *                             slave_type = NULL;
+
+    value = check_valid_name_toplevel(value, &slave_type, &local);
+    if (!value) {
+        g_set_error(error,
+                    NMCLI_ERROR,
+                    NMC_RESULT_ERROR_USER_INPUT,
+                    _("Error: bad connection type: %s"),
+                    local->message);
+        g_clear_error(&local);
+        return FALSE;
+    }
+
+    if (slave_type) {
+        if (!set_property(nmc->client,
+                          con,
+                          NM_SETTING_CONNECTION_SETTING_NAME,
+                          NM_SETTING_CONNECTION_SLAVE_TYPE,
+                          slave_type,
+                          NM_META_ACCESSOR_MODIFIER_SET,
+                          error)) {
+            return FALSE;
+        }
+        enable_options(NM_SETTING_CONNECTION_SETTING_NAME, NM_SETTING_CONNECTION_MASTER, master);
+    }
+
+    /* ifname is mandatory for all connection types except virtual ones (bond, team, bridge, vlan) */
+    if ((strcmp(value, NM_SETTING_BOND_SETTING_NAME) == 0)
+        || (strcmp(value, NM_SETTING_TEAM_SETTING_NAME) == 0)
+        || (strcmp(value, NM_SETTING_BRIDGE_SETTING_NAME) == 0)
+        || (strcmp(value, NM_SETTING_VLAN_SETTING_NAME) == 0)) {
+        disable_options(NM_SETTING_CONNECTION_SETTING_NAME, NM_SETTING_CONNECTION_INTERFACE_NAME);
+    }
+
+    if (!set_property(nmc->client,
+                      con,
+                      option->setting_info->general->setting_name,
+                      option->property,
+                      value,
+                      NM_META_ACCESSOR_MODIFIER_SET,
+                      error))
+        return FALSE;
+
+    if (!con_settings(con, &type_settings, &slv_settings, error))
+        return FALSE;
+
+    ensure_settings(con, slv_settings);
+    ensure_settings(con, type_settings);
+
+    return TRUE;
+}
+
+static gboolean
+set_connection_iface(NmCli *           nmc,
+                     NMConnection *    con,
+                     const OptionInfo *option,
+                     const char *      value,
+                     GError **         error)
+{
+    if (value) {
+        /* Special value of '*' means no specific interface name */
+        if (strcmp(value, "*") == 0)
+            value = NULL;
+    }
+
+    return set_property(nmc->client,
+                        con,
+                        option->setting_info->general->setting_name,
+                        option->property,
+                        value,
+                        NM_META_ACCESSOR_MODIFIER_SET,
+                        error);
+}
+
+static gboolean
+set_connection_master(NmCli *           nmc,
+                      NMConnection *    con,
+                      const OptionInfo *option,
+                      const char *      value,
+                      GError **         error)
+{
+    const GPtrArray *    connections;
+    NMSettingConnection *s_con;
+    const char *         slave_type;
+
+    s_con = nm_connection_get_setting_connection(con);
+    g_return_val_if_fail(s_con, FALSE);
+
+    if (!value) {
+        g_set_error_literal(error,
+                            NMCLI_ERROR,
+                            NMC_RESULT_ERROR_USER_INPUT,
+                            _("Error: master is required"));
+        return FALSE;
+    }
+
+    slave_type  = nm_setting_connection_get_slave_type(s_con);
+    connections = nm_client_get_connections(nmc->client);
+    value       = normalized_master_for_slave(connections, value, slave_type, &slave_type);
+
+    if (!set_property(nmc->client,
+                      con,
+                      NM_SETTING_CONNECTION_SETTING_NAME,
+                      NM_SETTING_CONNECTION_SLAVE_TYPE,
+                      slave_type,
+                      NM_META_ACCESSOR_MODIFIER_SET,
+                      error)) {
+        return FALSE;
+    }
+
+    return set_property(nmc->client,
+                        con,
+                        option->setting_info->general->setting_name,
+                        option->property,
+                        value,
+                        NM_META_ACCESSOR_MODIFIER_SET,
+                        error);
+}
+
+static gboolean
+set_bond_option(NmCli *           nmc,
+                NMConnection *    con,
+                const OptionInfo *option,
+                const char *      value,
+                GError **         error)
+{
+    NMSettingBond *s_bond;
+    gboolean       success;
+    gs_free char * name = NULL;
+    char *         p;
+
+    s_bond = nm_connection_get_setting_bond(con);
+    g_return_val_if_fail(s_bond, FALSE);
+
+    name = g_strdup(option->option);
+    for (p = name; p[0]; p++) {
+        if (p[0] == '-')
+            p[0] = '_';
+    }
+
+    if (nm_str_is_empty(value)) {
+        nm_setting_bond_remove_option(s_bond, name);
+        success = TRUE;
+    } else
+        success = _nm_meta_setting_bond_add_option(NM_SETTING(s_bond), name, value, error);
+
+    if (!success)
+        return FALSE;
+
+    if (success) {
+        if (nm_streq(name, NM_SETTING_BOND_OPTION_MODE)) {
+            value = nmc_bond_validate_mode(value, error);
+            if (nm_streq(value, "active-backup")) {
+                enable_options(NM_SETTING_BOND_SETTING_NAME,
+                               NM_SETTING_BOND_OPTIONS,
+                               NM_MAKE_STRV("primary"));
+            }
+        }
+    }
+
+    return success;
+}
+
+static gboolean
+set_bond_monitoring_mode(NmCli *           nmc,
+                         NMConnection *    con,
+                         const OptionInfo *option,
+                         const char *      value,
+                         GError **         error)
+{
+    NMSettingBond *s_bond;
+    gs_free char * monitor_mode  = NULL;
+    const char *   miimon_opts[] = {"miimon", "downdelay", "updelay", NULL};
+    const char *   arp_opts[]    = {"arp-interval", "arp-ip-target", NULL};
+
+    s_bond = nm_connection_get_setting_bond(con);
+    g_return_val_if_fail(s_bond, FALSE);
+
+    if (value) {
+        monitor_mode = g_strdup(value);
+        g_strstrip(monitor_mode);
+    } else {
+        monitor_mode = g_strdup(NM_META_TEXT_WORD_MIIMON);
+    }
+
+    if (matches(monitor_mode, NM_META_TEXT_WORD_MIIMON))
+        enable_options(NM_SETTING_BOND_SETTING_NAME, NM_SETTING_BOND_OPTIONS, miimon_opts);
+    else if (matches(monitor_mode, NM_META_TEXT_WORD_ARP))
+        enable_options(NM_SETTING_BOND_SETTING_NAME, NM_SETTING_BOND_OPTIONS, arp_opts);
+    else {
+        g_set_error(error,
+                    NMCLI_ERROR,
+                    NMC_RESULT_ERROR_USER_INPUT,
+                    _("Error: '%s' is not a valid monitoring mode; use '%s' or '%s'.\n"),
+                    monitor_mode,
+                    NM_META_TEXT_WORD_MIIMON,
+                    NM_META_TEXT_WORD_ARP);
+        return FALSE;
+    }
+
+    return TRUE;
+}
+
+static gboolean
+set_bluetooth_type(NmCli *           nmc,
+                   NMConnection *    con,
+                   const OptionInfo *option,
+                   const char *      value,
+                   GError **         error)
+{
+    NMSetting *setting;
+
+    if (!value)
+        return TRUE;
+
+    /* 'dun' type requires adding 'gsm' or 'cdma' setting */
+    if (!strcmp(value, NM_SETTING_BLUETOOTH_TYPE_DUN)
+        || !strcmp(value, NM_SETTING_BLUETOOTH_TYPE_DUN "-gsm")) {
+        value   = NM_SETTING_BLUETOOTH_TYPE_DUN;
+        setting = nm_meta_setting_info_editor_new_setting(
+            &nm_meta_setting_infos_editor[NM_META_SETTING_TYPE_GSM],
+            NM_META_ACCESSOR_SETTING_INIT_TYPE_CLI);
+        nm_connection_add_setting(con, setting);
+    } else if (!strcmp(value, NM_SETTING_BLUETOOTH_TYPE_DUN "-cdma")) {
+        value   = NM_SETTING_BLUETOOTH_TYPE_DUN;
+        setting = nm_setting_cdma_new();
+        nm_connection_add_setting(con, setting);
+    } else if (!strcmp(value, NM_SETTING_BLUETOOTH_TYPE_PANU)
+               || !strcmp(value, NM_SETTING_BLUETOOTH_TYPE_NAP)) {
+        /* no op */
+    } else {
+        g_set_error(error,
+                    NMCLI_ERROR,
+                    NMC_RESULT_ERROR_USER_INPUT,
+                    _("Error: 'bt-type': '%s' not valid; use [%s, %s, %s (%s), %s]."),
+                    value,
+                    NM_SETTING_BLUETOOTH_TYPE_PANU,
+                    NM_SETTING_BLUETOOTH_TYPE_NAP,
+                    NM_SETTING_BLUETOOTH_TYPE_DUN,
+                    NM_SETTING_BLUETOOTH_TYPE_DUN "-gsm",
+                    NM_SETTING_BLUETOOTH_TYPE_DUN "-cdma");
+        return FALSE;
+    }
+
+    return set_property(nmc->client,
+                        con,
+                        option->setting_info->general->setting_name,
+                        option->property,
+                        value,
+                        NM_META_ACCESSOR_MODIFIER_SET,
+                        error);
+}
+
+static gboolean
+set_ip4_address(NmCli *           nmc,
+                NMConnection *    con,
+                const OptionInfo *option,
+                const char *      value,
+                GError **         error)
+{
+    NMSettingIPConfig *s_ip4;
+
+    if (!value)
+        return TRUE;
+
+    s_ip4 = nm_connection_get_setting_ip4_config(con);
+    if (!s_ip4) {
+        s_ip4 = (NMSettingIPConfig *) nm_setting_ip4_config_new();
+        nm_connection_add_setting(con, NM_SETTING(s_ip4));
+        g_object_set(s_ip4, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP4_CONFIG_METHOD_MANUAL, NULL);
+    }
+    return set_property(nmc->client,
+                        con,
+                        option->setting_info->general->setting_name,
+                        option->property,
+                        value,
+                        NM_META_ACCESSOR_MODIFIER_ADD,
+                        error);
+}
+
+static gboolean
+set_ip6_address(NmCli *           nmc,
+                NMConnection *    con,
+                const OptionInfo *option,
+                const char *      value,
+                GError **         error)
+{
+    NMSettingIPConfig *s_ip6;
+
+    if (!value)
+        return TRUE;
+
+    s_ip6 = nm_connection_get_setting_ip6_config(con);
+    if (!s_ip6) {
+        s_ip6 = (NMSettingIPConfig *) nm_setting_ip6_config_new();
+        nm_connection_add_setting(con, NM_SETTING(s_ip6));
+        g_object_set(s_ip6, NM_SETTING_IP_CONFIG_METHOD, NM_SETTING_IP6_CONFIG_METHOD_MANUAL, NULL);
+    }
+    return set_property(nmc->client,
+                        con,
+                        option->setting_info->general->setting_name,
+                        option->property,
+                        value,
+                        NM_META_ACCESSOR_MODIFIER_ADD,
+                        error);
+}
+
+/*****************************************************************************/
+
+static const OptionInfo *
+_meta_abstract_get_option_info(const NMMetaAbstractInfo *abstract_info)
+{
+    static const OptionInfo option_info[] = {
+#define OPTION_INFO(name, property_name_, property_alias_, check_and_set_, generator_func_) \
+    {                                                                                       \
+        .setting_info   = &nm_meta_setting_infos_editor[NM_META_SETTING_TYPE_##name],       \
+        .property       = property_name_,                                                   \
+        .option         = property_alias_,                                                  \
+        .check_and_set  = check_and_set_,                                                   \
+        .generator_func = generator_func_,                                                  \
+    }
+        OPTION_INFO(CONNECTION, NM_SETTING_CONNECTION_TYPE, "type", set_connection_type, NULL),
+        OPTION_INFO(CONNECTION,
+                    NM_SETTING_CONNECTION_INTERFACE_NAME,
+                    "ifname",
+                    set_connection_iface,
+                    NULL),
+        OPTION_INFO(CONNECTION,
+                    NM_SETTING_CONNECTION_MASTER,
+                    "master",
+                    set_connection_master,
+                    NULL),
+        OPTION_INFO(BLUETOOTH,
+                    NM_SETTING_BLUETOOTH_TYPE,
+                    "bt-type",
+                    set_bluetooth_type,
+                    gen_func_bt_type),
+        OPTION_INFO(BOND, NM_SETTING_BOND_OPTIONS, "mode", set_bond_option, gen_func_bond_mode),
+        OPTION_INFO(BOND,
+                    NM_SETTING_BOND_OPTIONS,
+                    "primary",
+                    set_bond_option,
+                    nmc_rl_gen_func_ifnames),
+        OPTION_INFO(BOND,
+                    NM_SETTING_BOND_OPTIONS,
+                    NULL,
+                    set_bond_monitoring_mode,
+                    gen_func_bond_mon_mode),
+        OPTION_INFO(BOND, NM_SETTING_BOND_OPTIONS, "miimon", set_bond_option, NULL),
+        OPTION_INFO(BOND, NM_SETTING_BOND_OPTIONS, "downdelay", set_bond_option, NULL),
+        OPTION_INFO(BOND, NM_SETTING_BOND_OPTIONS, "updelay", set_bond_option, NULL),
+        OPTION_INFO(BOND, NM_SETTING_BOND_OPTIONS, "arp-interval", set_bond_option, NULL),
+        OPTION_INFO(BOND, NM_SETTING_BOND_OPTIONS, "arp-ip-target", set_bond_option, NULL),
+        OPTION_INFO(BOND,
+                    NM_SETTING_BOND_OPTIONS,
+                    "lacp-rate",
+                    set_bond_option,
+                    gen_func_bond_lacp_rate),
+        OPTION_INFO(IP4_CONFIG, NM_SETTING_IP_CONFIG_ADDRESSES, "ip4", set_ip4_address, NULL),
+        OPTION_INFO(IP6_CONFIG, NM_SETTING_IP_CONFIG_ADDRESSES, "ip6", set_ip6_address, NULL),
+        {0},
+    };
+    const char *                   property_name, *option;
+    const NMMetaSettingInfoEditor *setting_info;
+    const OptionInfo *             candidate;
+
+    _meta_abstract_get(abstract_info,
+                       &setting_info,
+                       NULL,
+                       &property_name,
+                       &option,
+                       NULL,
+                       NULL,
+                       NULL);
+
+    for (candidate = option_info; candidate->setting_info; candidate++) {
+        if (candidate->setting_info == setting_info && nm_streq0(candidate->property, property_name)
+            && nm_streq0(candidate->option, option)) {
+            return candidate;
+        }
+    }
+    return NULL;
+}
+
+static gboolean
+option_relevant(NMConnection *connection, const NMMetaAbstractInfo *abstract_info)
+{
+    const char *           setting_name;
+    NMMetaPropertyInfFlags inf_flags;
+
+    _meta_abstract_get(abstract_info, NULL, &setting_name, NULL, NULL, &inf_flags, NULL, NULL);
+
+    if ((inf_flags & NM_META_PROPERTY_INF_FLAG_DONT_ASK)
+        && !(_dynamic_options_get(abstract_info) & PROPERTY_INF_FLAG_ENABLED))
+        return FALSE;
+    if (_dynamic_options_get(abstract_info) & PROPERTY_INF_FLAG_DISABLED)
+        return FALSE;
+    if (!nm_connection_get_setting_by_name(connection, setting_name))
+        return FALSE;
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+complete_property_name(NmCli *                nmc,
+                       NMConnection *         connection,
+                       NMMetaAccessorModifier modifier,
+                       const char *           prefix,
+                       const char *           postfix)
+{
+    NMSettingConnection *                    s_con;
+    const NMMetaSettingValidPartItem *const *valid_settings_main;
+    const NMMetaSettingValidPartItem *const *valid_settings_slave;
+    const char *                             connection_type = NULL;
+    const char *                             slave_type      = NULL;
+    gs_free char *                           word_list       = NULL;
+    NMMetaSettingType                        s;
+
+    connection_type = nm_connection_get_connection_type(connection);
+    s_con           = nm_connection_get_setting_connection(connection);
+    if (s_con)
+        slave_type = nm_setting_connection_get_slave_type(s_con);
+    valid_settings_main  = get_valid_settings_array(connection_type);
+    valid_settings_slave = nm_meta_setting_info_valid_parts_for_slave_type(slave_type, NULL);
+
+    word_list = get_valid_properties_string(valid_settings_main,
+                                            valid_settings_slave,
+                                            modifier,
+                                            prefix,
+                                            postfix);
+    if (word_list)
+        g_print("%s", word_list);
+
+    if (modifier != NM_META_ACCESSOR_MODIFIER_SET)
+        return;
+
+    for (s = 0; s < _NM_META_SETTING_TYPE_NUM; s++) {
+        const NMMetaPropertyInfo *const *property_infos;
+        guint                            p;
+
+        if (!nm_connection_get_setting_by_name(
+                connection,
+                nm_meta_setting_infos_editor[s].general->setting_name))
+            continue;
+
+        property_infos = nm_meta_setting_infos_editor[s].properties;
+        if (!property_infos)
+            continue;
+        for (p = 0; property_infos[p]; p++) {
+            const NMMetaPropertyInfo *property_info = property_infos[p];
+
+            if (_meta_property_needs_bond_hack(property_info)) {
+                guint i;
+
+                for (i = 0; i < nm_meta_property_typ_data_bond.nested_len; i++) {
+                    const NMMetaNestedPropertyInfo *bi = &nm_meta_property_typ_data_bond.nested[i];
+
+                    if (!bi->base.property_alias
+                        || !g_str_has_prefix(bi->base.property_alias, prefix))
+                        continue;
+                    g_print("%s\n", bi->base.property_alias);
+                }
+            } else {
+                if (!property_info->is_cli_option)
+                    continue;
+                if (!property_info->property_alias
+                    || !g_str_has_prefix(property_info->property_alias, prefix))
+                    continue;
+                g_print("%s\n", property_info->property_alias);
+            }
+        }
+    }
+}
+
+static void
+run_rl_generator(rl_compentry_func_t *generator_func, const char *prefix)
+{
+    int   state = 0;
+    char *str;
+
+    while ((str = generator_func(prefix, state))) {
+        g_print("%s\n", str);
+        g_free(str);
+        if (state == 0)
+            state = 1;
+    }
+}
+
+static gboolean
+complete_option(NmCli *                   nmc,
+                const NMMetaAbstractInfo *abstract_info,
+                const char *              prefix,
+                NMConnection *            context_connection)
+{
+    const OptionInfo *           candidate;
+    const char *const *          values;
+    gs_strfreev char **          values_to_free    = NULL;
+    gboolean                     complete_filename = FALSE;
+    const NMMetaOperationContext ctx               = {
+        .connection = context_connection,
+    };
+
+    values = nm_meta_abstract_info_complete(abstract_info,
+                                            nmc_meta_environment,
+                                            (gpointer) nmc_meta_environment_arg,
+                                            &ctx,
+                                            prefix,
+                                            &complete_filename,
+                                            &values_to_free);
+    if (complete_filename) {
+        nmc->return_value = NMC_RESULT_COMPLETE_FILE;
+        return TRUE;
+    }
+    if (values) {
+        for (; values[0]; values++)
+            g_print("%s\n", values[0]);
+        return TRUE;
+    }
+
+    candidate = _meta_abstract_get_option_info(abstract_info);
+    if (candidate && candidate->generator_func) {
+        run_rl_generator(candidate->generator_func, prefix);
+        return TRUE;
+    }
+
+    return FALSE;
+}
+
+static void
+complete_existing_setting(NmCli *nmc, NMConnection *connection, const char *prefix)
+{
+    gs_free NMSetting **           settings = NULL;
+    const NMMetaSettingInfoEditor *editor;
+    guint                          i;
+
+    settings = nm_connection_get_settings(connection, NULL);
+    for (i = 0; settings && settings[i]; i++) {
+        editor = nm_meta_setting_info_editor_find_by_setting(settings[i]);
+
+        if (!prefix || g_str_has_prefix(editor->general->setting_name, prefix))
+            g_print("%s\n", editor->general->setting_name);
+
+        if (editor->alias) {
+            if (!prefix || g_str_has_prefix(editor->alias, prefix))
+                g_print("%s\n", editor->alias);
+        }
+    }
+}
+
+static void
+complete_property(NmCli *       nmc,
+                  const char *  setting_name,
+                  const char *  property,
+                  const char *  prefix,
+                  NMConnection *connection)
+{
+    const NMMetaPropertyInfo *property_info;
+
+    property_info = nm_meta_property_info_find_by_name(setting_name, property);
+    if (property_info)
+        complete_option(nmc, (const NMMetaAbstractInfo *) property_info, prefix, connection);
+}
+
+/*****************************************************************************/
+
+static gboolean
+connection_remove_setting(NMConnection *connection, NMSetting *setting, GError **error)
+{
+    gboolean mandatory;
+
+    g_return_val_if_fail(setting, FALSE);
+
+    mandatory = is_setting_mandatory(connection, setting);
+    if (!mandatory) {
+        nm_connection_remove_setting(connection, G_OBJECT_TYPE(setting));
+        return TRUE;
+    }
+    g_set_error(error,
+                NMCLI_ERROR,
+                NMC_RESULT_ERROR_USER_INPUT,
+                _("Error: setting '%s' is mandatory and cannot be removed."),
+                nm_setting_get_name(setting));
+    return FALSE;
+}
+
+static gboolean
+get_value(const char **       value,
+          int *               argc,
+          const char *const **argv,
+          const char *        option,
+          GError **           error)
+{
+    if (!**argv) {
+        g_set_error(error,
+                    NMCLI_ERROR,
+                    NMC_RESULT_ERROR_USER_INPUT,
+                    _("Error: value for '%s' is missing."),
+                    option);
+        return FALSE;
+    }
+
+    /* Empty string will reset the value to default */
+    if (**argv[0] == '\0')
+        *value = NULL;
+    else
+        *value = *argv[0];
+
+    (*argc)--;
+    (*argv)++;
+    return TRUE;
+}
+
+gboolean
+nmc_process_connection_properties(NmCli *             nmc,
+                                  NMConnection *      connection,
+                                  int *               argc,
+                                  const char *const **argv,
+                                  gboolean            allow_setting_removal,
+                                  GError **           error)
+{
+    /* First check if we have a slave-type, as this would mean we will not
+     * have ip properties but possibly others, slave-type specific.
+     */
+    /* Go through arguments and set properties */
+    do {
+        const NMMetaSettingValidPartItem *const *type_settings;
+        const NMMetaSettingValidPartItem *const *slv_settings;
+        NMMetaAccessorModifier                   modifier;
+        const char *                             option_orig;
+        const char *                             option;
+        const char *                             value = NULL;
+        const char *                             tmp;
+        const NMMetaAbstractInfo *               chosen              = NULL;
+        const char *                             chosen_setting_name = NULL;
+        const char *                             chosen_option       = NULL;
+        NMMetaSettingType                        s;
+
+        if (!con_settings(connection, &type_settings, &slv_settings, error))
+            return FALSE;
+
+        ensure_settings(connection, slv_settings);
+        ensure_settings(connection, type_settings);
+
+        if (*argc <= 0) {
+            g_set_error_literal(error,
+                                NMCLI_ERROR,
+                                NMC_RESULT_ERROR_USER_INPUT,
+                                _("Error: <setting>.<property> argument is missing."));
+            return FALSE;
+        }
+
+        nm_assert(argv);
+        nm_assert(*argv);
+        nm_assert(**argv);
+
+        option_orig = **argv;
+
+        switch (option_orig[0]) {
+        case '+':
+            modifier = NM_META_ACCESSOR_MODIFIER_ADD;
+            option   = &option_orig[1];
+            break;
+        case '-':
+            modifier = NM_META_ACCESSOR_MODIFIER_DEL;
+            option   = &option_orig[1];
+            break;
+        default:
+            modifier = NM_META_ACCESSOR_MODIFIER_SET;
+            option   = option_orig;
+            break;
+        }
+
+        if (allow_setting_removal && modifier == NM_META_ACCESSOR_MODIFIER_SET
+            && nm_streq(option, "remove")) {
+            NMSetting * ss;
+            const char *setting_name;
+
+            (*argc)--;
+            (*argv)++;
+
+            if (*argc == 1 && nmc->complete) {
+                complete_existing_setting(nmc, connection, value);
+                return TRUE;
+            }
+
+            if (!*argc) {
+                g_set_error_literal(error,
+                                    NMCLI_ERROR,
+                                    NMC_RESULT_ERROR_USER_INPUT,
+                                    _("Error: missing setting."));
+                return FALSE;
+            }
+
+            setting_name = **argv;
+            (*argc)--;
+            (*argv)++;
+
+            ss = is_setting_valid(connection, type_settings, slv_settings, setting_name);
+            if (!ss) {
+                if (!check_valid_name(setting_name, type_settings, slv_settings, NULL)) {
+                    g_set_error(error,
+                                NMCLI_ERROR,
+                                NMC_RESULT_ERROR_USER_INPUT,
+                                _("Error: invalid setting argument '%s'."),
+                                setting_name);
+                    return FALSE;
+                }
+                continue;
+            }
+
+            if (!connection_remove_setting(connection, ss, error))
+                return FALSE;
+
+            continue;
+        }
+
+        if ((tmp = strchr(option, '.'))) {
+            gs_free char *option_sett = g_strndup(option, tmp - option);
+            const char *  option_prop = &tmp[1];
+            const char *  option_sett_expanded;
+            GError *      local = NULL;
+
+            /* This seems like a <setting>.<property> (such as "connection.id" or "bond.mode"),
+             * optionally prefixed with "+| or "-". */
+
+            if (*argc == 1 && nmc->complete)
+                complete_property_name(nmc, connection, modifier, option_sett, option_prop);
+
+            option_sett_expanded =
+                check_valid_name(option_sett, type_settings, slv_settings, &local);
+            if (!option_sett_expanded) {
+                g_set_error(error,
+                            NMCLI_ERROR,
+                            NMC_RESULT_ERROR_USER_INPUT,
+                            _("Error: invalid or not allowed setting '%s': %s."),
+                            option_sett,
+                            local->message);
+                g_clear_error(&local);
+                return FALSE;
+            }
+
+            (*argc)--;
+            (*argv)++;
+            if (!get_value(&value, argc, argv, option_orig, error))
+                return FALSE;
+
+            if (!*argc && nmc->complete) {
+                complete_property(nmc, option_sett, option_prop, value ?: "", connection);
+                return TRUE;
+            }
+
+            if (!set_property(nmc->client,
+                              connection,
+                              option_sett_expanded,
+                              option_prop,
+                              value,
+                              modifier,
+                              error))
+                return FALSE;
+
+            continue;
+        }
+
+        /* Let's see if this is an property alias (such as "id", "mode", "type" or "con-name")*/
+        for (s = 0; s < _NM_META_SETTING_TYPE_NUM; s++) {
+            const NMMetaPropertyInfo *const *property_infos;
+            guint                            p;
+
+            if (!check_valid_name(nm_meta_setting_infos[s].setting_name,
+                                  type_settings,
+                                  slv_settings,
+                                  NULL))
+                continue;
+
+            property_infos = nm_meta_setting_infos_editor[s].properties;
+            if (!property_infos)
+                continue;
+            for (p = 0; property_infos[p]; p++) {
+                const NMMetaPropertyInfo *property_info = property_infos[p];
+
+                if (_meta_property_needs_bond_hack(property_info)) {
+                    guint i;
+
+                    for (i = 0; i < nm_meta_property_typ_data_bond.nested_len; i++) {
+                        const NMMetaNestedPropertyInfo *bi =
+                            &nm_meta_property_typ_data_bond.nested[i];
+
+                        if (!nm_streq0(bi->base.property_alias, option))
+                            continue;
+                        if (chosen) {
+                            g_set_error(error,
+                                        NMCLI_ERROR,
+                                        NMC_RESULT_ERROR_USER_INPUT,
+                                        _("Error: '%s' is ambiguous (%s.%s or %s.%s)."),
+                                        option,
+                                        chosen_setting_name,
+                                        chosen_option,
+                                        nm_meta_setting_infos[s].setting_name,
+                                        option);
+                            return FALSE;
+                        }
+                        chosen_setting_name = nm_meta_setting_infos[s].setting_name;
+                        chosen_option       = option;
+                        chosen              = (const NMMetaAbstractInfo *) bi;
+                    }
+                } else {
+                    if (!property_info->is_cli_option)
+                        continue;
+                    if (!nm_streq0(property_info->property_alias, option))
+                        continue;
+                    if (chosen) {
+                        g_set_error(error,
+                                    NMCLI_ERROR,
+                                    NMC_RESULT_ERROR_USER_INPUT,
+                                    _("Error: '%s' is ambiguous (%s.%s or %s.%s)."),
+                                    option,
+                                    chosen_setting_name,
+                                    chosen_option,
+                                    nm_meta_setting_infos[s].setting_name,
+                                    option);
+                        return FALSE;
+                    }
+                    chosen_setting_name = nm_meta_setting_infos[s].setting_name;
+                    chosen_option       = option;
+                    chosen              = (const NMMetaAbstractInfo *) property_info;
+                }
+            }
+        }
+
+        if (!chosen) {
+            if (*argc == 1 && nmc->complete) {
+                if (allow_setting_removal && g_str_has_prefix("remove", option))
+                    g_print("remove\n");
+                complete_property_name(nmc, connection, modifier, option, NULL);
+            }
+            g_set_error(error,
+                        NMCLI_ERROR,
+                        NMC_RESULT_ERROR_USER_INPUT,
+                        _("Error: invalid <setting>.<property> '%s'."),
+                        option);
+            return FALSE;
+        }
+
+        if (*argc == 1 && nmc->complete)
+            complete_property_name(nmc, connection, modifier, option, NULL);
+
+        (*argc)--;
+        (*argv)++;
+        if (!get_value(&value, argc, argv, option_orig, error))
+            return FALSE;
+
+        if (!*argc && nmc->complete)
+            complete_option(nmc, chosen, value ?: "", connection);
+
+        if (!set_option(nmc, connection, chosen, value, error))
+            return FALSE;
+
+    } while (*argc);
+
+    return TRUE;
+}
+
+static void
+add_connection_cb(GObject *client, GAsyncResult *result, gpointer user_data)
+{
+    nm_auto_free_add_connection_info AddConnectionInfo *info = user_data;
+    NmCli *                                             nmc  = info->nmc;
+    NMRemoteConnection *                                connection;
+    GError *                                            error = NULL;
+    const GPtrArray *                                   connections;
+    guint                                               i, found;
+
+    connection = nm_client_add_connection2_finish(NM_CLIENT(client), result, NULL, &error);
+    if (error) {
+        g_string_printf(nmc->return_text,
+                        _("Error: Failed to add '%s' connection: %s"),
+                        info->new_id,
+                        error->message);
+        g_error_free(error);
+        nmc->return_value = NMC_RESULT_ERROR_CON_ACTIVATION;
+    } else {
+        connections = nm_client_get_connections(nmc->client);
+        if (connections) {
+            found = 0;
+            for (i = 0; i < connections->len; i++) {
+                NMConnection *candidate = NM_CONNECTION(connections->pdata[i]);
+
+                if ((NMConnection *) connection == candidate)
+                    continue;
+                if (nm_streq0(nm_connection_get_id(candidate), info->new_id))
+                    found++;
+            }
+            if (found > 0) {
+                g_printerr(g_dngettext(GETTEXT_PACKAGE,
+                                       "Warning: There is another connection with the name '%1$s'. "
+                                       "Reference the connection by its uuid '%2$s'\n",
+                                       "Warning: There are %3$u other connections with the name "
+                                       "'%1$s'. Reference the connection by its uuid '%2$s'\n",
+                                       found),
+                           info->new_id,
+                           nm_connection_get_uuid(NM_CONNECTION(connection)),
+                           found);
+            }
+        }
+
+        /* We print here human readable text, but as scripts might parse this output
+         * (with LANG=C), this is important to not change in the future. At least
+         * not unless called with a new command line flag, that requests a different output.
+         *
+         * That means, be very careful if you change this message, it might break
+         * scripts!!
+         *
+         * This is true for many messages that the user might parse. But this one
+         * seems in particular interesting for a user to parse. */
+        g_print(_("Connection '%s' (%s) successfully added.\n"),
+                nm_connection_get_id(NM_CONNECTION(connection)),
+                nm_connection_get_uuid(NM_CONNECTION(connection)));
+        g_object_unref(connection);
+    }
+
+    quit();
+}
+
+static void
+add_connection(NMClient *          client,
+               NMConnection *      connection,
+               gboolean            temporary,
+               GAsyncReadyCallback callback,
+               gpointer            user_data)
+{
+    nm_client_add_connection2(client,
+                              nm_connection_to_dbus(connection, NM_CONNECTION_SERIALIZE_ALL),
+                              temporary ? NM_SETTINGS_ADD_CONNECTION2_FLAG_IN_MEMORY
+                                        : NM_SETTINGS_ADD_CONNECTION2_FLAG_TO_DISK,
+                              NULL,
+                              TRUE,
+                              NULL,
+                              callback,
+                              user_data);
+}
+
+static void
+update_connection(NMRemoteConnection *connection,
+                  gboolean            temporary,
+                  GAsyncReadyCallback callback,
+                  gpointer            user_data)
+{
+    nm_remote_connection_commit_changes_async(connection, !temporary, NULL, callback, user_data);
+}
+
+static gboolean
+is_single_word(const char *line)
+{
+    size_t n1, n2, n3;
+
+    n1 = strspn(line, " \t");
+    n2 = strcspn(line + n1, " \t\0") + n1;
+    n3 = strspn(line + n2, " \t");
+
+    if (n3 == 0)
+        return TRUE;
+    else
+        return FALSE;
+}
+
+static char **
+nmcli_con_add_tab_completion(const char *text, int start, int end)
+{
+    NMMetaSettingType         s;
+    char **                   match_array    = NULL;
+    rl_compentry_func_t *     generator_func = NULL;
+    gs_free char *            no             = g_strdup_printf("[%s]: ", _("no"));
+    gs_free char *            yes            = g_strdup_printf("[%s]: ", _("yes"));
+    const NMMetaAbstractInfo *info;
+
+    /* Disable readline's default filename completion */
+    rl_attempted_completion_over = 1;
+
+    /* Restore standard append character to space */
+    rl_completion_append_character = '\x00';
+
+    if (!is_single_word(rl_line_buffer))
+        return NULL;
+
+    for (s = 0; s < _NM_META_SETTING_TYPE_NUM; s++) {
+        const NMMetaPropertyInfo *const *property_infos;
+        guint                            p;
+
+        property_infos = nm_meta_setting_infos_editor[s].properties;
+        if (!property_infos)
+            continue;
+        for (p = 0; property_infos[p]; p++) {
+            const NMMetaPropertyInfo *property_info = property_infos[p];
+
+            if (_meta_property_needs_bond_hack(property_info)) {
+                guint i;
+
+                for (i = 0; i < nm_meta_property_typ_data_bond.nested_len; i++) {
+                    const NMMetaNestedPropertyInfo *bi = &nm_meta_property_typ_data_bond.nested[i];
+
+                    if (bi->base.prompt && g_str_has_prefix(rl_prompt, bi->base.prompt)) {
+                        goto next;
+                    }
+                }
+            } else {
+                if (property_info->prompt && g_str_has_prefix(rl_prompt, property_info->prompt)) {
+                    info                     = (const NMMetaAbstractInfo *) property_info;
+                    nmc_tab_completion.words = _meta_abstract_complete(info, text);
+                    if (nmc_tab_completion.words) {
+                        match_array = rl_completion_matches(text, _meta_abstract_generator);
+                        nm_clear_pointer(&nmc_tab_completion.words, g_strfreev);
+                    }
+                    return match_array;
+                }
+            }
+        }
+    }
+
+next:
+    if (g_str_has_prefix(rl_prompt, NM_META_TEXT_PROMPT_BT_TYPE))
+        generator_func = gen_func_bt_type;
+    else if (g_str_has_prefix(rl_prompt, NM_META_TEXT_PROMPT_BOND_MODE))
+        generator_func = gen_func_bond_mode;
+    else if (g_str_has_prefix(rl_prompt, NM_META_TEXT_PROMPT_BOND_MON_MODE))
+        generator_func = gen_func_bond_mon_mode;
+    else if (g_str_has_suffix(rl_prompt, yes) || g_str_has_suffix(rl_prompt, no))
+        generator_func = gen_func_bool_values_l10n;
+
+    if (generator_func)
+        match_array = rl_completion_matches(text, generator_func);
+
+    return match_array;
+}
+
+static void
+ask_option(NmCli *nmc, NMConnection *connection, const NMMetaAbstractInfo *abstract_info)
+{
+    char *                 value;
+    GError *               error  = NULL;
+    gs_free char *         prompt = NULL;
+    gboolean               multi;
+    const char *           opt_prompt, *opt_def_hint;
+    NMMetaPropertyInfFlags inf_flags;
+
+    _meta_abstract_get(abstract_info,
+                       NULL,
+                       NULL,
+                       NULL,
+                       NULL,
+                       &inf_flags,
+                       &opt_prompt,
+                       &opt_def_hint);
+    prompt =
+        g_strjoin("", gettext(opt_prompt), opt_def_hint ? " " : "", opt_def_hint ?: "", ": ", NULL);
+
+    multi = NM_FLAGS_HAS(inf_flags, NM_META_PROPERTY_INF_FLAG_MULTI);
+
+    if (multi)
+        g_print(_("You can specify this option more than once. Press <Enter> when you're done.\n"));
+
+again:
+    value = nmc_readline(&nmc->nmc_config, "%s", prompt);
+    if (multi && !value)
+        return;
+
+    if (!set_option(nmc, connection, abstract_info, value, &error)) {
+        g_printerr("%s\n", error->message);
+        g_clear_error(&error);
+        goto again;
+    }
+
+    if (multi && value)
+        goto again;
+}
+
+static NMMetaSettingType
+connection_get_base_meta_setting_type(NMConnection *connection)
+{
+    const char *                   connection_type;
+    NMSetting *                    base_setting;
+    const NMMetaSettingInfoEditor *editor;
+
+    connection_type = nm_connection_get_connection_type(connection);
+    nm_assert(connection_type);
+    base_setting = nm_connection_get_setting_by_name(connection, connection_type);
+    nm_assert(base_setting);
+    editor = nm_meta_setting_info_editor_find_by_setting(base_setting);
+    nm_assert(editor);
+
+    return editor - nm_meta_setting_infos_editor;
+}
+
+static void
+questionnaire_mandatory_ask_setting(NmCli *nmc, NMConnection *connection, NMMetaSettingType type)
+{
+    const NMMetaSettingInfoEditor *editor;
+    const NMMetaPropertyInfo *     property_info;
+    guint                          p;
+
+    editor = &nm_meta_setting_infos_editor[type];
+    if (!editor->properties)
+        return;
+
+    for (p = 0; editor->properties[p]; p++) {
+        property_info = editor->properties[p];
+
+        if (_meta_property_needs_bond_hack(property_info)) {
+            guint i;
+
+            for (i = 0; i < nm_meta_property_typ_data_bond.nested_len; i++) {
+                const NMMetaNestedPropertyInfo *bi = &nm_meta_property_typ_data_bond.nested[i];
+
+                if (!option_relevant(connection, (const NMMetaAbstractInfo *) bi))
+                    continue;
+                if ((bi->base.inf_flags & NM_META_PROPERTY_INF_FLAG_REQD)
+                    || (_dynamic_options_get((const NMMetaAbstractInfo *) bi)
+                        & PROPERTY_INF_FLAG_ENABLED))
+                    ask_option(nmc, connection, (const NMMetaAbstractInfo *) bi);
+            }
+        } else {
+            if (!property_info->is_cli_option)
+                continue;
+
+            if (!option_relevant(connection, (const NMMetaAbstractInfo *) property_info))
+                continue;
+            if ((property_info->inf_flags & NM_META_PROPERTY_INF_FLAG_REQD)
+                || (_dynamic_options_get((const NMMetaAbstractInfo *) property_info)
+                    & PROPERTY_INF_FLAG_ENABLED))
+                ask_option(nmc, connection, (const NMMetaAbstractInfo *) property_info);
+        }
+    }
+}
+
+static void
+questionnaire_mandatory(NmCli *nmc, NMConnection *connection)
+{
+    NMMetaSettingType s, base;
+
+    /* First ask connection properties */
+    questionnaire_mandatory_ask_setting(nmc, connection, NM_META_SETTING_TYPE_CONNECTION);
+
+    /* Ask properties of the base setting */
+    base = connection_get_base_meta_setting_type(connection);
+    questionnaire_mandatory_ask_setting(nmc, connection, base);
+
+    /* Remaining settings */
+    for (s = 0; s < _NM_META_SETTING_TYPE_NUM; s++) {
+        if (!NM_IN_SET(s, NM_META_SETTING_TYPE_CONNECTION, base))
+            questionnaire_mandatory_ask_setting(nmc, connection, s);
+    }
+}
+
+static gboolean
+want_provide_opt_args(const NmcConfig *nmc_config, const char *type, guint num)
+{
+    gs_free char *answer = NULL;
+
+    /* Ask for optional arguments. */
+    g_print(ngettext("There is %d optional setting for %s.\n",
+                     "There are %d optional settings for %s.\n",
+                     num),
+            (int) num,
+            type);
+    answer = nmc_readline(
+        nmc_config,
+        ngettext("Do you want to provide it? %s", "Do you want to provide them? %s", num),
+        prompt_yes_no(TRUE, NULL));
+    nm_strstrip(answer);
+    return !answer || matches(answer, WORD_YES);
+}
+
+static gboolean
+questionnaire_one_optional(NmCli *nmc, NMConnection *connection)
+{
+    NMMetaSettingType base;
+    gs_unref_ptrarray GPtrArray *infos = NULL;
+    guint                        i, j;
+    gboolean                     already_confirmed = FALSE;
+    NMMetaSettingType            s_asking          = NM_META_SETTING_TYPE_UNKNOWN;
+    NMMetaSettingType            settings[_NM_META_SETTING_TYPE_NUM];
+
+    base = connection_get_base_meta_setting_type(connection);
+
+    i             = 0;
+    settings[i++] = NM_META_SETTING_TYPE_CONNECTION;
+    settings[i++] = base;
+    for (j = 0; j < _NM_META_SETTING_TYPE_NUM; j++) {
+        if (!NM_IN_SET(j, NM_META_SETTING_TYPE_CONNECTION, base))
+            settings[i++] = j;
+    }
+
+    infos = g_ptr_array_new();
+
+    /* Find first setting with relevant options and count them. */
+again:
+    for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++) {
+        const NMMetaPropertyInfo *const *property_infos;
+        guint                            p;
+
+        if (s_asking != NM_META_SETTING_TYPE_UNKNOWN && settings[i] != s_asking)
+            continue;
+
+        property_infos = nm_meta_setting_infos_editor[settings[i]].properties;
+        if (!property_infos)
+            continue;
+        for (p = 0; property_infos[p]; p++) {
+            const NMMetaPropertyInfo *property_info = property_infos[p];
+
+            if (_meta_property_needs_bond_hack(property_info)) {
+                for (j = 0; j < nm_meta_property_typ_data_bond.nested_len; j++) {
+                    const NMMetaNestedPropertyInfo *bi = &nm_meta_property_typ_data_bond.nested[j];
+
+                    if (!option_relevant(connection, (const NMMetaAbstractInfo *) bi))
+                        continue;
+                    g_ptr_array_add(infos, (gpointer) bi);
+                }
+            } else {
+                if (!property_info->is_cli_option)
+                    continue;
+                if (!option_relevant(connection, (const NMMetaAbstractInfo *) property_info))
+                    continue;
+                g_ptr_array_add(infos, (gpointer) property_info);
+            }
+        }
+        if (infos->len) {
+            s_asking = settings[i];
+            break;
+        }
+    }
+
+    if (infos->len) {
+        const NMMetaSettingInfoEditor *setting_info = NULL;
+
+        _meta_abstract_get(infos->pdata[0], &setting_info, NULL, NULL, NULL, NULL, NULL, NULL);
+
+        /* Now ask for the settings. */
+        if (already_confirmed
+            || want_provide_opt_args(&nmc->nmc_config, _(setting_info->pretty_name), infos->len)) {
+            ask_option(nmc, connection, infos->pdata[0]);
+            already_confirmed = TRUE;
+            /* asking for an option may enable other options. Create the list again. */
+            g_ptr_array_set_size(infos, 0);
+            goto again;
+        }
+    }
+
+    if (s_asking == NM_META_SETTING_TYPE_UNKNOWN)
+        return FALSE;
+
+    /* Make sure we won't ask again. */
+    disable_options(nm_meta_setting_infos[s_asking].setting_name, NULL);
+    return TRUE;
+}
+
+static void
+do_connection_add(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    gs_unref_object NMConnection *connection = NULL;
+    NMSettingConnection *         s_con;
+    gs_free_error GError *error          = NULL;
+    gboolean              save_bool      = TRUE;
+    gboolean              seen_dash_dash = FALSE;
+    NMMetaSettingType     s;
+
+    next_arg(nmc, &argc, &argv, NULL);
+
+    rl_attempted_completion_function = nmcli_con_add_tab_completion;
+
+    nmc->return_value = NMC_RESULT_SUCCESS;
+
+    connection = nm_simple_connection_new();
+
+    s_con = (NMSettingConnection *) nm_setting_connection_new();
+    nm_connection_add_setting(connection, NM_SETTING(s_con));
+
+read_properties:
+    g_clear_error(&error);
+    /* Get the arguments from the command line if any */
+    if (argc && !nmc_process_connection_properties(nmc, connection, &argc, &argv, FALSE, &error)) {
+        if (g_strcmp0(*argv, "--") == 0 && !seen_dash_dash) {
+            /* This is for compatibility with older nmcli that required
+             * options and properties to be separated with "--" */
+            seen_dash_dash = TRUE;
+            next_arg(nmc, &argc, &argv, NULL);
+            goto read_properties;
+        } else if (g_strcmp0(*argv, "save") == 0) {
+            /* It would be better if "save" was a separate argument and not
+             * mixed with properties, but there's not much we can do about it now. */
+            argc--;
+            argv++;
+            if (!argc) {
+                g_string_printf(nmc->return_text,
+                                _("Error: value for '%s' argument is required."),
+                                "save");
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                goto finish;
+            }
+            g_clear_error(&error);
+            if (!nmc_string_to_bool(*argv, &save_bool, &error)) {
+                g_string_printf(nmc->return_text, _("Error: 'save': %s."), error->message);
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                goto finish;
+            }
+            next_arg(nmc, &argc, &argv, NULL);
+            goto read_properties;
+        }
+
+        g_string_assign(nmc->return_text, error->message);
+        nmc->return_value = error->code;
+        goto finish;
+    }
+
+    if (nmc->complete)
+        goto finish;
+
+    /* Now ask user for the rest of the mandatory options. */
+    if (nmc->ask)
+        questionnaire_mandatory(nmc, connection);
+
+    /* Traditionally, we didn't ask for these options for ethernet slaves. They don't
+     * make much sense, since these are likely to be set by the master anyway. */
+    if (nm_setting_connection_get_slave_type(s_con)) {
+        disable_options(NM_SETTING_WIRED_SETTING_NAME, NM_SETTING_WIRED_MTU);
+        disable_options(NM_SETTING_WIRED_SETTING_NAME, NM_SETTING_WIRED_MAC_ADDRESS);
+        disable_options(NM_SETTING_WIRED_SETTING_NAME, NM_SETTING_WIRED_CLONED_MAC_ADDRESS);
+    }
+
+    /* Connection id is special in that it's required but we don't insist
+     * on getting it from the user -- we just make up something sensible. */
+    if (!nm_setting_connection_get_id(s_con)) {
+        const char *ifname     = nm_setting_connection_get_interface_name(s_con);
+        const char *type       = nm_setting_connection_get_connection_type(s_con);
+        const char *slave_type = nm_setting_connection_get_slave_type(s_con);
+
+        /* If only bother when there's a type, which is not guaranteed at this point.
+         * Otherwise, the validation will fail anyway. */
+        if (type) {
+            gs_free char *   try_name     = NULL;
+            gs_free char *   default_name = NULL;
+            const GPtrArray *connections;
+
+            connections = nm_client_get_connections(nmc->client);
+            try_name =
+                ifname ? g_strdup_printf("%s-%s", get_name_alias_toplevel(type, slave_type), ifname)
+                       : g_strdup(get_name_alias_toplevel(type, slave_type));
+            default_name = nmc_unique_connection_name(connections, try_name);
+            g_object_set(s_con, NM_SETTING_CONNECTION_ID, default_name, NULL);
+        }
+    }
+
+    /* For some software connection types we generate the interface name for the user. */
+    set_default_interface_name(nmc, s_con);
+
+    /* Now see if there's something optional that needs to be asked for.
+     * Keep asking until there's no more things to ask for. */
+    do {
+        /* This ensures all settings that make sense are present. */
+        nm_connection_normalize(connection, NULL, NULL, NULL);
+    } while (nmc->ask && questionnaire_one_optional(nmc, connection));
+
+    /* Mandatory settings. No good reason to check this other than guarding the user
+     * from doing something that's not likely to make sense (such as missing ifname
+     * on a bond/bridge/team, etc.). Added just to preserve traditional behavior, it
+     * perhaps is a good idea to just remove this. */
+    for (s = 0; s < _NM_META_SETTING_TYPE_NUM; s++) {
+        const NMMetaPropertyInfo *const *property_infos;
+        guint                            p;
+
+        property_infos = nm_meta_setting_infos_editor[s].properties;
+        if (!property_infos)
+            continue;
+        for (p = 0; property_infos[p]; p++) {
+            const NMMetaPropertyInfo *property_info = property_infos[p];
+
+            if (_meta_property_needs_bond_hack(property_info)) {
+                guint i;
+
+                for (i = 0; i < nm_meta_property_typ_data_bond.nested_len; i++) {
+                    const NMMetaNestedPropertyInfo *bi = &nm_meta_property_typ_data_bond.nested[i];
+
+                    if (!option_relevant(connection, (const NMMetaAbstractInfo *) bi))
+                        continue;
+                    if (bi->base.inf_flags & NM_META_PROPERTY_INF_FLAG_REQD) {
+                        g_string_printf(nmc->return_text,
+                                        _("Error: '%s' argument is required."),
+                                        bi->base.property_alias);
+                        nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                        goto finish;
+                    }
+                }
+            } else {
+                if (!property_info->is_cli_option)
+                    continue;
+                if (!option_relevant(connection, (const NMMetaAbstractInfo *) property_info))
+                    continue;
+                if (property_info->inf_flags & NM_META_PROPERTY_INF_FLAG_REQD) {
+                    g_string_printf(nmc->return_text,
+                                    _("Error: '%s' argument is required."),
+                                    property_info->property_alias);
+                    nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                    goto finish;
+                }
+            }
+        }
+    }
+
+    add_connection(nmc->client,
+                   connection,
+                   !save_bool,
+                   add_connection_cb,
+                   _add_connection_info_new(nmc, NULL, connection));
+    nmc->should_wait++;
+
+finish:
+    reset_options();
+}
+
+/*****************************************************************************/
+/* Functions for readline TAB completion in editor */
+
+static void
+uuid_display_hook(char **array, int len, int max_len)
+{
+    const GPtrArray *connections;
+    NMConnection *   con;
+    int              i, max = 0;
+    char *           tmp;
+    const char *     id;
+    for (i = 1; i <= len; i++) {
+        connections = nm_client_get_connections(nmc_tab_completion.nmc->client);
+        con         = nmc_find_connection(connections, "uuid", array[i], NULL, FALSE);
+        id          = con ? nm_connection_get_id(con) : NULL;
+        if (id) {
+            tmp = g_strdup_printf("%s (%s)", array[i], id);
+            g_free(array[i]);
+            array[i] = tmp;
+            if (max < strlen(id))
+                max = strlen(id);
+        }
+    }
+    rl_display_match_list(array, len, max_len + max + 3);
+    rl_forced_update_display();
+}
+
+static char *
+gen_nmcli_cmds_menu(const char *text, int state)
+{
+    const char *words[] = {"goto",
+                           "set",
+                           "remove",
+                           "describe",
+                           "print",
+                           "verify",
+                           "save",
+                           "activate",
+                           "back",
+                           "help",
+                           "quit",
+                           "nmcli",
+                           NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+
+static char *
+gen_nmcli_cmds_submenu(const char *text, int state)
+{
+    const char *words[] =
+        {"set", "add", "change", "remove", "describe", "print", "back", "help", "quit", NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+
+static char *
+gen_cmd_nmcli(const char *text, int state)
+{
+    const char *words[] = {"status-line", "save-confirmation", "show-secrets", NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+
+static char *
+gen_func_bool_values(const char *text, int state)
+{
+    const char *words[] = {"yes", "no", NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+
+static char *
+gen_cmd_verify0(const char *text, int state)
+{
+    const char *words[] = {"all", "fix", NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+
+static char *
+gen_cmd_print0(const char *text, int state)
+{
+    static char **words = NULL;
+    char *        ret   = NULL;
+
+    if (!state) {
+        GVariant *   settings;
+        GVariantIter iter;
+        const char * setting_name;
+        int          i = 0;
+
+        settings = nm_connection_to_dbus(nmc_tab_completion.connection,
+                                         NM_CONNECTION_SERIALIZE_WITH_NON_SECRET);
+        words    = g_new(char *, g_variant_n_children(settings) + 2);
+        g_variant_iter_init(&iter, settings);
+        while (g_variant_iter_next(&iter, "{&s@a{sv}}", &setting_name, NULL))
+            words[i++] = g_strdup(setting_name);
+        words[i++] = g_strdup("all");
+        words[i]   = NULL;
+        g_variant_unref(settings);
+    }
+
+    if (words) {
+        ret = nmc_rl_gen_func_basic(text, state, (const char **) words);
+        if (ret == NULL) {
+            g_strfreev(words);
+            words = NULL;
+        }
+    }
+    return ret;
+}
+
+static char *
+gen_cmd_print2(const char *text, int state)
+{
+    const char *words[] = {"setting", "connection", "all", NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+
+static char *
+gen_cmd_save(const char *text, int state)
+{
+    const char *words[] = {"persistent", "temporary", NULL};
+    return nmc_rl_gen_func_basic(text, state, words);
+}
+
+static rl_compentry_func_t *
+gen_connection_types(const char *text)
+{
+    gs_free char **                values = NULL;
+    const NMMetaSettingInfoEditor *editor;
+    GPtrArray *                    array;
+    int                            i;
+
+    array = g_ptr_array_new();
+
+    for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++) {
+        editor = &nm_meta_setting_infos_editor[i];
+        if (!editor->valid_parts)
+            continue;
+        g_ptr_array_add(array, (gpointer) nm_meta_setting_infos[i].setting_name);
+        if (editor->alias)
+            g_ptr_array_add(array, (gpointer) editor->alias);
+    }
+
+    g_ptr_array_add(array, "bond-slave");
+    g_ptr_array_add(array, "bridge-slave");
+    g_ptr_array_add(array, "team-slave");
+    g_ptr_array_add(array, NULL);
+
+    values = (char **) g_ptr_array_free(array, FALSE);
+
+    return nmc_rl_compentry_func_wrap((const char *const *) values);
+}
+
+static char *
+gen_setting_names(const char *text, int state)
+{
+    static int                               list_idx, len, is_slv;
+    const char *                             s_name, *a_name;
+    const NMMetaSettingValidPartItem *const *valid_settings_arr;
+    NMSettingConnection *                    s_con;
+    const char *                             s_type = NULL;
+
+    if (!state) {
+        list_idx = 0;
+        len      = strlen(text);
+        is_slv   = 0;
+    }
+
+    if (!is_slv) {
+        valid_settings_arr = get_valid_settings_array(nmc_tab_completion.con_type);
+        if (list_idx >= NM_PTRARRAY_LEN(valid_settings_arr))
+            return NULL;
+        for (; valid_settings_arr[list_idx];) {
+            const NMMetaSettingInfoEditor *setting_info =
+                valid_settings_arr[list_idx]->setting_info;
+
+            a_name = setting_info->alias;
+            s_name = setting_info->general->setting_name;
+            list_idx++;
+            if (len == 0 && a_name)
+                return g_strdup_printf("%s (%s)", s_name, a_name);
+            if (a_name && !strncmp(text, a_name, len))
+                return g_strdup(a_name);
+            if (s_name && !strncmp(text, s_name, len))
+                return g_strdup(s_name);
+        }
+
+        /* Let's give a try to parameters related to slave type */
+        list_idx = 0;
+        is_slv   = 1;
+    }
+
+    /* is_slv */
+    s_con = nm_connection_get_setting_connection(nmc_tab_completion.connection);
+    if (s_con)
+        s_type = nm_setting_connection_get_slave_type(s_con);
+    valid_settings_arr = nm_meta_setting_info_valid_parts_for_slave_type(s_type, NULL);
+
+    if (list_idx < NM_PTRARRAY_LEN(valid_settings_arr)) {
+        while (valid_settings_arr[list_idx]) {
+            const NMMetaSettingInfoEditor *setting_info =
+                valid_settings_arr[list_idx]->setting_info;
+
+            a_name = setting_info->alias;
+            s_name = setting_info->general->setting_name;
+            list_idx++;
+            if (len == 0 && a_name)
+                return g_strdup_printf("%s (%s)", s_name, a_name);
+            if (a_name && !strncmp(text, a_name, len))
+                return g_strdup(a_name);
+            if (s_name && !strncmp(text, s_name, len))
+                return g_strdup(s_name);
+        }
+    }
+
+    return NULL;
+}
+
+static char *
+gen_property_names(const char *text, int state)
+{
+    NMSetting *                              setting     = NULL;
+    char **                                  valid_props = NULL;
+    char *                                   ret         = NULL;
+    const char *                             line        = rl_line_buffer;
+    const char *                             setting_name;
+    char **                                  strv = NULL;
+    const NMMetaSettingValidPartItem *const *valid_settings_main;
+    const NMMetaSettingValidPartItem *const *valid_settings_slave;
+    const char *                             p1;
+    const char *                             slv_type;
+
+    /* Try to get the setting from 'line' - setting_name.property */
+    p1 = strchr(line, '.');
+    if (p1) {
+        while (p1 > line && !g_ascii_isspace(*p1))
+            p1--;
+
+        strv = g_strsplit(p1 + 1, ".", 2);
+
+        valid_settings_main = get_valid_settings_array(nmc_tab_completion.con_type);
+
+        /* Support autocompletion of slave-connection parameters
+         * guessing the slave type from the setting name already
+         * typed (or autocompleted) */
+        if (nm_streq0(strv[0], NM_SETTING_TEAM_PORT_SETTING_NAME))
+            slv_type = NM_SETTING_TEAM_SETTING_NAME;
+        else if (nm_streq0(strv[0], NM_SETTING_BRIDGE_PORT_SETTING_NAME))
+            slv_type = NM_SETTING_BRIDGE_SETTING_NAME;
+        else
+            slv_type = NULL;
+        valid_settings_slave = nm_meta_setting_info_valid_parts_for_slave_type(slv_type, NULL);
+
+        setting_name = check_valid_name(strv[0], valid_settings_main, valid_settings_slave, NULL);
+        if (setting_name) {
+            setting = nm_meta_setting_info_editor_new_setting(
+                nm_meta_setting_info_editor_find_by_name(setting_name, FALSE),
+                NM_META_ACCESSOR_SETTING_INIT_TYPE_DEFAULT);
+        }
+    }
+
+    if (!setting) {
+        /* Else take the current setting, if any */
+        setting = nmc_tab_completion.setting ? g_object_ref(nmc_tab_completion.setting) : NULL;
+    }
+
+    if (setting) {
+        valid_props = nmc_setting_get_valid_properties(setting);
+        ret         = nmc_rl_gen_func_basic(text, state, (const char **) valid_props);
+    }
+
+    g_strfreev(strv);
+    g_strfreev(valid_props);
+    if (setting)
+        g_object_unref(setting);
+    return ret;
+}
+
+static char *
+gen_compat_devices(const char *text, int state)
+{
+    guint            i, j = 0;
+    const GPtrArray *devices;
+    const char **    compatible_devices;
+    char *           ret;
+
+    devices = nm_client_get_devices(nmc_tab_completion.nmc->client);
+    if (devices->len == 0)
+        return NULL;
+
+    compatible_devices = g_new(const char *, devices->len + 1);
+    for (i = 0; i < devices->len; i++) {
+        NMDevice *  dev         = g_ptr_array_index(devices, i);
+        const char *ifname      = nm_device_get_iface(dev);
+        NMDevice *  device      = NULL;
+        const char *spec_object = NULL;
+
+        if (find_device_for_connection(nmc_tab_completion.nmc,
+                                       nmc_tab_completion.connection,
+                                       ifname,
+                                       NULL,
+                                       NULL,
+                                       &device,
+                                       &spec_object,
+                                       NULL)) {
+            compatible_devices[j++] = ifname;
+        }
+    }
+    compatible_devices[j] = NULL;
+
+    ret = nmc_rl_gen_func_basic(text, state, compatible_devices);
+
+    g_free(compatible_devices);
+    return ret;
+}
+
+static const char **
+_create_vpn_array(const GPtrArray *connections, gboolean uuid)
+{
+    int          c, idx = 0;
+    const char **array;
+
+    if (connections->len < 1)
+        return NULL;
+
+    array = g_new(const char *, connections->len + 1);
+    for (c = 0; c < connections->len; c++) {
+        NMConnection *connection = NM_CONNECTION(connections->pdata[c]);
+        const char *  type       = nm_connection_get_connection_type(connection);
+
+        if (g_strcmp0(type, NM_SETTING_VPN_SETTING_NAME) == 0)
+            array[idx++] =
+                uuid ? nm_connection_get_uuid(connection) : nm_connection_get_id(connection);
+    }
+    array[idx] = NULL;
+    return array;
+}
+
+static char *
+gen_vpn_uuids(const char *text, int state)
+{
+    const GPtrArray *connections;
+    const char **    uuids;
+    char *           ret;
+
+    connections = nm_client_get_connections(nm_cli_global_readline->client);
+    if (connections->len < 1)
+        return NULL;
+
+    uuids = _create_vpn_array(connections, TRUE);
+    ret   = nmc_rl_gen_func_basic(text, state, uuids);
+    g_free(uuids);
+    return ret;
+}
+
+static char *
+gen_vpn_ids(const char *text, int state)
+{
+    const GPtrArray *connections;
+    const char **    ids;
+    char *           ret;
+
+    connections = nm_client_get_connections(nm_cli_global_readline->client);
+    if (connections->len < 1)
+        return NULL;
+
+    ids = _create_vpn_array(connections, FALSE);
+    ret = nmc_rl_gen_func_basic(text, state, ids);
+    g_free(ids);
+    return ret;
+}
+
+static rl_compentry_func_t *
+get_gen_func_cmd_nmcli(const char *str)
+{
+    if (!str)
+        return NULL;
+    if (matches(str, "status-line"))
+        return gen_func_bool_values;
+    if (matches(str, "save-confirmation"))
+        return gen_func_bool_values;
+    if (matches(str, "show-secrets"))
+        return gen_func_bool_values;
+    return NULL;
+}
+
+/*
+ * Helper function parsing line for completion.
+ * IN:
+ *   line : the whole line to be parsed
+ *   end  : the position of cursor in the line
+ *   cmd  : command to match
+ * OUT:
+ *   cw_num    : is set to the word number being completed (1, 2, 3, 4).
+ *   prev_word : returns the previous word (so that we have some context).
+ *
+ * Returns TRUE when the first word of the 'line' matches 'cmd'.
+ *
+ * Examples:
+ * line="rem"              cmd="remove"   -> TRUE  cw_num=1
+ * line="set con"          cmd="set"      -> TRUE  cw_num=2
+ * line="go ipv4.method"   cmd="goto"     -> TRUE  cw_num=2
+ * line="  des eth.mtu "   cmd="describe" -> TRUE  cw_num=3
+ * line=" bla ipv4.method" cmd="goto"     -> FALSE
+ */
+static gboolean
+should_complete_cmd(const char *line, int end, const char *cmd, int *cw_num, char **prev_word)
+{
+    char *      tmp;
+    const char *word1, *word2, *word3;
+    size_t      n1, n2, n3, n4, n5, n6;
+    gboolean    word1_done, word2_done, word3_done;
+    gboolean    ret = FALSE;
+
+    if (!line)
+        return FALSE;
+
+    tmp = g_strdup(line);
+
+    n1 = strspn(tmp, " \t");
+    n2 = strcspn(tmp + n1, " \t\0") + n1;
+    n3 = strspn(tmp + n2, " \t") + n2;
+    n4 = strcspn(tmp + n3, " \t\0") + n3;
+    n5 = strspn(tmp + n4, " \t") + n4;
+    n6 = strcspn(tmp + n5, " \t\0") + n5;
+
+    word1_done = end > n2;
+    word2_done = end > n4;
+    word3_done = end > n6;
+    tmp[n2] = tmp[n4] = tmp[n6] = '\0';
+
+    word1 = tmp[n1] ? tmp + n1 : NULL;
+    word2 = tmp[n3] ? tmp + n3 : NULL;
+    word3 = tmp[n5] ? tmp + n5 : NULL;
+
+    if (!word1_done) {
+        if (cw_num)
+            *cw_num = 1;
+        if (prev_word)
+            *prev_word = NULL;
+    } else if (!word2_done) {
+        if (cw_num)
+            *cw_num = 2;
+        if (prev_word)
+            *prev_word = g_strdup(word1);
+    } else if (!word3_done) {
+        if (cw_num)
+            *cw_num = 3;
+        if (prev_word)
+            *prev_word = g_strdup(word2);
+    } else {
+        if (cw_num)
+            *cw_num = 4;
+        if (prev_word)
+            *prev_word = g_strdup(word3);
+    }
+
+    if (word1 && matches(word1, cmd))
+        ret = TRUE;
+
+    g_free(tmp);
+    return ret;
+}
+
+/*
+ * extract_setting_and_property:
+ * prompt: (in) (allow-none): prompt string, or NULL
+ * line: (in) (allow-none): line, or NULL
+ * setting: (out) (transfer full) (array zero-terminated=1):
+ *   return location for setting name
+ * property: (out) (transfer full) (array zero-terminated=1):
+ *   return location for property name
+ *
+ * Extract setting and property names from prompt and/or line.
+ */
+static void
+extract_setting_and_property(const char *prompt, const char *line, char **setting, char **property)
+{
+    char *prop = NULL;
+    char *sett = NULL;
+
+    if (prompt) {
+        /* prompt looks like this:
+         * "nmcli 802-1x>" or "nmcli 802-1x.pac-file>" */
+        const char *p1, *p2, *dot;
+        size_t      num1, num2;
+        p1 = strchr(prompt, ' ');
+        if (p1) {
+            dot = strchr(++p1, '.');
+            if (dot) {
+                p2   = dot + 1;
+                num1 = strcspn(p1, ".");
+                num2 = strcspn(p2, ">");
+                sett = num1 > 0 ? g_strndup(p1, num1) : NULL;
+                prop = num2 > 0 ? g_strndup(p2, num2) : NULL;
+            } else {
+                num1 = strcspn(p1, ">");
+                sett = num1 > 0 ? g_strndup(p1, num1) : NULL;
+            }
+        }
+    }
+
+    if (line) {
+        /* line looks like this:
+         * " set 802-1x.pac-file ..." or " set pac-file ..." */
+        const char *p1, *p2, *dot;
+        size_t      n1, n2, n3, n4;
+        size_t      num1, num2, len;
+        n1  = strspn(line, " \t");              /* white-space */
+        n2  = strcspn(line + n1, " \t\0") + n1; /* command */
+        n3  = strspn(line + n2, " \t") + n2;    /* white-space */
+        n4  = strcspn(line + n3, " \t\0") + n3; /* setting/property */
+        p1  = line + n3;
+        len = n4 - n3;
+
+        dot = strchr(p1, '.');
+        if (dot && dot < p1 + len) {
+            p2   = dot + 1;
+            num1 = strcspn(p1, ".");
+            num2 = len > num1 + 1 ? len - num1 - 1 : 0;
+            sett = num1 > 0 ? g_strndup(p1, num1) : sett;
+            prop = num2 > 0 ? g_strndup(p2, num2) : prop;
+        } else {
+            if (!prop)
+                prop = len > 0 ? g_strndup(p1, len) : NULL;
+        }
+    }
+
+    if (setting)
+        *setting = sett;
+    else
+        g_free(sett);
+    if (property)
+        *property = prop;
+    else
+        g_free(prop);
+}
+
+static void
+get_setting_and_property(const char *prompt,
+                         const char *line,
+                         NMSetting **setting_out,
+                         char **     property_out)
+{
+    const NMMetaSettingValidPartItem *const *valid_settings_main;
+    const NMMetaSettingValidPartItem *const *valid_settings_slave;
+    gs_unref_object NMSetting *setting  = NULL;
+    gs_free char *             property = NULL;
+    NMSettingConnection *      s_con;
+    gs_free char *             sett   = NULL;
+    gs_free char *             prop   = NULL;
+    const char *               s_type = NULL;
+    const char *               setting_name;
+
+    extract_setting_and_property(prompt, line, &sett, &prop);
+
+    if (sett) {
+        /* Is this too much (and useless?) effort for an unlikely case? */
+        s_con = nm_connection_get_setting_connection(nmc_tab_completion.connection);
+        if (s_con)
+            s_type = nm_setting_connection_get_slave_type(s_con);
+
+        valid_settings_main  = get_valid_settings_array(nmc_tab_completion.con_type);
+        valid_settings_slave = nm_meta_setting_info_valid_parts_for_slave_type(s_type, NULL);
+
+        setting_name = check_valid_name(sett, valid_settings_main, valid_settings_slave, NULL);
+        setting      = nm_meta_setting_info_editor_new_setting(
+            nm_meta_setting_info_editor_find_by_name(setting_name, FALSE),
+            NM_META_ACCESSOR_SETTING_INIT_TYPE_DEFAULT);
+    } else
+        setting = nm_g_object_ref(nmc_tab_completion.setting);
+
+    if (setting && prop)
+        property = is_property_valid(setting, prop, NULL);
+    else
+        property = g_strdup(nmc_tab_completion.property);
+
+    *setting_out  = g_steal_pointer(&setting);
+    *property_out = g_steal_pointer(&property);
+}
+
+static gboolean
+_get_and_check_property(const char * prompt,
+                        const char * line,
+                        const char **array,
+                        const char **array_multi,
+                        gboolean *   multi)
+{
+    gs_free char *prop  = NULL;
+    gboolean      found = FALSE;
+
+    extract_setting_and_property(prompt, line, NULL, &prop);
+    if (prop) {
+        if (array)
+            found = !!nmc_string_is_valid(prop, array, NULL);
+        if (array_multi && multi)
+            *multi = !!nmc_string_is_valid(prop, array_multi, NULL);
+    }
+    return found;
+}
+
+static gboolean
+should_complete_files(const char *prompt, const char *line)
+{
+    const char *file_properties[] = {/* '802-1x' properties */
+                                     "ca-cert",
+                                     "ca-path",
+                                     "client-cert",
+                                     "pac-file",
+                                     "phase2-ca-cert",
+                                     "phase2-ca-path",
+                                     "phase2-client-cert",
+                                     "private-key",
+                                     "phase2-private-key",
+                                     /* 'team' and 'team-port' properties */
+                                     "config",
+                                     /* 'proxy' properties */
+                                     "pac-script",
+                                     NULL};
+    return _get_and_check_property(prompt, line, file_properties, NULL, NULL);
+}
+
+static gboolean
+should_complete_vpn_uuids(const char *prompt, const char *line)
+{
+    const char *uuid_properties[] = {/* 'connection' properties */
+                                     "secondaries",
+                                     NULL};
+    return _get_and_check_property(prompt, line, uuid_properties, NULL, NULL);
+}
+
+static const char *const *
+get_allowed_property_values(char ***out_to_free)
+{
+    gs_unref_object NMSetting *setting  = NULL;
+    gs_free char *             property = NULL;
+    const char *const *        avals    = NULL;
+
+    get_setting_and_property(rl_prompt, rl_line_buffer, &setting, &property);
+    if (setting && property)
+        avals = nmc_setting_get_property_allowed_values(setting, property, out_to_free);
+    return avals;
+}
+
+static gboolean
+should_complete_property_values(const char *prompt, const char *line, gboolean *multi)
+{
+    gs_strfreev char **to_free = NULL;
+
+    /* properties allowing multiple values */
+    const char *multi_props[] = {/* '802-1x' properties */
+                                 NM_SETTING_802_1X_EAP,
+                                 /* '802-11-wireless-security' properties */
+                                 NM_SETTING_WIRELESS_SECURITY_PROTO,
+                                 NM_SETTING_WIRELESS_SECURITY_PAIRWISE,
+                                 NM_SETTING_WIRELESS_SECURITY_GROUP,
+                                 /* 'bond' properties */
+                                 NM_SETTING_BOND_OPTIONS,
+                                 /* 'ethernet' properties */
+                                 NM_SETTING_WIRED_S390_OPTIONS,
+                                 NULL};
+    _get_and_check_property(prompt, line, NULL, multi_props, multi);
+    return !!get_allowed_property_values(&to_free);
+}
+
+static gboolean
+_setting_property_is_boolean(NMSetting *setting, const char *property_name)
+{
+    const GParamSpec *pspec;
+
+    nm_assert(NM_IS_SETTING(setting));
+    nm_assert(property_name);
+
+    pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(setting), property_name);
+    return pspec && pspec->value_type == G_TYPE_BOOLEAN;
+}
+
+static gboolean
+should_complete_boolean(const char *prompt, const char *line)
+{
+    gs_unref_object NMSetting *setting  = NULL;
+    gs_free char *             property = NULL;
+
+    get_setting_and_property(prompt, line, &setting, &property);
+    return setting && property && _setting_property_is_boolean(setting, property);
+}
+
+static char *
+gen_property_values(const char *text, int state)
+{
+    gs_strfreev char **to_free = NULL;
+    const char *const *avals;
+
+    avals = get_allowed_property_values(&to_free);
+    if (!avals)
+        return NULL;
+    return nmc_rl_gen_func_basic(text, state, avals);
+}
+
+/* from readline */
+extern int rl_complete_with_tilde_expansion;
+
+/*
+ * Attempt to complete on the contents of TEXT.  START and END show the
+ * region of TEXT that contains the word to complete.  We can use the
+ * entire line in case we want to do some simple parsing.  Return the
+ * array of matches, or NULL if there aren't any.
+ */
+static char **
+nmcli_editor_tab_completion(const char *text, int start, int end)
+{
+    rl_compentry_func_t *generator_func = NULL;
+    const char *         line           = rl_line_buffer;
+    gs_free char *       prompt_tmp     = NULL;
+    gs_free char *       word           = NULL;
+    char **              match_array    = NULL;
+    size_t               n1;
+    int                  num;
+
+    /* Restore standard append character to space */
+    rl_completion_append_character = ' ';
+
+    /* Restore standard function for displaying matches */
+    rl_completion_display_matches_hook = NULL;
+
+    /* Disable default filename completion */
+    rl_attempted_completion_over = 1;
+
+    /* Enable tilde expansion when filenames are completed */
+    rl_complete_with_tilde_expansion = 1;
+
+    /* Filter out possible ANSI color escape sequences */
+    prompt_tmp = nmc_filter_out_colors((const char *) rl_prompt);
+
+    /* Find the first non-space character */
+    n1 = strspn(line, " \t");
+
+    /* Choose the right generator function */
+    if (strcmp(prompt_tmp, EDITOR_PROMPT_CON_TYPE) == 0)
+        generator_func = gen_connection_types(text);
+    else if (strcmp(prompt_tmp, EDITOR_PROMPT_SETTING) == 0)
+        generator_func = gen_setting_names;
+    else if (strcmp(prompt_tmp, EDITOR_PROMPT_PROPERTY) == 0)
+        generator_func = gen_property_names;
+    else if (g_str_has_suffix(rl_prompt, prompt_yes_no(TRUE, NULL))
+             || g_str_has_suffix(rl_prompt, prompt_yes_no(FALSE, NULL)))
+        generator_func = gen_func_bool_values_l10n;
+    else if (g_str_has_prefix(prompt_tmp, "nmcli")) {
+        if (!strchr(prompt_tmp, '.')) {
+            int         level = g_str_has_prefix(prompt_tmp, "nmcli>") ? 0 : 1;
+            const char *dot   = strchr(line, '.');
+            gboolean    multi;
+
+            /* Main menu  - level 0,1 */
+            if (start == n1)
+                generator_func = gen_nmcli_cmds_menu;
+            else {
+                if (should_complete_cmd(line, end, "goto", &num, NULL) && num <= 2) {
+                    if (level == 0 && (!dot || dot >= line + end))
+                        generator_func = gen_setting_names;
+                    else
+                        generator_func = gen_property_names;
+                } else if (should_complete_cmd(line, end, "set", &num, NULL)) {
+                    if (num < 3) {
+                        if (level == 0 && (!dot || dot >= line + end)) {
+                            generator_func                 = gen_setting_names;
+                            rl_completion_append_character = '.';
+                        } else
+                            generator_func = gen_property_names;
+                    } else {
+                        if (num == 3 && should_complete_files(NULL, line))
+                            rl_attempted_completion_over = 0;
+                        else if (should_complete_vpn_uuids(NULL, line)) {
+                            rl_completion_display_matches_hook = uuid_display_hook;
+                            generator_func                     = gen_vpn_uuids;
+                        } else if (should_complete_property_values(NULL, line, &multi)
+                                   && (num == 3 || multi)) {
+                            generator_func = gen_property_values;
+                        } else if (should_complete_boolean(NULL, line) && num == 3)
+                            generator_func = gen_func_bool_values;
+                    }
+                } else if ((should_complete_cmd(line, end, "remove", &num, NULL)
+                            || should_complete_cmd(line, end, "describe", &num, NULL))
+                           && num <= 2) {
+                    if (level == 0 && (!dot || dot >= line + end)) {
+                        generator_func                 = gen_setting_names;
+                        rl_completion_append_character = '.';
+                    } else
+                        generator_func = gen_property_names;
+                } else if (should_complete_cmd(line, end, "nmcli", &num, &word)) {
+                    if (num < 3)
+                        generator_func = gen_cmd_nmcli;
+                    else if (num == 3)
+                        generator_func = get_gen_func_cmd_nmcli(word);
+                } else if (should_complete_cmd(line, end, "print", &num, NULL) && num <= 2) {
+                    if (level == 0 && (!dot || dot >= line + end))
+                        generator_func = gen_cmd_print0;
+                    else
+                        generator_func = gen_property_names;
+                } else if (should_complete_cmd(line, end, "verify", &num, NULL) && num <= 2) {
+                    generator_func = gen_cmd_verify0;
+                } else if (should_complete_cmd(line, end, "activate", &num, NULL) && num <= 2) {
+                    generator_func = gen_compat_devices;
+                } else if (should_complete_cmd(line, end, "save", &num, NULL) && num <= 2) {
+                    generator_func = gen_cmd_save;
+                } else if (should_complete_cmd(line, end, "help", &num, NULL) && num <= 2)
+                    generator_func = gen_nmcli_cmds_menu;
+            }
+        } else {
+            /* Submenu - level 2 */
+            if (start == n1)
+                generator_func = gen_nmcli_cmds_submenu;
+            else {
+                gboolean multi;
+
+                if (should_complete_cmd(line, end, "add", &num, NULL)
+                    || should_complete_cmd(line, end, "set", &num, NULL)) {
+                    if (num <= 2 && should_complete_files(prompt_tmp, line))
+                        rl_attempted_completion_over = 0;
+                    else if (should_complete_vpn_uuids(prompt_tmp, line)) {
+                        rl_completion_display_matches_hook = uuid_display_hook;
+                        generator_func                     = gen_vpn_uuids;
+                    } else if (should_complete_property_values(prompt_tmp, NULL, &multi)
+                               && (num <= 2 || multi)) {
+                        generator_func = gen_property_values;
+                    } else if (should_complete_boolean(prompt_tmp, NULL) && num <= 2)
+                        generator_func = gen_func_bool_values;
+                }
+                if (should_complete_cmd(line, end, "print", &num, NULL) && num <= 2)
+                    generator_func = gen_cmd_print2;
+                else if (should_complete_cmd(line, end, "help", &num, NULL) && num <= 2)
+                    generator_func = gen_nmcli_cmds_submenu;
+            }
+        }
+    }
+
+    if (generator_func)
+        match_array = rl_completion_matches(text, generator_func);
+
+    return match_array;
+}
+
+#define NMCLI_EDITOR_HISTORY ".nmcli-history"
+
+static void
+load_history_cmds(const char *uuid)
+{
+    GKeyFile *kf;
+    char *    filename;
+    char **   keys;
+    char *    line;
+    size_t    i;
+    GError *  err = NULL;
+
+    filename = g_build_filename(g_get_home_dir(), NMCLI_EDITOR_HISTORY, NULL);
+    kf       = g_key_file_new();
+    if (!g_key_file_load_from_file(kf, filename, G_KEY_FILE_KEEP_COMMENTS, &err)) {
+        if (g_error_matches(err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_PARSE))
+            g_print("Warning: %s parse error: %s\n", filename, err->message);
+        g_key_file_free(kf);
+        g_free(filename);
+        return;
+    }
+    keys = g_key_file_get_keys(kf, uuid, NULL, NULL);
+    for (i = 0; keys && keys[i]; i++) {
+        line = g_key_file_get_string(kf, uuid, keys[i], NULL);
+        if (line && *line)
+            add_history(line);
+        g_free(line);
+    }
+    g_strfreev(keys);
+    g_key_file_free(kf);
+    g_free(filename);
+}
+
+static void
+save_history_cmds(const char *uuid)
+{
+    nm_auto_unref_keyfile GKeyFile *kf = NULL;
+    gs_free_error GError *error        = NULL;
+    gs_free char *        filename     = NULL;
+    gs_free char *        data         = NULL;
+    HIST_ENTRY **         hist;
+    gsize                 len;
+    gsize                 i;
+
+    hist = history_list();
+    if (!hist)
+        return;
+
+    filename = g_build_filename(g_get_home_dir(), NMCLI_EDITOR_HISTORY, NULL);
+
+    kf = g_key_file_new();
+
+    if (!g_key_file_load_from_file(kf, filename, G_KEY_FILE_KEEP_COMMENTS, &error)) {
+        if (!g_error_matches(error, G_FILE_ERROR, G_FILE_ERROR_NOENT)
+            && !g_error_matches(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_NOT_FOUND)) {
+            g_print("Warning: %s parse error: %s\n", filename, error->message);
+            return;
+        }
+        g_clear_error(&error);
+    }
+
+    /* Remove previous history group and save new history entries */
+    g_key_file_remove_group(kf, uuid, NULL);
+    for (i = 0; hist[i]; i++) {
+        char key[100];
+
+        nm_sprintf_buf(key, "%zd", i);
+        g_key_file_set_string(kf, uuid, key, hist[i]->line);
+    }
+
+    /* Write history to file */
+    data = g_key_file_to_data(kf, &len, NULL);
+    if (data)
+        g_file_set_contents(filename, data, len, NULL);
+}
+
+/*****************************************************************************/
+
+static void
+editor_show_connection(NMConnection *connection, NmCli *nmc)
+{
+    nmc->nmc_config_mutable.print_output     = NMC_PRINT_PRETTY;
+    nmc->nmc_config_mutable.multiline_output = TRUE;
+    nmc->nmc_config_mutable.escape_values    = 0;
+
+    nmc_connection_profile_details(connection, nmc);
+}
+
+static void
+editor_show_setting(NMSetting *setting, NmCli *nmc)
+{
+    g_print(_("['%s' setting values]\n"), nm_setting_get_name(setting));
+
+    nmc->nmc_config_mutable.print_output     = NMC_PRINT_NORMAL;
+    nmc->nmc_config_mutable.multiline_output = TRUE;
+    nmc->nmc_config_mutable.escape_values    = 0;
+
+    setting_details(&nmc->nmc_config, setting, NULL);
+}
+
+typedef enum {
+    NMC_EDITOR_MAIN_CMD_UNKNOWN = 0,
+    NMC_EDITOR_MAIN_CMD_GOTO,
+    NMC_EDITOR_MAIN_CMD_REMOVE,
+    NMC_EDITOR_MAIN_CMD_SET,
+    NMC_EDITOR_MAIN_CMD_DESCRIBE,
+    NMC_EDITOR_MAIN_CMD_PRINT,
+    NMC_EDITOR_MAIN_CMD_VERIFY,
+    NMC_EDITOR_MAIN_CMD_SAVE,
+    NMC_EDITOR_MAIN_CMD_ACTIVATE,
+    NMC_EDITOR_MAIN_CMD_BACK,
+    NMC_EDITOR_MAIN_CMD_HELP,
+    NMC_EDITOR_MAIN_CMD_NMCLI,
+    NMC_EDITOR_MAIN_CMD_QUIT,
+} NmcEditorMainCmd;
+
+static void
+_split_cmd(const char *cmd, char **out_arg0, const char **out_argr)
+{
+    gs_free char *arg0 = NULL;
+    const char *  argr = NULL;
+    gsize         l;
+
+    NM_SET_OUT(out_arg0, NULL);
+    NM_SET_OUT(out_argr, NULL);
+
+    if (!cmd)
+        return;
+    while (nm_utils_is_separator(cmd[0]))
+        cmd++;
+    if (!cmd[0])
+        return;
+
+    l    = strcspn(cmd, " \t");
+    arg0 = g_strndup(cmd, l);
+    cmd += l;
+    if (cmd[0]) {
+        while (nm_utils_is_separator(cmd[0]))
+            cmd++;
+        if (cmd[0])
+            argr = cmd;
+    }
+
+    NM_SET_OUT(out_arg0, g_steal_pointer(&arg0));
+    NM_SET_OUT(out_argr, argr);
+}
+
+static NmcEditorMainCmd
+parse_editor_main_cmd(const char *cmd, char **cmd_arg)
+{
+    NmcEditorMainCmd editor_cmd = NMC_EDITOR_MAIN_CMD_UNKNOWN;
+    gs_free char *   cmd_arg0   = NULL;
+    const char *     cmd_argr;
+
+    _split_cmd(cmd, &cmd_arg0, &cmd_argr);
+    if (!cmd_arg0)
+        goto fail;
+
+    if (matches(cmd_arg0, "goto"))
+        editor_cmd = NMC_EDITOR_MAIN_CMD_GOTO;
+    else if (matches(cmd_arg0, "remove"))
+        editor_cmd = NMC_EDITOR_MAIN_CMD_REMOVE;
+    else if (matches(cmd_arg0, "set"))
+        editor_cmd = NMC_EDITOR_MAIN_CMD_SET;
+    else if (matches(cmd_arg0, "describe"))
+        editor_cmd = NMC_EDITOR_MAIN_CMD_DESCRIBE;
+    else if (matches(cmd_arg0, "print"))
+        editor_cmd = NMC_EDITOR_MAIN_CMD_PRINT;
+    else if (matches(cmd_arg0, "verify"))
+        editor_cmd = NMC_EDITOR_MAIN_CMD_VERIFY;
+    else if (matches(cmd_arg0, "save"))
+        editor_cmd = NMC_EDITOR_MAIN_CMD_SAVE;
+    else if (matches(cmd_arg0, "activate"))
+        editor_cmd = NMC_EDITOR_MAIN_CMD_ACTIVATE;
+    else if (matches(cmd_arg0, "back"))
+        editor_cmd = NMC_EDITOR_MAIN_CMD_BACK;
+    else if (matches(cmd_arg0, "help") || strcmp(cmd_arg0, "?") == 0)
+        editor_cmd = NMC_EDITOR_MAIN_CMD_HELP;
+    else if (matches(cmd_arg0, "quit"))
+        editor_cmd = NMC_EDITOR_MAIN_CMD_QUIT;
+    else if (matches(cmd_arg0, "nmcli"))
+        editor_cmd = NMC_EDITOR_MAIN_CMD_NMCLI;
+    else
+        goto fail;
+
+    NM_SET_OUT(cmd_arg, g_strdup(cmd_argr));
+    return editor_cmd;
+fail:
+    NM_SET_OUT(cmd_arg, NULL);
+    return NMC_EDITOR_MAIN_CMD_UNKNOWN;
+}
+
+static void
+editor_main_usage(void)
+{
+    g_print("------------------------------------------------------------------------------\n");
+    /* TRANSLATORS: do not translate command names and keywords before ::
+     *              However, you should translate terms enclosed in <>.
+     */
+    g_print(_("---[ Main menu ]---\n"
+              "goto     [<setting> | <prop>]        :: go to a setting or property\n"
+              "remove   <setting>[.<prop>] | <prop> :: remove setting or reset property value\n"
+              "set      [<setting>.<prop> <value>]  :: set property value\n"
+              "describe [<setting>.<prop>]          :: describe property\n"
+              "print    [all | <setting>[.<prop>]]  :: print the connection\n"
+              "verify   [all | fix]                 :: verify the connection\n"
+              "save     [persistent|temporary]      :: save the connection\n"
+              "activate [<ifname>] [/<ap>|<nsp>]    :: activate the connection\n"
+              "back                                 :: go one level up (back)\n"
+              "help/?   [<command>]                 :: print this help\n"
+              "nmcli    <conf-option> <value>       :: nmcli configuration\n"
+              "quit                                 :: exit nmcli\n"));
+    g_print("------------------------------------------------------------------------------\n");
+}
+
+static void
+editor_main_help(const char *command)
+{
+    if (!command)
+        editor_main_usage();
+    else {
+        /* detailed command descriptions */
+        NmcEditorMainCmd cmd = parse_editor_main_cmd(command, NULL);
+
+        switch (cmd) {
+        case NMC_EDITOR_MAIN_CMD_GOTO:
+            g_print(_("goto <setting>[.<prop>] | <prop>  :: enter setting/property for editing\n\n"
+                      "This command enters into a setting or property for editing it.\n\n"
+                      "Examples: nmcli> goto connection\n"
+                      "          nmcli connection> goto secondaries\n"
+                      "          nmcli> goto ipv4.addresses\n"));
+            break;
+        case NMC_EDITOR_MAIN_CMD_REMOVE:
+            g_print(
+                _("remove <setting>[.<prop>]  :: remove setting or reset property value\n\n"
+                  "This command removes an entire setting from the connection, or if a property\n"
+                  "is given, resets that property to the default value.\n\n"
+                  "Examples: nmcli> remove wifi-sec\n"
+                  "          nmcli> remove eth.mtu\n"));
+            break;
+        case NMC_EDITOR_MAIN_CMD_SET:
+            g_print(_("set [<setting>.<prop> <value>]  :: set property value\n\n"
+                      "This command sets property value.\n\n"
+                      "Example: nmcli> set con.id My connection\n"));
+            break;
+        case NMC_EDITOR_MAIN_CMD_DESCRIBE:
+            g_print(_("describe [<setting>.<prop>]  :: describe property\n\n"
+                      "Shows property description. You can consult nm-settings(5) "
+                      "manual page to see all NM settings and properties.\n"));
+            break;
+        case NMC_EDITOR_MAIN_CMD_PRINT:
+            g_print(_("print [all]  :: print setting or connection values\n\n"
+                      "Shows current property or the whole connection.\n\n"
+                      "Example: nmcli ipv4> print all\n"));
+            break;
+        case NMC_EDITOR_MAIN_CMD_VERIFY:
+            g_print(
+                _("verify [all | fix]  :: verify setting or connection validity\n\n"
+                  "Verifies whether the setting or connection is valid and can be saved later.\n"
+                  "It indicates invalid values on error. Some errors may be fixed automatically\n"
+                  "by 'fix' option.\n\n"
+                  "Examples: nmcli> verify\n"
+                  "          nmcli> verify fix\n"
+                  "          nmcli bond> verify\n"));
+            break;
+        case NMC_EDITOR_MAIN_CMD_SAVE:
+            g_print(
+                _("save [persistent|temporary]  :: save the connection\n\n"
+                  "Sends the connection profile to NetworkManager that either will save it\n"
+                  "persistently, or will only keep it in memory. 'save' without an argument\n"
+                  "means 'save persistent'.\n"
+                  "Note that once you save the profile persistently those settings are saved\n"
+                  "across reboot or restart. Subsequent changes can also be temporary or\n"
+                  "persistent, but any temporary changes will not persist across reboot or\n"
+                  "restart. If you want to fully remove the persistent connection, the connection\n"
+                  "profile must be deleted.\n"));
+            break;
+        case NMC_EDITOR_MAIN_CMD_ACTIVATE:
+            g_print(_("activate [<ifname>] [/<ap>|<nsp>]  :: activate the connection\n\n"
+                      "Activates the connection.\n\n"
+                      "Available options:\n"
+                      "<ifname>    - device the connection will be activated on\n"
+                      "/<ap>|<nsp> - AP (Wi-Fi) or NSP (WiMAX) (prepend with / when <ifname> is "
+                      "not specified)\n"));
+            break;
+        case NMC_EDITOR_MAIN_CMD_BACK:
+            g_print(_("back  :: go to upper menu level\n\n"));
+            break;
+        case NMC_EDITOR_MAIN_CMD_HELP:
+            g_print(_("help/? [<command>]  :: help for the nmcli commands\n\n"));
+            break;
+        case NMC_EDITOR_MAIN_CMD_NMCLI:
+            g_print(_("nmcli [<conf-option> <value>]  :: nmcli configuration\n\n"
+                      "Configures nmcli. The following options are available:\n"
+                      "status-line yes | no          [default: no]\n"
+                      "save-confirmation yes | no    [default: yes]\n"
+                      "show-secrets yes | no         [default: no]\n"
+                      "prompt-color <color> | <0-8>  [default: 0]\n"
+                      "%s" /* color table description */
+                      "\n"
+                      "Examples: nmcli> nmcli status-line yes\n"
+                      "          nmcli> nmcli save-confirmation no\n"
+                      "          nmcli> nmcli prompt-color 3\n"),
+                    "  0 = normal\n"
+                    "  1 = \33[30mblack\33[0m\n"
+                    "  2 = \33[31mred\33[0m\n"
+                    "  3 = \33[32mgreen\33[0m\n"
+                    "  4 = \33[33myellow\33[0m\n"
+                    "  5 = \33[34mblue\33[0m\n"
+                    "  6 = \33[35mmagenta\33[0m\n"
+                    "  7 = \33[36mcyan\33[0m\n"
+                    "  8 = \33[37mwhite\33[0m\n");
+            break;
+        case NMC_EDITOR_MAIN_CMD_QUIT:
+            g_print(_("quit  :: exit nmcli\n\n"
+                      "This command exits nmcli. When the connection being edited "
+                      "is not saved, the user is asked to confirm the action.\n"));
+            break;
+        default:
+            g_print(_("Unknown command: '%s'\n"), command);
+            break;
+        }
+    }
+}
+
+typedef enum {
+    NMC_EDITOR_SUB_CMD_UNKNOWN = 0,
+    NMC_EDITOR_SUB_CMD_SET,
+    NMC_EDITOR_SUB_CMD_ADD,
+    NMC_EDITOR_SUB_CMD_CHANGE,
+    NMC_EDITOR_SUB_CMD_REMOVE,
+    NMC_EDITOR_SUB_CMD_DESCRIBE,
+    NMC_EDITOR_SUB_CMD_PRINT,
+    NMC_EDITOR_SUB_CMD_BACK,
+    NMC_EDITOR_SUB_CMD_HELP,
+    NMC_EDITOR_SUB_CMD_QUIT
+} NmcEditorSubCmd;
+
+static NmcEditorSubCmd
+parse_editor_sub_cmd(const char *cmd, char **cmd_arg)
+{
+    NmcEditorSubCmd editor_cmd = NMC_EDITOR_SUB_CMD_UNKNOWN;
+    gs_free char *  cmd_arg0   = NULL;
+    const char *    cmd_argr;
+
+    _split_cmd(cmd, &cmd_arg0, &cmd_argr);
+    if (!cmd_arg0)
+        goto fail;
+
+    if (matches(cmd_arg0, "set"))
+        editor_cmd = NMC_EDITOR_SUB_CMD_SET;
+    else if (matches(cmd_arg0, "add"))
+        editor_cmd = NMC_EDITOR_SUB_CMD_ADD;
+    else if (matches(cmd_arg0, "change"))
+        editor_cmd = NMC_EDITOR_SUB_CMD_CHANGE;
+    else if (matches(cmd_arg0, "remove"))
+        editor_cmd = NMC_EDITOR_SUB_CMD_REMOVE;
+    else if (matches(cmd_arg0, "describe"))
+        editor_cmd = NMC_EDITOR_SUB_CMD_DESCRIBE;
+    else if (matches(cmd_arg0, "print"))
+        editor_cmd = NMC_EDITOR_SUB_CMD_PRINT;
+    else if (matches(cmd_arg0, "back"))
+        editor_cmd = NMC_EDITOR_SUB_CMD_BACK;
+    else if (matches(cmd_arg0, "help") || strcmp(cmd_arg0, "?") == 0)
+        editor_cmd = NMC_EDITOR_SUB_CMD_HELP;
+    else if (matches(cmd_arg0, "quit"))
+        editor_cmd = NMC_EDITOR_SUB_CMD_QUIT;
+    else
+        goto fail;
+
+    NM_SET_OUT(cmd_arg, g_strdup(cmd_argr));
+    return editor_cmd;
+fail:
+    NM_SET_OUT(cmd_arg, NULL);
+    return NMC_EDITOR_SUB_CMD_UNKNOWN;
+}
+
+static void
+editor_sub_help(void)
+{
+    g_print("------------------------------------------------------------------------------\n");
+    /* TRANSLATORS: do not translate command names and keywords before ::
+     *              However, you should translate terms enclosed in <>.
+     */
+    g_print(_("---[ Property menu ]---\n"
+              "set      [<value>]               :: set new value\n"
+              "add      [<value>]               :: add new option to the property\n"
+              "change                           :: change current value\n"
+              "remove   [<index> | <option>]    :: delete the value\n"
+              "describe                         :: describe property\n"
+              "print    [setting | connection]  :: print property (setting/connection) value(s)\n"
+              "back                             :: go to upper level\n"
+              "help/?   [<command>]             :: print this help or command description\n"
+              "quit                             :: exit nmcli\n"));
+    g_print("------------------------------------------------------------------------------\n");
+}
+
+static void
+editor_sub_usage(const char *command)
+{
+    if (!command)
+        editor_sub_help();
+    else {
+        /* detailed command descriptions */
+        NmcEditorSubCmd cmdsub = parse_editor_sub_cmd(command, NULL);
+
+        switch (cmdsub) {
+        case NMC_EDITOR_SUB_CMD_SET:
+            g_print(_("set [<value>]  :: set new value\n\n"
+                      "This command sets provided <value> to this property\n"));
+            break;
+        case NMC_EDITOR_SUB_CMD_ADD:
+            g_print(_("add [<value>]  :: append new value to the property\n\n"
+                      "This command adds provided <value> to this property, if "
+                      "the property is of a container type. For single-valued "
+                      "properties the property value is replaced (same as 'set').\n"));
+            break;
+        case NMC_EDITOR_SUB_CMD_CHANGE:
+            g_print(_("change  :: change current value\n\n"
+                      "Displays current value and allows editing it.\n"));
+            break;
+        case NMC_EDITOR_SUB_CMD_REMOVE:
+            g_print(_(
+                "remove [<value>|<index>|<option name>]  :: delete the value\n\n"
+                "Removes the property value. For single-valued properties, this sets the\n"
+                "property back to its default value. For container-type properties, this removes\n"
+                "all the values of that property or you can specify an argument to remove just\n"
+                "a single item or option. The argument is either a value or index of the item to\n"
+                "remove, or an option name (for properties with named options).\n\n"
+                "Examples: nmcli ipv4.dns> remove 8.8.8.8\n"
+                "          nmcli ipv4.dns> remove 2\n"
+                "          nmcli bond.options> remove downdelay\n\n"));
+            break;
+        case NMC_EDITOR_SUB_CMD_DESCRIBE:
+            g_print(_("describe  :: describe property\n\n"
+                      "Shows property description. You can consult nm-settings(5) "
+                      "manual page to see all NM settings and properties.\n"));
+            break;
+        case NMC_EDITOR_SUB_CMD_PRINT:
+            g_print(_("print [property|setting|connection]  :: print property (setting, "
+                      "connection) value(s)\n\n"
+                      "Shows property value. Providing an argument you can also display "
+                      "values for the whole setting or connection.\n"));
+            break;
+        case NMC_EDITOR_SUB_CMD_BACK:
+            g_print(_("back  :: go to upper menu level\n\n"));
+            break;
+        case NMC_EDITOR_SUB_CMD_HELP:
+            g_print(_("help/? [<command>]  :: help for nmcli commands\n\n"));
+            break;
+        case NMC_EDITOR_SUB_CMD_QUIT:
+            g_print(_("quit  :: exit nmcli\n\n"
+                      "This command exits nmcli. When the connection being edited "
+                      "is not saved, the user is asked to confirm the action.\n"));
+            break;
+        default:
+            g_print(_("Unknown command: '%s'\n"), command);
+            break;
+        }
+    }
+}
+
+/*****************************************************************************/
+
+typedef struct {
+    NMDevice *          device;
+    NMActiveConnection *ac;
+    guint               monitor_id;
+    NmCli *             nmc;
+} MonitorACInfo;
+
+static gboolean       nmc_editor_cb_called;
+static GError *       nmc_editor_error;
+static MonitorACInfo *nmc_editor_monitor_ac;
+
+static void
+editor_connection_changed_cb(NMConnection *connection, gboolean *changed)
+{
+    *changed = TRUE;
+}
+
+/*
+ * Store 'error' to shared 'nmc_editor_error' and monitoring info to
+ * 'nmc_editor_monitor_ac' and signal the condition so that
+ * the 'editor-thread' thread could process that.
+ */
+static void
+set_info_and_signal_editor_thread(GError *error, MonitorACInfo *monitor_ac_info)
+{
+    nmc_editor_cb_called  = TRUE;
+    nmc_editor_error      = error ? g_error_copy(error) : NULL;
+    nmc_editor_monitor_ac = monitor_ac_info;
+}
+
+static void
+add_connection_editor_cb(GObject *client, GAsyncResult *result, gpointer user_data)
+{
+    gs_unref_object NMRemoteConnection *connection = NULL;
+    gs_free_error GError *error                    = NULL;
+
+    connection = nm_client_add_connection2_finish(NM_CLIENT(client), result, NULL, &error);
+    set_info_and_signal_editor_thread(error, NULL);
+}
+
+static void
+update_connection_editor_cb(GObject *connection, GAsyncResult *result, gpointer user_data)
+{
+    GError *error = NULL;
+
+    nm_remote_connection_commit_changes_finish(NM_REMOTE_CONNECTION(connection), result, &error);
+    set_info_and_signal_editor_thread(error, NULL);
+    g_clear_error(&error);
+}
+
+static gboolean
+progress_activation_editor_cb(gpointer user_data)
+{
+    MonitorACInfo *         info   = (MonitorACInfo *) user_data;
+    NMDevice *              device = info->device;
+    NMActiveConnection *    ac     = info->ac;
+    NMActiveConnectionState ac_state;
+    NMDeviceState           dev_state;
+
+    if (!device || !ac)
+        goto finish;
+
+    ac_state  = nm_active_connection_get_state(ac);
+    dev_state = nm_device_get_state(device);
+
+    nmc_terminal_show_progress(gettext(nmc_device_state_to_string_with_external(device)));
+
+    if (ac_state == NM_ACTIVE_CONNECTION_STATE_ACTIVATED
+        || dev_state == NM_DEVICE_STATE_ACTIVATED) {
+        nmc_terminal_erase_line();
+        g_print(_("Connection successfully activated (D-Bus active path: %s)\n"),
+                nm_object_get_path(NM_OBJECT(ac)));
+        goto finish;
+    } else if (ac_state == NM_ACTIVE_CONNECTION_STATE_DEACTIVATED
+               || dev_state == NM_DEVICE_STATE_FAILED) {
+        nmc_terminal_erase_line();
+        g_print(_("Error: Connection activation failed.\n"));
+        goto finish;
+    }
+
+    if (info->nmc->secret_agent) {
+        NMRemoteConnection *connection;
+
+        connection = nm_active_connection_get_connection(ac);
+        nm_secret_agent_simple_enable(info->nmc->secret_agent,
+                                      nm_object_get_path(NM_OBJECT(connection)));
+    }
+
+    return G_SOURCE_CONTINUE;
+
+finish:
+    nm_g_object_unref(device);
+    nm_g_object_unref(ac);
+    info->monitor_id = 0;
+    return G_SOURCE_REMOVE;
+}
+
+static void
+activate_connection_editor_cb(GObject *client, GAsyncResult *result, gpointer user_data)
+{
+    ActivateConnectionInfo *info   = (ActivateConnectionInfo *) user_data;
+    NMDevice *              device = info->device;
+    const GPtrArray *       ac_devs;
+    MonitorACInfo *         monitor_ac_info = NULL;
+    NMActiveConnection *    active;
+    GError *                error = NULL;
+
+    active = nm_client_activate_connection_finish(NM_CLIENT(client), result, &error);
+
+    if (!error) {
+        if (!device) {
+            ac_devs = nm_active_connection_get_devices(active);
+            device  = ac_devs->len > 0 ? g_ptr_array_index(ac_devs, 0) : NULL;
+        }
+        if (device) {
+            monitor_ac_info         = g_malloc0(sizeof(MonitorACInfo));
+            monitor_ac_info->device = g_object_ref(device);
+            monitor_ac_info->ac     = active;
+            monitor_ac_info->monitor_id =
+                g_timeout_add(120, progress_activation_editor_cb, monitor_ac_info);
+            monitor_ac_info->nmc = info->nmc;
+        } else
+            g_object_unref(active);
+    }
+
+    nm_g_object_unref(info->device);
+    g_free(info);
+
+    set_info_and_signal_editor_thread(error, monitor_ac_info);
+    g_clear_error(&error);
+}
+
+/*****************************************************************************/
+
+static void
+print_property_description(NMSetting *setting, const char *prop_name)
+{
+    char *desc;
+
+    desc = nmc_setting_get_property_desc(setting, prop_name);
+    if (desc) {
+        g_print("\n=== [%s] ===\n%s\n", prop_name, desc);
+        g_free(desc);
+    }
+}
+
+static void
+print_setting_description(NMSetting *setting)
+{
+    /* Show description of all properties */
+    char **all_props;
+    int    i;
+
+    all_props = nmc_setting_get_valid_properties(setting);
+    g_print(("<<< %s >>>\n"), nm_setting_get_name(setting));
+    for (i = 0; all_props && all_props[i]; i++)
+        print_property_description(setting, all_props[i]);
+    g_strfreev(all_props);
+}
+
+static void
+editor_show_status_line(NMConnection *connection, gboolean dirty, gboolean temp)
+{
+    NMSettingConnection *s_con;
+    const char *         con_type, *con_id, *con_uuid;
+
+    s_con = nm_connection_get_setting_connection(connection);
+    g_assert(s_con);
+    con_type = nm_setting_connection_get_connection_type(s_con);
+    con_id   = nm_connection_get_id(connection);
+    con_uuid = nm_connection_get_uuid(connection);
+
+    /* TRANSLATORS: status line in nmcli connection editor */
+    g_print(_("[ Type: %s | Name: %s | UUID: %s | Dirty: %s | Temp: %s ]\n"),
+            con_type,
+            con_id,
+            con_uuid,
+            dirty ? _("yes") : _("no"),
+            temp ? _("yes") : _("no"));
+}
+
+static gboolean
+refresh_remote_connection(GWeakRef *weak, NMRemoteConnection **remote)
+{
+    gboolean previous;
+
+    g_return_val_if_fail(remote, FALSE);
+
+    previous = (*remote != NULL);
+    if (*remote)
+        g_object_unref(*remote);
+    *remote = g_weak_ref_get(weak);
+
+    return (previous && !*remote);
+}
+
+static gboolean
+is_connection_dirty(NMConnection *connection, NMRemoteConnection *remote)
+{
+    return !nm_connection_compare(connection,
+                                  remote ? NM_CONNECTION(remote) : NULL,
+                                  NM_SETTING_COMPARE_FLAG_IGNORE_SECRETS
+                                      | NM_SETTING_COMPARE_FLAG_IGNORE_TIMESTAMP);
+}
+
+static gboolean
+confirm_quit(const NmcConfig *nmc_config)
+{
+    gs_free char *answer = NULL;
+
+    answer = nmc_readline(nmc_config,
+                          _("The connection is not saved. "
+                            "Do you really want to quit? %s"),
+                          prompt_yes_no(FALSE, NULL));
+    nm_strstrip(answer);
+    return (answer && matches(answer, WORD_YES));
+}
+
+/*
+ * Submenu for detailed property editing
+ * Return: TRUE - continue;  FALSE - should quit
+ */
+static gboolean
+property_edit_submenu(NmCli *              nmc,
+                      NMConnection *       connection,
+                      NMRemoteConnection **rem_con,
+                      GWeakRef *           rem_con_weak,
+                      NMSetting *          curr_setting,
+                      const char *         prop_name)
+{
+    NmcEditorSubCmd cmdsub;
+    gboolean        set_result;
+    GError *        tmp_err = NULL;
+    gs_free char *  prompt  = NULL;
+    gboolean        temp_changes;
+
+    /* Set global variable for use in TAB completion */
+    nmc_tab_completion.property = prop_name;
+
+    prompt = nmc_colorize(&nmc->nmc_config,
+                          NM_META_COLOR_PROMPT,
+                          "nmcli %s.%s> ",
+                          nm_setting_get_name(curr_setting),
+                          prop_name);
+
+    for (;;) {
+        gs_free char *cmd_property_user = NULL;
+        gs_free char *cmd_property_arg  = NULL;
+        gs_free char *prop_val_user     = NULL;
+        gboolean      removed;
+        gboolean      dirty;
+
+        /* Get the remote connection again, it may have disappeared */
+        removed = refresh_remote_connection(rem_con_weak, rem_con);
+        if (removed) {
+            g_print(_("The connection profile has been removed from another client. "
+                      "You may type 'save' in the main menu to restore it.\n"));
+        }
+
+        /* Connection is dirty? (not saved or differs from the saved) */
+        dirty        = is_connection_dirty(connection, *rem_con);
+        temp_changes = *rem_con ? nm_remote_connection_get_unsaved(*rem_con) : TRUE;
+        if (nmc->editor_status_line)
+            editor_show_status_line(connection, dirty, temp_changes);
+
+        cmd_property_user = nmc_readline(&nmc->nmc_config, "%s", prompt);
+        if (!cmd_property_user || !*cmd_property_user)
+            continue;
+        g_strstrip(cmd_property_user);
+        cmdsub = parse_editor_sub_cmd(cmd_property_user, &cmd_property_arg);
+
+        switch (cmdsub) {
+        case NMC_EDITOR_SUB_CMD_SET:
+        case NMC_EDITOR_SUB_CMD_ADD:
+            /* list, arrays,...: SET replaces the whole property value
+             *                   ADD adds the new value(s)
+             * single values:  : both SET and ADD sets the new value
+             */
+            if (!cmd_property_arg) {
+                gs_strfreev char **to_free = NULL;
+                const char *const *avals;
+
+                avals = nmc_setting_get_property_allowed_values(curr_setting, prop_name, &to_free);
+                if (avals) {
+                    gs_free char *avals_str = NULL;
+
+                    avals_str = nmc_util_strv_for_display(avals, FALSE);
+                    g_print(_("Allowed values for '%s' property: %s\n"), prop_name, avals_str);
+                }
+                prop_val_user = nmc_readline(&nmc->nmc_config, _("Enter '%s' value: "), prop_name);
+            } else
+                prop_val_user = g_strdup(cmd_property_arg);
+
+            set_result = nmc_setting_set_property(nmc->client,
+                                                  curr_setting,
+                                                  prop_name,
+                                                  (cmdsub == NMC_EDITOR_SUB_CMD_SET)
+                                                      ? NM_META_ACCESSOR_MODIFIER_SET
+                                                      : NM_META_ACCESSOR_MODIFIER_ADD,
+                                                  prop_val_user,
+                                                  &tmp_err);
+            if (!set_result) {
+                g_print(_("Error: failed to set '%s' property: %s\n"), prop_name, tmp_err->message);
+                g_clear_error(&tmp_err);
+            }
+            break;
+
+        case NMC_EDITOR_SUB_CMD_CHANGE:
+            rl_startup_hook = nmc_rl_set_deftext;
+            nmc_rl_pre_input_deftext =
+                nmc_setting_get_property_parsable(curr_setting, prop_name, NULL);
+            prop_val_user = nmc_readline(&nmc->nmc_config, _("Edit '%s' value: "), prop_name);
+
+            if (!nmc_setting_set_property(nmc->client,
+                                          curr_setting,
+                                          prop_name,
+                                          NM_META_ACCESSOR_MODIFIER_SET,
+                                          prop_val_user,
+                                          &tmp_err)) {
+                g_print(_("Error: failed to set '%s' property: %s\n"), prop_name, tmp_err->message);
+                g_clear_error(&tmp_err);
+            }
+            break;
+
+        case NMC_EDITOR_SUB_CMD_REMOVE:
+            if (!nmc_setting_set_property(nmc->client,
+                                          curr_setting,
+                                          prop_name,
+                                          (cmd_property_arg ? NM_META_ACCESSOR_MODIFIER_DEL
+                                                            : NM_META_ACCESSOR_MODIFIER_SET),
+                                          cmd_property_arg,
+                                          &tmp_err)) {
+                g_print(_("Error: %s\n"), tmp_err->message);
+                g_clear_error(&tmp_err);
+            }
+            break;
+
+        case NMC_EDITOR_SUB_CMD_DESCRIBE:
+            /* Show property description */
+            print_property_description(curr_setting, prop_name);
+            break;
+
+        case NMC_EDITOR_SUB_CMD_PRINT:
+            /* Print current connection settings/properties */
+            if (cmd_property_arg) {
+                if (matches(cmd_property_arg, "setting"))
+                    editor_show_setting(curr_setting, nmc);
+                else if (matches(cmd_property_arg, "connection")
+                         || matches(cmd_property_arg, "all"))
+                    editor_show_connection(connection, nmc);
+                else
+                    g_print(_("Unknown command argument: '%s'\n"), cmd_property_arg);
+            } else {
+                gs_free char *prop_val = NULL;
+
+                prop_val = nmc_setting_get_property(curr_setting, prop_name, NULL);
+                g_print("%s: %s\n", prop_name, prop_val);
+            }
+            break;
+
+        case NMC_EDITOR_SUB_CMD_BACK:
+            /* Set global variable for use in TAB completion */
+            nmc_tab_completion.property = NULL;
+            return TRUE;
+
+        case NMC_EDITOR_SUB_CMD_HELP:
+            editor_sub_usage(cmd_property_arg);
+            break;
+
+        case NMC_EDITOR_SUB_CMD_QUIT:
+            if (is_connection_dirty(connection, *rem_con)) {
+                if (confirm_quit(&nmc->nmc_config))
+                    return FALSE;
+            } else
+                return FALSE;
+            break;
+
+        case NMC_EDITOR_SUB_CMD_UNKNOWN:
+        default:
+            g_print(_("Unknown command: '%s'\n"), cmd_property_user);
+            break;
+        }
+    }
+}
+
+/*
+ * Split 'str' in the following format:  [[[setting.]property] [value]]
+ * and return the components in 'setting', 'property' and 'value'
+ * Use g_free() to deallocate the returned strings.
+ */
+static void
+split_editor_main_cmd_args(const char *str, char **setting, char **property, char **value)
+{
+    gs_free char *cmd_arg0 = NULL;
+    const char *  cmd_argr;
+    const char *  s;
+
+    NM_SET_OUT(setting, NULL);
+    NM_SET_OUT(property, NULL);
+    NM_SET_OUT(value, NULL);
+
+    _split_cmd(str, &cmd_arg0, &cmd_argr);
+    if (!cmd_arg0)
+        return;
+
+    NM_SET_OUT(value, g_strdup(cmd_argr));
+    s = strchr(cmd_arg0, '.');
+    if (s && s > cmd_arg0) {
+        NM_SET_OUT(setting, g_strndup(cmd_arg0, s - cmd_arg0));
+        NM_SET_OUT(property, g_strdup(&s[1]));
+    } else {
+        NM_SET_OUT(property, g_steal_pointer(&cmd_arg0));
+    }
+}
+
+static NMSetting *
+create_setting_by_name(const char *                             name,
+                       const NMMetaSettingValidPartItem *const *valid_settings_main,
+                       const NMMetaSettingValidPartItem *const *valid_settings_slave)
+{
+    const char *setting_name;
+    NMSetting * setting = NULL;
+
+    /* Get a valid setting name */
+    setting_name = check_valid_name(name, valid_settings_main, valid_settings_slave, NULL);
+
+    if (setting_name) {
+        setting = nm_meta_setting_info_editor_new_setting(
+            nm_meta_setting_info_editor_find_by_name(setting_name, FALSE),
+            NM_META_ACCESSOR_SETTING_INIT_TYPE_CLI);
+    }
+    return setting;
+}
+
+static const char *
+ask_check_setting(const NmcConfig *                        nmc_config,
+                  const char *                             arg,
+                  const NMMetaSettingValidPartItem *const *valid_settings_main,
+                  const NMMetaSettingValidPartItem *const *valid_settings_slave,
+                  const char *                             valid_settings_str)
+{
+    gs_free char *setting_name_user = NULL;
+    const char *  setting_name;
+    GError *      err = NULL;
+
+    if (!arg) {
+        g_print(_("Available settings: %s\n"), valid_settings_str);
+        setting_name_user = nmc_readline(nmc_config, EDITOR_PROMPT_SETTING);
+    } else
+        setting_name_user = g_strdup(arg);
+
+    nm_strstrip(setting_name_user);
+
+    if (!(setting_name = check_valid_name(setting_name_user,
+                                          valid_settings_main,
+                                          valid_settings_slave,
+                                          &err))) {
+        g_print(_("Error: invalid setting name; %s\n"), err->message);
+        g_clear_error(&err);
+    }
+    return setting_name;
+}
+
+static const char *
+ask_check_property(const NmcConfig *nmc_config,
+                   const char *     arg,
+                   const char **    valid_props,
+                   const char *     valid_props_str)
+{
+    gs_free_error GError *tmp_err        = NULL;
+    gs_free char *        prop_name_user = NULL;
+    const char *          prop_name;
+
+    if (!arg) {
+        g_print(_("Available properties: %s\n"), valid_props_str);
+        prop_name_user = nmc_readline(nmc_config, EDITOR_PROMPT_PROPERTY);
+        nm_strstrip(prop_name_user);
+    } else
+        prop_name_user = g_strdup(arg);
+
+    prop_name = nmc_string_is_valid(prop_name_user, valid_props, &tmp_err);
+    if (!prop_name)
+        g_print(_("Error: property %s\n"), tmp_err->message);
+
+    return prop_name;
+}
+
+/* Copy timestamp from src do dst */
+static void
+update_connection_timestamp(NMConnection *src, NMConnection *dst)
+{
+    NMSettingConnection *s_con_src, *s_con_dst;
+
+    s_con_src = nm_connection_get_setting_connection(src);
+    s_con_dst = nm_connection_get_setting_connection(dst);
+    if (s_con_src && s_con_dst) {
+        guint64 timestamp = nm_setting_connection_get_timestamp(s_con_src);
+
+        g_object_set(s_con_dst, NM_SETTING_CONNECTION_TIMESTAMP, timestamp, NULL);
+    }
+}
+
+static gboolean
+confirm_connection_saving(const NmcConfig *nmc_config, NMConnection *local, NMConnection *remote)
+{
+    NMSettingConnection *s_con_loc, *s_con_rem;
+    gboolean             ac_local, ac_remote;
+    gboolean             confirmed = TRUE;
+
+    s_con_loc = nm_connection_get_setting_connection(local);
+    g_assert(s_con_loc);
+    ac_local = nm_setting_connection_get_autoconnect(s_con_loc);
+
+    if (remote) {
+        s_con_rem = nm_connection_get_setting_connection(remote);
+        g_assert(s_con_rem);
+        ac_remote = nm_setting_connection_get_autoconnect(s_con_rem);
+    } else
+        ac_remote = FALSE;
+
+    if (ac_local && !ac_remote) {
+        gs_free char *answer = NULL;
+
+        answer = nmc_readline(nmc_config,
+                              _("Saving the connection with 'autoconnect=yes'. "
+                                "That might result in an immediate activation of the connection.\n"
+                                "Do you still want to save? %s"),
+                              prompt_yes_no(TRUE, NULL));
+        nm_strstrip(answer);
+        confirmed = (!answer || matches(answer, WORD_YES));
+    }
+    return confirmed;
+}
+
+typedef struct {
+    guint      level;
+    char *     main_prompt;
+    NMSetting *curr_setting;
+    char **    valid_props;
+    char *     valid_props_str;
+} NmcEditorMenuContext;
+
+static void
+menu_switch_to_level0(const NmcConfig *     nmc_config,
+                      NmcEditorMenuContext *menu_ctx,
+                      const char *          prompt)
+{
+    menu_ctx->level = 0;
+    g_free(menu_ctx->main_prompt);
+    menu_ctx->main_prompt  = nmc_colorize(nmc_config, NM_META_COLOR_PROMPT, "%s", prompt);
+    menu_ctx->curr_setting = NULL;
+    g_strfreev(menu_ctx->valid_props);
+    menu_ctx->valid_props = NULL;
+    g_free(menu_ctx->valid_props_str);
+    menu_ctx->valid_props_str = NULL;
+}
+
+static void
+menu_switch_to_level1(const NmcConfig *     nmc_config,
+                      NmcEditorMenuContext *menu_ctx,
+                      NMSetting *           setting,
+                      const char *          setting_name)
+{
+    menu_ctx->level = 1;
+    g_free(menu_ctx->main_prompt);
+    menu_ctx->main_prompt =
+        nmc_colorize(nmc_config, NM_META_COLOR_PROMPT, "nmcli %s> ", setting_name);
+    menu_ctx->curr_setting = setting;
+    g_strfreev(menu_ctx->valid_props);
+    menu_ctx->valid_props = nmc_setting_get_valid_properties(menu_ctx->curr_setting);
+    g_free(menu_ctx->valid_props_str);
+    menu_ctx->valid_props_str = g_strjoinv(", ", menu_ctx->valid_props);
+}
+
+static gboolean
+editor_save_timeout(gpointer user_data)
+{
+    gboolean *timeout = user_data;
+
+    *timeout = TRUE;
+
+    return G_SOURCE_REMOVE;
+}
+
+static gboolean
+editor_menu_main(NmCli *nmc, NMConnection *connection, const char *connection_type)
+{
+    gs_unref_object NMRemoteConnection *     rem_con = NULL;
+    NMSettingConnection *                    s_con;
+    NMRemoteConnection *                     con_tmp;
+    GWeakRef                                 weak = {{NULL}};
+    gboolean                                 removed;
+    NmcEditorMainCmd                         cmd;
+    gboolean                                 cmd_loop = TRUE;
+    const NMMetaSettingValidPartItem *const *valid_settings_main;
+    const NMMetaSettingValidPartItem *const *valid_settings_slave;
+    gs_free char *                           valid_settings_str = NULL;
+    const char *                             s_type             = NULL;
+    gboolean                                 temp_changes;
+    GError *                                 err1     = NULL;
+    NmcEditorMenuContext                     menu_ctx = {0};
+
+    s_con = nm_connection_get_setting_connection(connection);
+    if (s_con)
+        s_type = nm_setting_connection_get_slave_type(s_con);
+
+    valid_settings_main  = get_valid_settings_array(connection_type);
+    valid_settings_slave = nm_meta_setting_info_valid_parts_for_slave_type(s_type, NULL);
+
+    valid_settings_str = get_valid_options_string(valid_settings_main, valid_settings_slave);
+    g_print(_("You may edit the following settings: %s\n"), valid_settings_str);
+
+    menu_ctx.main_prompt = nmc_colorize(&nmc->nmc_config, NM_META_COLOR_PROMPT, BASE_PROMPT);
+
+    /* Get remote connection */
+    con_tmp = nm_client_get_connection_by_uuid(nmc->client, nm_connection_get_uuid(connection));
+    g_weak_ref_init(&weak, con_tmp);
+    rem_con = g_weak_ref_get(&weak);
+
+    while (cmd_loop) {
+        gs_free char *cmd_user  = NULL;
+        gs_free char *cmd_arg   = NULL;
+        gs_free char *cmd_arg_s = NULL;
+        gs_free char *cmd_arg_p = NULL;
+        gs_free char *cmd_arg_v = NULL;
+        gboolean      dirty;
+
+        /* Connection is dirty? (not saved or differs from the saved) */
+        dirty        = is_connection_dirty(connection, rem_con);
+        temp_changes = rem_con ? nm_remote_connection_get_unsaved(rem_con) : TRUE;
+        if (nmc->editor_status_line)
+            editor_show_status_line(connection, dirty, temp_changes);
+
+        cmd_user = nmc_readline(&nmc->nmc_config, "%s", menu_ctx.main_prompt);
+
+        /* Get the remote connection again, it may have disappeared */
+        removed = refresh_remote_connection(&weak, &rem_con);
+        if (removed) {
+            g_print(_("The connection profile has been removed from another client. "
+                      "You may type 'save' to restore it.\n"));
+        }
+
+        if (!cmd_user || !*cmd_user)
+            continue;
+
+        g_strstrip(cmd_user);
+
+        cmd = parse_editor_main_cmd(cmd_user, &cmd_arg);
+
+        split_editor_main_cmd_args(cmd_arg, &cmd_arg_s, &cmd_arg_p, &cmd_arg_v);
+        switch (cmd) {
+        case NMC_EDITOR_MAIN_CMD_SET:
+            /* Set property value */
+            if (!cmd_arg) {
+                if (menu_ctx.level == 1) {
+                    gs_strfreev char **avals_to_free = NULL;
+                    gs_free char *     prop_val_user = NULL;
+                    const char *       prop_name;
+                    const char *const *avals;
+                    GError *           tmp_err = NULL;
+
+                    prop_name = ask_check_property(&nmc->nmc_config,
+                                                   cmd_arg,
+                                                   (const char **) menu_ctx.valid_props,
+                                                   menu_ctx.valid_props_str);
+                    if (!prop_name)
+                        break;
+
+                    avals = nmc_setting_get_property_allowed_values(menu_ctx.curr_setting,
+                                                                    prop_name,
+                                                                    &avals_to_free);
+                    if (avals) {
+                        gs_free char *avals_str = NULL;
+
+                        avals_str = nmc_util_strv_for_display(avals, FALSE);
+                        g_print(_("Allowed values for '%s' property: %s\n"), prop_name, avals_str);
+                    }
+                    prop_val_user =
+                        nmc_readline(&nmc->nmc_config, _("Enter '%s' value: "), prop_name);
+
+                    if (!nmc_setting_set_property(nmc->client,
+                                                  menu_ctx.curr_setting,
+                                                  prop_name,
+                                                  NM_META_ACCESSOR_MODIFIER_ADD,
+                                                  prop_val_user,
+                                                  &tmp_err)) {
+                        g_print(_("Error: failed to set '%s' property: %s\n"),
+                                prop_name,
+                                tmp_err->message);
+                        g_clear_error(&tmp_err);
+                    }
+                } else {
+                    g_print(_("Error: no setting selected; valid are [%s]\n"), valid_settings_str);
+                    g_print(_("use 'goto <setting>' first, or 'set <setting>.<property>'\n"));
+                }
+            } else {
+                gs_free char *  prop_name             = NULL;
+                gs_unref_object NMSetting *ss_created = NULL;
+                NMSetting *                ss         = NULL;
+                GError *                   tmp_err    = NULL;
+
+                if (cmd_arg_s) {
+                    /* setting provided as "setting.property" */
+                    ss = is_setting_valid(connection,
+                                          valid_settings_main,
+                                          valid_settings_slave,
+                                          cmd_arg_s);
+                    if (!ss) {
+                        ss_created = create_setting_by_name(cmd_arg_s,
+                                                            valid_settings_main,
+                                                            valid_settings_slave);
+                        ss         = ss_created;
+                        if (!ss) {
+                            g_print(_("Error: invalid setting argument '%s'; valid are [%s]\n"),
+                                    cmd_arg_s,
+                                    valid_settings_str);
+                            break;
+                        }
+                    }
+                } else {
+                    if (menu_ctx.curr_setting)
+                        ss = menu_ctx.curr_setting;
+                    else {
+                        g_print(_("Error: missing setting for '%s' property\n"), cmd_arg_p);
+                        break;
+                    }
+                }
+
+                prop_name = is_property_valid(ss, cmd_arg_p, &tmp_err);
+                if (!prop_name) {
+                    g_print(_("Error: invalid property: %s\n"), tmp_err->message);
+                    g_clear_error(&tmp_err);
+                    break;
+                }
+
+                /* Ask for value */
+                if (!cmd_arg_v) {
+                    gs_strfreev char **avals_to_free = NULL;
+                    const char *const *avals;
+
+                    avals = nmc_setting_get_property_allowed_values(ss, prop_name, &avals_to_free);
+                    if (avals) {
+                        gs_free char *avals_str = NULL;
+
+                        avals_str = nmc_util_strv_for_display(avals, FALSE);
+                        g_print(_("Allowed values for '%s' property: %s\n"), prop_name, avals_str);
+                    }
+                    cmd_arg_v = nmc_readline(&nmc->nmc_config, _("Enter '%s' value: "), prop_name);
+                }
+
+                /* setting a value in edit mode "appends". That seems unexpected behavior. */
+                if (!nmc_setting_set_property(nmc->client,
+                                              ss,
+                                              prop_name,
+                                              cmd_arg_v ? NM_META_ACCESSOR_MODIFIER_ADD
+                                                        : NM_META_ACCESSOR_MODIFIER_SET,
+                                              cmd_arg_v,
+                                              &tmp_err)) {
+                    g_print(_("Error: failed to set '%s' property: %s\n"),
+                            prop_name,
+                            tmp_err->message);
+                    g_clear_error(&tmp_err);
+                }
+
+                if (ss_created)
+                    nm_connection_add_setting(connection, g_steal_pointer(&ss_created));
+            }
+            break;
+
+        case NMC_EDITOR_MAIN_CMD_GOTO:
+            /* cmd_arg_s != NULL means 'setting.property' argument */
+            if (menu_ctx.level == 0 || cmd_arg_s) {
+                /* in top level - no setting selected yet */
+                const char *setting_name;
+                NMSetting * setting;
+                const char *user_arg = cmd_arg_s ?: cmd_arg_p;
+
+                setting_name = ask_check_setting(&nmc->nmc_config,
+                                                 user_arg,
+                                                 valid_settings_main,
+                                                 valid_settings_slave,
+                                                 valid_settings_str);
+                if (!setting_name)
+                    break;
+
+                setting = nm_connection_get_setting_by_name(connection, setting_name);
+                if (!setting) {
+                    const NMMetaSettingInfoEditor *setting_info;
+
+                    setting_info = nm_meta_setting_info_editor_find_by_name(setting_name, FALSE);
+                    if (!setting_info) {
+                        g_print(_("Error: unknown setting '%s'\n"), setting_name);
+                        break;
+                    }
+
+                    setting = nm_meta_setting_info_editor_new_setting(
+                        setting_info,
+                        NM_META_ACCESSOR_SETTING_INIT_TYPE_CLI);
+
+                    if (NM_IS_SETTING_WIRELESS(setting))
+                        nmc_setting_wireless_connect_handlers(NM_SETTING_WIRELESS(setting));
+                    else if (NM_IS_SETTING_IP4_CONFIG(setting))
+                        nmc_setting_ip4_connect_handlers(NM_SETTING_IP_CONFIG(setting));
+                    else if (NM_IS_SETTING_IP6_CONFIG(setting))
+                        nmc_setting_ip6_connect_handlers(NM_SETTING_IP_CONFIG(setting));
+                    else if (NM_IS_SETTING_PROXY(setting))
+                        nmc_setting_proxy_connect_handlers(NM_SETTING_PROXY(setting));
+
+                    nm_connection_add_setting(connection, setting);
+                }
+                /* Set global variable for use in TAB completion */
+                nmc_tab_completion.setting = setting;
+
+                /* Switch to level 1 */
+                menu_switch_to_level1(&nmc->nmc_config, &menu_ctx, setting, setting_name);
+
+                if (!cmd_arg_s) {
+                    g_print(_("You may edit the following properties: %s\n"),
+                            menu_ctx.valid_props_str);
+                    break;
+                }
+            }
+            if (menu_ctx.level == 1 || cmd_arg_s) {
+                /* level 1 - setting selected */
+                const char *prop_name;
+
+                prop_name = ask_check_property(&nmc->nmc_config,
+                                               cmd_arg_p,
+                                               (const char **) menu_ctx.valid_props,
+                                               menu_ctx.valid_props_str);
+                if (!prop_name)
+                    break;
+
+                /* submenu - level 2 - editing properties */
+                cmd_loop = property_edit_submenu(nmc,
+                                                 connection,
+                                                 &rem_con,
+                                                 &weak,
+                                                 menu_ctx.curr_setting,
+                                                 prop_name);
+            }
+            break;
+
+        case NMC_EDITOR_MAIN_CMD_REMOVE:
+            /* Remove setting from connection, or delete value of a property */
+            if (!cmd_arg) {
+                if (menu_ctx.level == 1) {
+                    GError *    tmp_err = NULL;
+                    const char *prop_name;
+
+                    prop_name = ask_check_property(&nmc->nmc_config,
+                                                   cmd_arg,
+                                                   (const char **) menu_ctx.valid_props,
+                                                   menu_ctx.valid_props_str);
+                    if (!prop_name)
+                        break;
+
+                    if (!nmc_setting_set_property(nmc->client,
+                                                  menu_ctx.curr_setting,
+                                                  prop_name,
+                                                  NM_META_ACCESSOR_MODIFIER_SET,
+                                                  NULL,
+                                                  &tmp_err)) {
+                        g_print(_("Error: failed to remove value of '%s': %s\n"),
+                                prop_name,
+                                tmp_err->message);
+                        g_clear_error(&tmp_err);
+                    }
+                } else
+                    g_print(_("Error: no argument given; valid are [%s]\n"), valid_settings_str);
+            } else {
+                NMSetting *ss = NULL;
+                gboolean   descr_all;
+                char *     user_s;
+
+                /* cmd_arg_s != NULL means argument is "setting.property" */
+                descr_all = !cmd_arg_s && !menu_ctx.curr_setting;
+                user_s    = descr_all ? cmd_arg_p : cmd_arg_s;
+                if (user_s) {
+                    ss = is_setting_valid(connection,
+                                          valid_settings_main,
+                                          valid_settings_slave,
+                                          user_s);
+                    if (!ss) {
+                        if (check_valid_name(user_s,
+                                             valid_settings_main,
+                                             valid_settings_slave,
+                                             NULL)) {
+                            g_print(_("Setting '%s' is not present in the connection.\n"), user_s);
+                        } else {
+                            g_print(_("Error: invalid setting argument '%s'; valid are [%s]\n"),
+                                    user_s,
+                                    valid_settings_str);
+                        }
+                        break;
+                    }
+                } else
+                    ss = menu_ctx.curr_setting;
+
+                if (descr_all) {
+                    gs_free_error GError *local = NULL;
+
+                    /* Remove setting from the connection */
+                    if (!connection_remove_setting(connection, ss, &local))
+                        g_print("%s\n", local->message);
+
+                    if (ss == menu_ctx.curr_setting) {
+                        /* If we removed the setting we are in, go up */
+                        menu_switch_to_level0(&nmc->nmc_config, &menu_ctx, BASE_PROMPT);
+                        nmc_tab_completion.setting = NULL; /* for TAB completion */
+                    }
+                } else {
+                    gs_free char *prop_name       = NULL;
+                    gs_free_error GError *tmp_err = NULL;
+
+                    prop_name = is_property_valid(ss, cmd_arg_p, &tmp_err);
+                    if (prop_name) {
+                        if (!nmc_setting_set_property(nmc->client,
+                                                      ss,
+                                                      prop_name,
+                                                      NM_META_ACCESSOR_MODIFIER_SET,
+                                                      NULL,
+                                                      &tmp_err)) {
+                            g_print(_("Error: failed to remove value of '%s': %s\n"),
+                                    prop_name,
+                                    tmp_err->message);
+                        }
+                    } else {
+                        NMSetting *s_tmp;
+
+                        /* If the string is not a property, try it as a setting */
+                        s_tmp = is_setting_valid(connection,
+                                                 valid_settings_main,
+                                                 valid_settings_slave,
+                                                 cmd_arg_p);
+                        if (s_tmp) {
+                            gs_free_error GError *local = NULL;
+
+                            /* Remove setting from the connection */
+                            if (!connection_remove_setting(connection, s_tmp, &local))
+                                g_print("%s\n", local->message);
+
+                            /* coverity[copy_paste_error] - suppress Coverity COPY_PASTE_ERROR defect */
+                            if (ss == menu_ctx.curr_setting) {
+                                /* If we removed the setting we are in, go up */
+                                menu_switch_to_level0(&nmc->nmc_config, &menu_ctx, BASE_PROMPT);
+                                nmc_tab_completion.setting = NULL; /* for TAB completion */
+                            }
+                        } else {
+                            g_print(_("Error: %s properties, nor it is a setting name.\n"),
+                                    tmp_err->message);
+                        }
+                    }
+                }
+            }
+            break;
+
+        case NMC_EDITOR_MAIN_CMD_DESCRIBE:
+            /* Print property description */
+            if (!cmd_arg) {
+                if (menu_ctx.level == 1) {
+                    const char *prop_name;
+
+                    prop_name = ask_check_property(&nmc->nmc_config,
+                                                   cmd_arg,
+                                                   (const char **) menu_ctx.valid_props,
+                                                   menu_ctx.valid_props_str);
+                    if (!prop_name)
+                        break;
+
+                    /* Show property description */
+                    print_property_description(menu_ctx.curr_setting, prop_name);
+                } else {
+                    g_print(_("Error: no setting selected; valid are [%s]\n"), valid_settings_str);
+                    g_print(_("use 'goto <setting>' first, or 'describe <setting>.<property>'\n"));
+                }
+            } else {
+                gs_unref_object NMSetting *ss_free = NULL;
+                NMSetting *                ss      = NULL;
+                gboolean                   descr_all;
+                char *                     user_s;
+
+                /* cmd_arg_s != NULL means argument is "setting.property" */
+                descr_all = !cmd_arg_s && !menu_ctx.curr_setting;
+                user_s    = descr_all ? cmd_arg_p : cmd_arg_s;
+                if (user_s) {
+                    ss = is_setting_valid(connection,
+                                          valid_settings_main,
+                                          valid_settings_slave,
+                                          user_s);
+                    if (!ss) {
+                        ss = create_setting_by_name(user_s,
+                                                    valid_settings_main,
+                                                    valid_settings_slave);
+                        if (!ss) {
+                            g_print(_("Error: invalid setting argument '%s'; valid are [%s]\n"),
+                                    user_s,
+                                    valid_settings_str);
+                            break;
+                        }
+                        ss_free = ss;
+                    }
+                } else
+                    ss = menu_ctx.curr_setting;
+
+                if (!ss) {
+                    g_print(_("Error: no setting selected; valid are [%s]\n"), valid_settings_str);
+                    g_print(_("use 'goto <setting>' first, or 'describe <setting>.<property>'\n"));
+                } else if (descr_all) {
+                    /* Show description for all properties */
+                    print_setting_description(ss);
+                } else {
+                    gs_free_error GError *tmp_err   = NULL;
+                    gs_free char *        prop_name = NULL;
+
+                    prop_name = is_property_valid(ss, cmd_arg_p, &tmp_err);
+                    if (prop_name) {
+                        /* Show property description */
+                        print_property_description(ss, prop_name);
+                    } else {
+                        /* If the string is not a property, try it as a setting */
+                        NMSetting *s_tmp;
+
+                        s_tmp = is_setting_valid(connection,
+                                                 valid_settings_main,
+                                                 valid_settings_slave,
+                                                 cmd_arg_p);
+                        if (s_tmp)
+                            print_setting_description(s_tmp);
+                        else {
+                            g_print(_("Error: invalid property: %s, "
+                                      "neither a valid setting name.\n"),
+                                    tmp_err->message);
+                        }
+                    }
+                }
+            }
+            break;
+
+        case NMC_EDITOR_MAIN_CMD_PRINT:
+            /* Print current connection settings/properties */
+            if (cmd_arg) {
+                if (nm_streq(cmd_arg, "all"))
+                    editor_show_connection(connection, nmc);
+                else {
+                    NMSetting *ss = NULL;
+                    gboolean   whole_setting;
+                    char *     user_s;
+
+                    /* cmd_arg_s != NULL means argument is "setting.property" */
+                    whole_setting = !cmd_arg_s && !menu_ctx.curr_setting;
+                    user_s        = whole_setting ? cmd_arg_p : cmd_arg_s;
+                    if (user_s) {
+                        const char *s_name;
+
+                        s_name = check_valid_name(user_s,
+                                                  valid_settings_main,
+                                                  valid_settings_slave,
+                                                  NULL);
+                        if (!s_name) {
+                            g_print(_("Error: unknown setting: '%s'\n"), user_s);
+                            break;
+                        }
+                        ss = nm_connection_get_setting_by_name(connection, s_name);
+                        if (!ss) {
+                            g_print(_("Error: '%s' setting not present in the connection\n"),
+                                    s_name);
+                            break;
+                        }
+                    } else
+                        ss = menu_ctx.curr_setting;
+
+                    if (whole_setting) {
+                        /* Print the whole setting */
+                        editor_show_setting(ss, nmc);
+                    } else {
+                        gs_free char *prop_name = NULL;
+                        GError *      err       = NULL;
+
+                        prop_name = is_property_valid(ss, cmd_arg_p, &err);
+                        if (prop_name) {
+                            /* Print one property */
+                            gs_free char *prop_val = NULL;
+
+                            prop_val = nmc_setting_get_property(ss, prop_name, NULL);
+                            g_print("%s.%s: %s\n", nm_setting_get_name(ss), prop_name, prop_val);
+                        } else {
+                            /* If the string is not a property, try it as a setting */
+                            NMSetting *s_tmp;
+                            s_tmp = is_setting_valid(connection,
+                                                     valid_settings_main,
+                                                     valid_settings_slave,
+                                                     cmd_arg_p);
+                            if (s_tmp) {
+                                /* Print the whole setting */
+                                editor_show_setting(s_tmp, nmc);
+                            } else
+                                g_print(_("Error: invalid property: %s%s\n"),
+                                        err->message,
+                                        cmd_arg_s ? "" : _(", neither a valid setting name"));
+                            g_clear_error(&err);
+                        }
+                    }
+                }
+            } else {
+                if (menu_ctx.curr_setting)
+                    editor_show_setting(menu_ctx.curr_setting, nmc);
+                else
+                    editor_show_connection(connection, nmc);
+            }
+            break;
+
+        case NMC_EDITOR_MAIN_CMD_VERIFY:
+            /* Verify current setting or the whole connection */
+            if (cmd_arg && strcmp(cmd_arg, "all") && strcmp(cmd_arg, "fix")) {
+                g_print(_("Invalid verify option: %s\n"), cmd_arg);
+                break;
+            }
+
+            if (menu_ctx.curr_setting && (!cmd_arg || strcmp(cmd_arg, "all") != 0)) {
+                gs_free_error GError *tmp_err = NULL;
+
+                nm_setting_verify(menu_ctx.curr_setting, NULL, &tmp_err);
+                g_print(_("Verify setting '%s': %s\n"),
+                        nm_setting_get_name(menu_ctx.curr_setting),
+                        tmp_err ? tmp_err->message : "OK");
+            } else {
+                gs_free_error GError *tmp_err = NULL;
+                gboolean              fixed   = TRUE;
+                gboolean              modified;
+                gboolean              valid;
+
+                valid = nm_connection_verify(connection, &tmp_err);
+                if (!valid && nm_streq0(cmd_arg, "fix")) {
+                    /* Try to fix normalizable errors */
+                    g_clear_error(&tmp_err);
+                    fixed = nm_connection_normalize(connection, NULL, &modified, &tmp_err);
+                }
+                g_print(_("Verify connection: %s\n"), tmp_err ? tmp_err->message : "OK");
+                if (!fixed)
+                    g_print(_("The error cannot be fixed automatically.\n"));
+            }
+            break;
+
+        case NMC_EDITOR_MAIN_CMD_SAVE:
+            /* Save the connection */
+            if (nm_connection_verify(connection, &err1)) {
+                gboolean              temporary = FALSE;
+                gboolean              connection_changed;
+                nm_auto_unref_gsource GSource *source     = NULL;
+                gboolean                       timeout    = FALSE;
+                gulong                         handler_id = 0;
+
+                /* parse argument */
+                if (cmd_arg) {
+                    if (matches(cmd_arg, "temporary"))
+                        temporary = TRUE;
+                    else if (matches(cmd_arg, "persistent"))
+                        temporary = FALSE;
+                    else {
+                        g_print(_("Error: invalid argument '%s'\n"), cmd_arg);
+                        break;
+                    }
+                }
+
+                /* Ask for save confirmation if the connection changes to autoconnect=yes */
+                if (nmc->editor_save_confirmation) {
+                    if (!confirm_connection_saving(&nmc->nmc_config,
+                                                   connection,
+                                                   NM_CONNECTION(rem_con)))
+                        break;
+                }
+
+                if (!rem_con) {
+                    add_connection(nmc->client,
+                                   connection,
+                                   temporary,
+                                   add_connection_editor_cb,
+                                   NULL);
+                    connection_changed = TRUE;
+                } else {
+                    /* Save/update already saved (existing) connection */
+                    nm_connection_replace_settings_from_connection(NM_CONNECTION(rem_con),
+                                                                   connection);
+                    update_connection(rem_con, temporary, update_connection_editor_cb, NULL);
+
+                    handler_id         = g_signal_connect(rem_con,
+                                                  NM_CONNECTION_CHANGED,
+                                                  G_CALLBACK(editor_connection_changed_cb),
+                                                  &connection_changed);
+                    connection_changed = FALSE;
+                }
+
+                source = g_timeout_source_new(10 * NM_UTILS_MSEC_PER_SEC);
+                g_source_set_callback(source, editor_save_timeout, &timeout, NULL);
+                g_source_attach(source, g_main_loop_get_context(loop));
+
+                while (!nmc_editor_cb_called && !timeout)
+                    g_main_context_iteration(NULL, TRUE);
+
+                if (!nmc_editor_error) {
+                    while (!connection_changed && !timeout)
+                        g_main_context_iteration(NULL, TRUE);
+                }
+
+                if (handler_id)
+                    g_signal_handler_disconnect(rem_con, handler_id);
+                g_source_destroy(source);
+
+                if (nmc_editor_error) {
+                    g_print(_("Error: Failed to save '%s' (%s) connection: %s\n"),
+                            nm_connection_get_id(connection),
+                            nm_connection_get_uuid(connection),
+                            nmc_editor_error->message);
+                    g_error_free(nmc_editor_error);
+                } else if (timeout) {
+                    g_print(_("Error: Timeout saving '%s' (%s) connection\n"),
+                            nm_connection_get_id(connection),
+                            nm_connection_get_uuid(connection));
+                } else {
+                    g_print(!rem_con ? _("Connection '%s' (%s) successfully saved.\n")
+                                     : _("Connection '%s' (%s) successfully updated.\n"),
+                            nm_connection_get_id(connection),
+                            nm_connection_get_uuid(connection));
+
+                    con_tmp = nm_client_get_connection_by_uuid(nmc->client,
+                                                               nm_connection_get_uuid(connection));
+                    g_weak_ref_set(&weak, con_tmp);
+                    refresh_remote_connection(&weak, &rem_con);
+
+                    /* Replace local connection with the remote one to be sure they are equal.
+                     * This mitigates problems with plugins not preserving some properties or
+                     * adding ipv{4,6} settings when not present.
+                     */
+                    if (con_tmp) {
+                        gs_free char *s_name = NULL;
+
+                        if (menu_ctx.curr_setting)
+                            s_name = g_strdup(nm_setting_get_name(menu_ctx.curr_setting));
+
+                        /* Update settings and secrets in the local connection */
+                        nm_connection_replace_settings_from_connection(connection,
+                                                                       NM_CONNECTION(con_tmp));
+                        update_secrets_in_connection(con_tmp, connection);
+
+                        /* Also update setting for menu context and TAB-completion */
+                        menu_ctx.curr_setting =
+                            s_name ? nm_connection_get_setting_by_name(connection, s_name) : NULL;
+                        nmc_tab_completion.setting = menu_ctx.curr_setting;
+                    }
+                }
+
+                nmc_editor_cb_called = FALSE;
+                nmc_editor_error     = NULL;
+            } else {
+                g_print(_("Error: connection verification failed: %s\n"),
+                        err1 ? err1->message : _("(unknown error)"));
+                g_print(_("You may try running 'verify fix' to fix errors.\n"));
+            }
+
+            g_clear_error(&err1);
+            break;
+
+        case NMC_EDITOR_MAIN_CMD_ACTIVATE:
+        {
+            GError *    tmp_err = NULL;
+            const char *ifname  = cmd_arg_p;
+            const char *ap_nsp  = cmd_arg_v;
+
+            /* When only AP/NSP is specified it is prepended with '/' */
+            if (!cmd_arg_v) {
+                if (ifname && ifname[0] == '/') {
+                    ap_nsp = ifname + 1;
+                    ifname = NULL;
+                }
+            } else
+                ap_nsp = ap_nsp && ap_nsp[0] == '/' ? ap_nsp + 1 : ap_nsp;
+
+            if (is_connection_dirty(connection, rem_con)) {
+                /* TRANSLATORS: do not translate 'save', leave it as it is */
+                g_print(_("Error: connection is not saved. Type 'save' first.\n"));
+                break;
+            }
+            if (!nm_connection_verify(NM_CONNECTION(rem_con), &tmp_err)) {
+                g_print(_("Error: connection is not valid: %s\n"), tmp_err->message);
+                g_clear_error(&tmp_err);
+                break;
+            }
+
+            nmc->nowait_flag = FALSE;
+            nmc->should_wait++;
+            nmc->nmc_config_mutable.print_output = NMC_PRINT_PRETTY;
+            if (!nmc_activate_connection(nmc,
+                                         NM_CONNECTION(rem_con),
+                                         ifname,
+                                         ap_nsp,
+                                         ap_nsp,
+                                         NULL,
+                                         activate_connection_editor_cb,
+                                         &tmp_err)) {
+                g_print(_("Error: Cannot activate connection: %s.\n"), tmp_err->message);
+                g_clear_error(&tmp_err);
+                break;
+            }
+
+            while (!nmc_editor_cb_called)
+                g_main_context_iteration(NULL, TRUE);
+
+            if (nmc_editor_error) {
+                g_print(_("Error: Failed to activate '%s' (%s) connection: %s\n"),
+                        nm_connection_get_id(connection),
+                        nm_connection_get_uuid(connection),
+                        nmc_editor_error->message);
+                g_error_free(nmc_editor_error);
+            } else {
+                nmc_readline(&nmc->nmc_config,
+                             _("Monitoring connection activation (press any key to continue)\n"));
+            }
+
+            if (nmc_editor_monitor_ac) {
+                if (nmc_editor_monitor_ac->monitor_id)
+                    g_source_remove(nmc_editor_monitor_ac->monitor_id);
+                g_free(nmc_editor_monitor_ac);
+            }
+            nmc_editor_cb_called  = FALSE;
+            nmc_editor_error      = NULL;
+            nmc_editor_monitor_ac = NULL;
+
+            /* Update timestamp in local connection */
+            update_connection_timestamp(NM_CONNECTION(rem_con), connection);
+
+        } break;
+
+        case NMC_EDITOR_MAIN_CMD_BACK:
+            /* Go back (up) an the menu */
+            if (menu_ctx.level == 1) {
+                menu_switch_to_level0(&nmc->nmc_config, &menu_ctx, BASE_PROMPT);
+                nmc_tab_completion.setting = NULL; /* for TAB completion */
+            }
+            break;
+
+        case NMC_EDITOR_MAIN_CMD_HELP:
+            /* Print command help */
+            editor_main_help(cmd_arg);
+            break;
+
+        case NMC_EDITOR_MAIN_CMD_NMCLI:
+            if (cmd_arg_p && matches(cmd_arg_p, "status-line")) {
+                GError * tmp_err = NULL;
+                gboolean bb;
+                if (!nmc_string_to_bool(cmd_arg_v ? g_strstrip(cmd_arg_v) : "", &bb, &tmp_err)) {
+                    g_print(_("Error: status-line: %s\n"), tmp_err->message);
+                    g_clear_error(&tmp_err);
+                } else
+                    nmc->editor_status_line = bb;
+            } else if (cmd_arg_p && matches(cmd_arg_p, "save-confirmation")) {
+                GError * tmp_err = NULL;
+                gboolean bb;
+                if (!nmc_string_to_bool(cmd_arg_v ? g_strstrip(cmd_arg_v) : "", &bb, &tmp_err)) {
+                    g_print(_("Error: save-confirmation: %s\n"), tmp_err->message);
+                    g_clear_error(&tmp_err);
+                } else
+                    nmc->editor_save_confirmation = bb;
+            } else if (cmd_arg_p && matches(cmd_arg_p, "show-secrets")) {
+                GError * tmp_err = NULL;
+                gboolean bb;
+                if (!nmc_string_to_bool(cmd_arg_v ? g_strstrip(cmd_arg_v) : "", &bb, &tmp_err)) {
+                    g_print(_("Error: show-secrets: %s\n"), tmp_err->message);
+                    g_clear_error(&tmp_err);
+                } else
+                    nmc->nmc_config_mutable.show_secrets = bb;
+            } else if (cmd_arg_p && matches(cmd_arg_p, "prompt-color")) {
+                g_debug("Ignoring erroneous --prompt-color argument. Use terminal-colors.d(5) to "
+                        "set the prompt color.\n");
+            } else if (!cmd_arg_p) {
+                g_print(_("Current nmcli configuration:\n"));
+                g_print("status-line: %s\n"
+                        "save-confirmation: %s\n"
+                        "show-secrets: %s\n",
+                        nmc->editor_status_line ? "yes" : "no",
+                        nmc->editor_save_confirmation ? "yes" : "no",
+                        nmc->nmc_config.show_secrets ? "yes" : "no");
+            } else
+                g_print(_("Invalid configuration option '%s'; allowed [%s]\n"),
+                        cmd_arg_v ?: "",
+                        "status-line, save-confirmation, show-secrets");
+
+            break;
+
+        case NMC_EDITOR_MAIN_CMD_QUIT:
+            if (is_connection_dirty(connection, rem_con)) {
+                if (confirm_quit(&nmc->nmc_config))
+                    cmd_loop = FALSE; /* quit command loop */
+            } else
+                cmd_loop = FALSE; /* quit command loop */
+            break;
+
+        case NMC_EDITOR_MAIN_CMD_UNKNOWN:
+        default:
+            g_print(_("Unknown command: '%s'\n"), cmd_user);
+            break;
+        }
+    }
+
+    g_free(menu_ctx.main_prompt);
+    g_strfreev(menu_ctx.valid_props);
+    g_free(menu_ctx.valid_props_str);
+    g_weak_ref_clear(&weak);
+
+    quit();
+
+    /* Save history file */
+    save_history_cmds(nm_connection_get_uuid(connection));
+
+    return TRUE;
+}
+
+static const char *
+get_ethernet_device_name(NmCli *nmc)
+{
+    const GPtrArray *devices;
+    guint            i;
+
+    devices = nm_client_get_devices(nmc->client);
+    for (i = 0; i < devices->len; i++) {
+        NMDevice *dev = g_ptr_array_index(devices, i);
+        if (NM_IS_DEVICE_ETHERNET(dev))
+            return nm_device_get_iface(dev);
+    }
+    return NULL;
+}
+
+static void
+editor_init_new_connection(NmCli *nmc, NMConnection *connection, const char *slave_type)
+{
+    NMSetting *          setting, *base_setting;
+    NMSettingConnection *s_con;
+    const char *         con_type;
+
+    s_con = nm_connection_get_setting_connection(connection);
+    g_assert(s_con);
+    con_type = nm_setting_connection_get_connection_type(s_con);
+
+    /* Initialize new connection according to its type using sensible defaults. */
+
+    nmc_setting_connection_connect_handlers(s_con, connection);
+
+    if (slave_type) {
+        const char *dev_ifname = get_ethernet_device_name(nmc);
+
+        /* For bond/team/bridge slaves add 'wired' setting */
+        setting = nm_setting_wired_new();
+        nm_connection_add_setting(connection, setting);
+
+        g_object_set(s_con,
+                     NM_SETTING_CONNECTION_TYPE,
+                     NM_SETTING_WIRED_SETTING_NAME,
+                     NM_SETTING_CONNECTION_MASTER,
+                     dev_ifname ?: "eth0",
+                     NM_SETTING_CONNECTION_SLAVE_TYPE,
+                     slave_type,
+                     NULL);
+    } else {
+        const NMMetaSettingInfoEditor *setting_info;
+
+        /* Add a "base" setting to the connection by default */
+        setting_info = nm_meta_setting_info_editor_find_by_name(con_type, FALSE);
+        if (!setting_info)
+            return;
+        base_setting =
+            nm_meta_setting_info_editor_new_setting(setting_info,
+                                                    NM_META_ACCESSOR_SETTING_INIT_TYPE_CLI);
+        nm_connection_add_setting(connection, base_setting);
+
+        set_default_interface_name(nmc, s_con);
+
+        /* Set sensible initial VLAN values */
+        if (g_strcmp0(con_type, NM_SETTING_VLAN_SETTING_NAME) == 0) {
+            const char *dev_ifname = get_ethernet_device_name(nmc);
+
+            g_object_set(NM_SETTING_VLAN(base_setting),
+                         NM_SETTING_VLAN_PARENT,
+                         dev_ifname ?: "eth0",
+                         NULL);
+        }
+
+        setting = nm_meta_setting_info_editor_new_setting(
+            &nm_meta_setting_infos_editor[NM_META_SETTING_TYPE_IP4_CONFIG],
+            NM_META_ACCESSOR_SETTING_INIT_TYPE_CLI);
+        nm_connection_add_setting(connection, setting);
+
+        setting = nm_meta_setting_info_editor_new_setting(
+            &nm_meta_setting_infos_editor[NM_META_SETTING_TYPE_IP6_CONFIG],
+            NM_META_ACCESSOR_SETTING_INIT_TYPE_CLI);
+        nm_connection_add_setting(connection, setting);
+
+        setting = nm_meta_setting_info_editor_new_setting(
+            &nm_meta_setting_infos_editor[NM_META_SETTING_TYPE_PROXY],
+            NM_META_ACCESSOR_SETTING_INIT_TYPE_CLI);
+        nm_connection_add_setting(connection, setting);
+    }
+}
+
+static void
+editor_init_existing_connection(NMConnection *connection)
+{
+    NMSettingIPConfig *  s_ip4, *s_ip6;
+    NMSettingProxy *     s_proxy;
+    NMSettingWireless *  s_wireless;
+    NMSettingConnection *s_con;
+
+    /* FIXME: this approach of connecting handlers to do something is fundamentally
+     * flawed. See the comment in nmc_setting_ip6_connect_handlers(). */
+
+    s_ip4      = nm_connection_get_setting_ip4_config(connection);
+    s_ip6      = nm_connection_get_setting_ip6_config(connection);
+    s_proxy    = nm_connection_get_setting_proxy(connection);
+    s_wireless = nm_connection_get_setting_wireless(connection);
+    s_con      = nm_connection_get_setting_connection(connection);
+
+    if (s_ip4)
+        nmc_setting_ip4_connect_handlers(s_ip4);
+    if (s_ip6)
+        nmc_setting_ip6_connect_handlers(s_ip6);
+    if (s_proxy)
+        nmc_setting_proxy_connect_handlers(s_proxy);
+    if (s_wireless)
+        nmc_setting_wireless_connect_handlers(s_wireless);
+    if (s_con)
+        nmc_setting_connection_connect_handlers(s_con, connection);
+}
+
+static void
+nmc_complete_connection_type(const char *prefix)
+{
+    guint i;
+
+    for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++) {
+        const NMMetaSettingInfoEditor *setting_info = &nm_meta_setting_infos_editor[i];
+
+        if (!*prefix || matches(prefix, setting_info->general->setting_name))
+            g_print("%s\n", setting_info->general->setting_name);
+        if (setting_info->alias && (!*prefix || matches(prefix, setting_info->alias)))
+            g_print("%s\n", setting_info->alias);
+    }
+}
+
+static void
+do_connection_edit(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    const GPtrArray *connections;
+    gs_unref_object NMConnection *connection = NULL;
+    NMSettingConnection *         s_con;
+    const char *                  connection_type;
+    const char *                  type         = NULL;
+    const char *                  con_name     = NULL;
+    const char *                  con          = NULL;
+    const char *                  con_id       = NULL;
+    const char *                  con_uuid     = NULL;
+    const char *                  con_path     = NULL;
+    const char *                  con_filename = NULL;
+    const char *                  selector     = NULL;
+    gs_free_error GError *error                = NULL;
+    GError *              err1                 = NULL;
+    nmc_arg_t             exp_args[]           = {{"type", TRUE, &type, FALSE},
+                            {"con-name", TRUE, &con_name, FALSE},
+                            {"id", TRUE, &con_id, FALSE},
+                            {"uuid", TRUE, &con_uuid, FALSE},
+                            {"path", TRUE, &con_path, FALSE},
+                            {"filename", TRUE, &con_filename, FALSE},
+                            {NULL}};
+
+    next_arg(nmc, &argc, &argv, NULL);
+    if (argc == 1 && nmc->complete)
+        nmc_complete_strings(*argv, "type", "con-name", "id", "uuid", "path", "filename");
+
+    nmc->return_value = NMC_RESULT_SUCCESS;
+
+    if (argc == 1)
+        con = *argv;
+    else {
+        if (!nmc_parse_args(exp_args, TRUE, &argc, &argv, &error)) {
+            g_string_assign(nmc->return_text, error->message);
+            nmc->return_value = error->code;
+            return;
+        }
+    }
+
+    /* Setup some readline completion stuff */
+    /* Set a pointer to an alternative function to create matches */
+    rl_attempted_completion_function = nmcli_editor_tab_completion;
+    /* Use ' ' and '.' as word break characters */
+    rl_completer_word_break_characters = ". ";
+
+    connections = nm_client_get_connections(nmc->client);
+
+    if (!con) {
+        if (con_id && !con_uuid && !con_path && !con_filename) {
+            con      = con_id;
+            selector = "id";
+        } else if (con_uuid && !con_id && !con_path && !con_filename) {
+            con      = con_uuid;
+            selector = "uuid";
+        } else if (con_path && !con_id && !con_uuid && !con_filename) {
+            con      = con_path;
+            selector = "path";
+        } else if (con_filename && !con_path && !con_id && !con_uuid) {
+            con      = con_filename;
+            selector = "filename";
+        } else if (!con_path && !con_id && !con_uuid && !con_filename) {
+            /* no-op */
+        } else {
+            g_string_printf(
+                nmc->return_text,
+                _("Error: only one of 'id', 'filename', uuid, or 'path' can be provided."));
+            nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+            return;
+        }
+    }
+
+    if (con) {
+        /* Existing connection */
+        NMConnection *found_con;
+
+        found_con = nmc_find_connection(connections, selector, con, NULL, nmc->complete);
+        if (nmc->complete)
+            return;
+
+        if (!found_con) {
+            g_string_printf(nmc->return_text, _("Error: Unknown connection '%s'."), con);
+            nmc->return_value = NMC_RESULT_ERROR_NOT_FOUND;
+            return;
+        }
+
+        /* Duplicate the connection and use that so that we need not
+         * differentiate existing vs. new later
+         */
+        connection = nm_simple_connection_new_clone(found_con);
+
+        /* Merge secrets into the connection */
+        update_secrets_in_connection(NM_REMOTE_CONNECTION(found_con), connection);
+
+        s_con           = nm_connection_get_setting_connection(connection);
+        connection_type = nm_setting_connection_get_connection_type(s_con);
+
+        if (type)
+            g_print(_("Warning: editing existing connection '%s'; 'type' argument is ignored\n"),
+                    nm_connection_get_id(connection));
+        if (con_name)
+            g_print(
+                _("Warning: editing existing connection '%s'; 'con-name' argument is ignored\n"),
+                nm_connection_get_id(connection));
+
+        /* Load previously saved history commands for the connection */
+        load_history_cmds(nm_connection_get_uuid(connection));
+
+        editor_init_existing_connection(connection);
+    } else {
+        const char *  slave_type   = NULL;
+        gs_free char *uuid         = NULL;
+        gs_free char *default_name = NULL;
+        gs_free char *tmp_str      = NULL;
+
+        /* New connection */
+        if (nmc->complete) {
+            if (type && argc == 0)
+                nmc_complete_connection_type(type);
+            return;
+        }
+
+        connection_type = check_valid_name_toplevel(type, &slave_type, &err1);
+        tmp_str         = get_valid_options_string_toplevel();
+
+        while (!connection_type) {
+            gs_free char *type_ask = NULL;
+
+            if (!type)
+                g_print(_("Valid connection types: %s\n"), tmp_str);
+            else
+                g_print(_("Error: invalid connection type; %s\n"), err1->message);
+            g_clear_error(&err1);
+
+            type_ask = nmc_readline(&nmc->nmc_config, EDITOR_PROMPT_CON_TYPE);
+            type = type_ask = nm_strstrip(type_ask);
+            connection_type = check_valid_name_toplevel(type_ask, &slave_type, &err1);
+        }
+        nm_clear_g_free(&tmp_str);
+
+        connection = nm_simple_connection_new();
+
+        s_con = (NMSettingConnection *) nm_setting_connection_new();
+        uuid  = nm_utils_uuid_generate();
+        if (con_name)
+            default_name = g_strdup(con_name);
+        else {
+            default_name =
+                nmc_unique_connection_name(connections,
+                                           get_name_alias_toplevel(connection_type, NULL));
+        }
+
+        g_object_set(s_con,
+                     NM_SETTING_CONNECTION_ID,
+                     default_name,
+                     NM_SETTING_CONNECTION_UUID,
+                     uuid,
+                     NM_SETTING_CONNECTION_TYPE,
+                     connection_type,
+                     NULL);
+        nm_connection_add_setting(connection, NM_SETTING(s_con));
+
+        /* Initialize the new connection so that it is valid from the start */
+        editor_init_new_connection(nmc, connection, slave_type);
+    }
+
+    /* nmcli runs the editor */
+    nmc->nmc_config_mutable.in_editor = TRUE;
+
+    g_print("\n");
+    g_print(_("===| nmcli interactive connection editor |==="));
+    g_print("\n\n");
+    if (con)
+        g_print(_("Editing existing '%s' connection: '%s'"), connection_type, con);
+    else
+        g_print(_("Adding a new '%s' connection"), connection_type);
+    g_print("\n\n");
+    /* TRANSLATORS: do not translate 'help', leave it as it is */
+    g_print(_("Type 'help' or '?' for available commands."));
+    g_print("\n");
+    /* TRANSLATORS: do not translate 'print', leave it as it is */
+    g_print(_("Type 'print' to show all the connection properties."));
+    g_print("\n");
+    /* TRANSLATORS: do not translate 'describe', leave it as it is */
+    g_print(_("Type 'describe [<setting>.<prop>]' for detailed property description."));
+    g_print("\n\n");
+
+    nmc_tab_completion.nmc        = nmc;
+    nmc_tab_completion.con_type   = g_strdup(connection_type);
+    nmc_tab_completion.connection = connection;
+
+    /* Run menu loop */
+    editor_menu_main(nmc, connection, connection_type);
+
+    nmc_tab_completion.nmc = NULL;
+    nm_clear_g_free(&nmc_tab_completion.con_type);
+    nmc_tab_completion.connection = NULL;
+
+    return;
+}
+
+static void
+modify_connection_cb(GObject *connection, GAsyncResult *result, gpointer user_data)
+{
+    NmCli *       nmc           = user_data;
+    gs_free_error GError *error = NULL;
+
+    if (!nm_remote_connection_commit_changes_finish(NM_REMOTE_CONNECTION(connection),
+                                                    result,
+                                                    &error)) {
+        g_string_printf(nmc->return_text,
+                        _("Error: Failed to modify connection '%s': %s"),
+                        nm_connection_get_id(NM_CONNECTION(connection)),
+                        error->message);
+        nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+    } else {
+        if (nmc->nmc_config.print_output == NMC_PRINT_PRETTY) {
+            g_print(_("Connection '%s' (%s) successfully modified.\n"),
+                    nm_connection_get_id(NM_CONNECTION(connection)),
+                    nm_connection_get_uuid(NM_CONNECTION(connection)));
+        }
+    }
+    quit();
+}
+
+static void
+do_connection_modify(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    NMConnection *      connection  = NULL;
+    NMRemoteConnection *rc          = NULL;
+    gs_free_error GError *error     = NULL;
+    gboolean              temporary = FALSE;
+
+    if (next_arg(nmc, &argc, &argv, "--temporary", NULL) > 0) {
+        temporary = TRUE;
+        next_arg(nmc, &argc, &argv, NULL);
+    }
+
+    connection = get_connection(nmc, &argc, &argv, NULL, NULL, NULL, &error);
+    if (!connection) {
+        g_string_printf(nmc->return_text, _("Error: %s."), error->message);
+        nmc->return_value = error->code;
+        return;
+    }
+
+    rc = nm_client_get_connection_by_uuid(nmc->client, nm_connection_get_uuid(connection));
+    if (!rc) {
+        g_string_printf(nmc->return_text,
+                        _("Error: Unknown connection '%s'."),
+                        nm_connection_get_uuid(connection));
+        nmc->return_value = NMC_RESULT_ERROR_NOT_FOUND;
+        return;
+    }
+
+    if (!nmc_process_connection_properties(nmc, NM_CONNECTION(rc), &argc, &argv, TRUE, &error)) {
+        g_string_assign(nmc->return_text, error->message);
+        nmc->return_value = error->code;
+        return;
+    }
+
+    if (nmc->complete)
+        return;
+
+    update_connection(rc, temporary, modify_connection_cb, nmc);
+    nmc->should_wait++;
+}
+
+static void
+clone_connection_cb(GObject *client, GAsyncResult *result, gpointer user_data)
+{
+    nm_auto_free_add_connection_info AddConnectionInfo *info = user_data;
+    NmCli *                                             nmc  = info->nmc;
+    gs_unref_object NMRemoteConnection *connection           = NULL;
+    gs_free_error GError *error                              = NULL;
+
+    connection = nm_client_add_connection2_finish(NM_CLIENT(client), result, NULL, &error);
+    if (error) {
+        g_string_printf(nmc->return_text,
+                        _("Error: Failed to add '%s' connection: %s"),
+                        info->new_id,
+                        error->message);
+        nmc->return_value = NMC_RESULT_ERROR_CON_ACTIVATION;
+    } else {
+        g_print(_("%s (%s) cloned as %s (%s).\n"),
+                info->orig_id,
+                info->orig_uuid,
+                nm_connection_get_id(NM_CONNECTION(connection)),
+                nm_connection_get_uuid(NM_CONNECTION(connection)));
+    }
+
+    quit();
+}
+
+static void
+do_connection_clone(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    NMConnection *  connection                   = NULL;
+    gs_unref_object NMConnection *new_connection = NULL;
+    const char *                  new_name;
+    gs_free char *                new_name_free = NULL;
+    gs_free char *                uuid          = NULL;
+    gboolean                      temporary     = FALSE;
+    gs_strfreev char **           arg_arr       = NULL;
+    int                           arg_num;
+    const char *const **          argv_ptr;
+    int *                         argc_ptr;
+    GError *                      error = NULL;
+
+    if (next_arg(nmc, &argc, &argv, "--temporary", NULL) > 0) {
+        temporary = TRUE;
+        next_arg(nmc, &argc, &argv, NULL);
+    }
+
+    argv_ptr = &argv;
+    argc_ptr = &argc;
+
+    if (argc == 0 && nmc->ask) {
+        gs_free char *line = NULL;
+
+        /* nmc_do_cmd() should not call this with argc=0. */
+        g_assert(!nmc->complete);
+
+        line = nmc_readline(&nmc->nmc_config, PROMPT_CONNECTION);
+        nmc_string_to_arg_array(line, NULL, TRUE, &arg_arr, &arg_num);
+        argv_ptr = (const char *const **) &arg_arr;
+        argc_ptr = &arg_num;
+    }
+
+    connection = get_connection(nmc, argc_ptr, argv_ptr, NULL, NULL, NULL, &error);
+    if (!connection) {
+        g_string_printf(nmc->return_text, _("Error: %s."), error->message);
+        nmc->return_value = error->code;
+        return;
+    }
+
+    if (nmc->complete)
+        return;
+
+    if (argv[0])
+        new_name = *argv;
+    else if (nmc->ask) {
+        new_name = new_name_free = nmc_readline(&nmc->nmc_config, _("New connection name: "));
+    } else {
+        g_string_printf(nmc->return_text, _("Error: <new name> argument is missing."));
+        nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+        return;
+    }
+
+    if (next_arg(nmc->ask ? NULL : nmc, argc_ptr, argv_ptr, NULL) == 0) {
+        g_string_printf(nmc->return_text, _("Error: unknown extra argument: '%s'."), *argv);
+        nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+        return;
+    }
+
+    new_connection = nm_simple_connection_new_clone(connection);
+
+    uuid = nm_utils_uuid_generate();
+    g_object_set(nm_connection_get_setting_connection(new_connection),
+                 NM_SETTING_CONNECTION_ID,
+                 new_name,
+                 NM_SETTING_CONNECTION_UUID,
+                 uuid,
+                 NULL);
+
+    update_secrets_in_connection(NM_REMOTE_CONNECTION(connection), new_connection);
+
+    add_connection(nmc->client,
+                   new_connection,
+                   temporary,
+                   clone_connection_cb,
+                   _add_connection_info_new(nmc, connection, new_connection));
+    nmc->should_wait++;
+}
+
+static void
+delete_cb(GObject *con, GAsyncResult *result, gpointer user_data)
+{
+    ConnectionCbInfo *info  = (ConnectionCbInfo *) user_data;
+    GError *          error = NULL;
+
+    if (!nm_remote_connection_delete_finish(NM_REMOTE_CONNECTION(con), result, &error)) {
+        if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+            return;
+        g_string_printf(info->nmc->return_text, _("Error: not all connections deleted."));
+        g_printerr(_("Error: Connection deletion failed: %s\n"), error->message);
+        g_error_free(error);
+        info->nmc->return_value = NMC_RESULT_ERROR_CON_DEL;
+        connection_cb_info_finish(info, con);
+    } else {
+        if (info->nmc->nowait_flag)
+            connection_cb_info_finish(info, con);
+    }
+}
+
+static void
+do_connection_delete(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    NMConnection *       connection;
+    ConnectionCbInfo *   info    = NULL;
+    gs_strfreev char **  arg_arr = NULL;
+    const char *const *  arg_ptr;
+    guint                i;
+    int                  arg_num;
+    nm_auto_free_gstring GString *invalid_cons = NULL;
+    gs_unref_ptrarray GPtrArray *found_cons    = NULL;
+    GError *                     error         = NULL;
+
+    if (nmc->timeout == -1)
+        nmc->timeout = 10;
+
+    next_arg(nmc, &argc, &argv, NULL);
+    arg_ptr = argv;
+    arg_num = argc;
+
+    if (argc == 0) {
+        if (nmc->ask) {
+            gs_free char *line = NULL;
+
+            /* nmc_do_cmd() should not call this with argc=0. */
+            g_assert(!nmc->complete);
+
+            line = nmc_readline(&nmc->nmc_config, PROMPT_CONNECTIONS);
+            nmc_string_to_arg_array(line, NULL, TRUE, &arg_arr, &arg_num);
+            arg_ptr = (const char *const *) arg_arr;
+        }
+        if (arg_num == 0) {
+            g_string_printf(nmc->return_text, _("Error: No connection specified."));
+            nmc->return_value = NMC_RESULT_ERROR_NOT_FOUND;
+            goto finish;
+        }
+    }
+
+    while (arg_num > 0) {
+        const char *cur_selector, *cur_value;
+
+        connection =
+            get_connection(nmc, &arg_num, &arg_ptr, &cur_selector, &cur_value, &found_cons, &error);
+        if (!connection) {
+            if (!nmc->complete)
+                g_printerr(_("Error: %s.\n"), error->message);
+            g_string_printf(nmc->return_text, _("Error: not all connections found."));
+            nmc->return_value = error->code;
+            g_clear_error(&error);
+
+            if (nmc->return_value != NMC_RESULT_ERROR_NOT_FOUND)
+                goto finish;
+
+            if (!invalid_cons)
+                invalid_cons = g_string_new(NULL);
+            if (cur_selector)
+                g_string_append_printf(invalid_cons, "%s '%s', ", cur_selector, cur_value);
+            else
+                g_string_append_printf(invalid_cons, "'%s', ", cur_value);
+        }
+    }
+
+    if (!found_cons) {
+        if (!invalid_cons) {
+            g_string_printf(nmc->return_text, _("Error: No connection specified."));
+            nmc->return_value = NMC_RESULT_ERROR_NOT_FOUND;
+        }
+        goto finish;
+    }
+
+    if (nmc->complete)
+        goto finish;
+
+    info           = g_slice_new0(ConnectionCbInfo);
+    info->nmc      = nmc;
+    info->obj_list = g_ptr_array_sized_new(found_cons->len);
+    for (i = 0; i < found_cons->len; i++) {
+        connection = found_cons->pdata[i];
+        g_ptr_array_add(info->obj_list, g_object_ref(connection));
+    }
+    info->timeout_id  = g_timeout_add_seconds(nmc->timeout, connection_op_timeout_cb, info);
+    info->cancellable = g_cancellable_new();
+
+    nmc->nowait_flag = (nmc->timeout == 0);
+    nmc->should_wait++;
+
+    g_signal_connect(nmc->client,
+                     NM_CLIENT_CONNECTION_REMOVED,
+                     G_CALLBACK(connection_removed_cb),
+                     info);
+
+    for (i = 0; i < found_cons->len; i++) {
+        nm_remote_connection_delete_async(NM_REMOTE_CONNECTION(found_cons->pdata[i]),
+                                          info->cancellable,
+                                          delete_cb,
+                                          info);
+    }
+
+finish:
+    if (invalid_cons) {
+        g_string_truncate(invalid_cons, invalid_cons->len - 2); /* truncate trailing ", " */
+        g_string_printf(nmc->return_text,
+                        _("Error: cannot delete unknown connection(s): %s."),
+                        invalid_cons->str);
+        nmc->return_value = NMC_RESULT_ERROR_NOT_FOUND;
+    }
+}
+
+static void
+connection_changed(NMConnection *connection, NmCli *nmc)
+{
+    g_print(_("%s: connection profile changed\n"), nm_connection_get_id(connection));
+}
+
+static void
+connection_watch(NmCli *nmc, NMConnection *connection)
+{
+    nmc->should_wait++;
+    g_signal_connect(connection, NM_CONNECTION_CHANGED, G_CALLBACK(connection_changed), nmc);
+}
+
+static void
+connection_unwatch(NmCli *nmc, NMConnection *connection)
+{
+    if (g_signal_handlers_disconnect_by_func(connection, G_CALLBACK(connection_changed), nmc))
+        nmc->should_wait--;
+
+    /* Terminate if all the watched connections disappeared. */
+    if (!nmc->should_wait)
+        quit();
+}
+
+static void
+connection_added(NMClient *client, NMRemoteConnection *con, NmCli *nmc)
+{
+    NMConnection *connection = NM_CONNECTION(con);
+
+    g_print(_("%s: connection profile created\n"), nm_connection_get_id(connection));
+    connection_watch(nmc, connection);
+}
+
+static void
+connection_removed(NMClient *client, NMRemoteConnection *con, NmCli *nmc)
+{
+    NMConnection *connection = NM_CONNECTION(con);
+
+    g_print(_("%s: connection profile removed\n"), nm_connection_get_id(connection));
+    connection_unwatch(nmc, connection);
+}
+
+static void
+do_connection_monitor(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    GError *          error = NULL;
+    guint             i;
+    gs_unref_ptrarray GPtrArray *found_cons  = NULL;
+    const GPtrArray *            connections = NULL;
+
+    next_arg(nmc, &argc, &argv, NULL);
+    if (argc == 0) {
+        /* No connections specified. Monitor all. */
+
+        /* nmc_do_cmd() should not call this with argc=0. */
+        g_assert(!nmc->complete);
+
+        connections = nm_client_get_connections(nmc->client);
+    } else {
+        while (argc > 0) {
+            if (!get_connection(nmc, &argc, &argv, NULL, NULL, &found_cons, &error)) {
+                if (!nmc->complete)
+                    g_printerr(_("Error: %s.\n"), error->message);
+                g_string_printf(nmc->return_text, _("Error: not all connections found."));
+                nmc->return_value = error->code;
+                return;
+            }
+
+            if (nmc->complete)
+                continue;
+
+            connections = found_cons;
+        }
+    }
+
+    if (nmc->complete)
+        return;
+
+    for (i = 0; i < connections->len; i++)
+        connection_watch(nmc, connections->pdata[i]);
+
+    if (argc == 0) {
+        /* We'll watch the connection additions too, never exit. */
+        nmc->should_wait++;
+        g_signal_connect(nmc->client,
+                         NM_CLIENT_CONNECTION_ADDED,
+                         G_CALLBACK(connection_added),
+                         nmc);
+    }
+
+    g_signal_connect(nmc->client,
+                     NM_CLIENT_CONNECTION_REMOVED,
+                     G_CALLBACK(connection_removed),
+                     nmc);
+}
+
+static void
+connection_reload_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NmCli *       nmc              = user_data;
+    gs_free_error GError *error    = NULL;
+    gs_unref_variant GVariant *ret = NULL;
+
+    ret = nm_dbus_call_finish(result, &error);
+    if (error) {
+        g_string_printf(nmc->return_text,
+                        _("Error: failed to reload connections: %s."),
+                        nmc_error_get_simple_message(error));
+        nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+    }
+
+    quit();
+}
+
+static void
+do_connection_reload(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    next_arg(nmc, &argc, &argv, NULL);
+    if (nmc->complete)
+        return;
+
+    nmc->should_wait++;
+    nm_dbus_call(G_BUS_TYPE_SYSTEM,
+                 NM_DBUS_SERVICE,
+                 NM_DBUS_PATH_SETTINGS,
+                 NM_DBUS_INTERFACE_SETTINGS,
+                 "ReloadConnections",
+                 g_variant_new("()"),
+                 G_VARIANT_TYPE("(b)"),
+                 NULL,
+                 (nmc->timeout == -1 ? 90 : nmc->timeout) * 1000,
+                 connection_reload_cb,
+                 nmc);
+}
+
+static void
+do_connection_load(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    GError *             error     = NULL;
+    gs_free const char **filenames = NULL;
+    gs_strfreev char **  failures  = NULL;
+    int                  i;
+
+    next_arg(nmc, &argc, &argv, NULL);
+    if (argc == 0) {
+        g_string_printf(nmc->return_text, _("Error: No connection specified."));
+        nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+        return;
+    }
+
+    if (nmc->complete) {
+        nmc->return_value = NMC_RESULT_COMPLETE_FILE;
+        return;
+    }
+
+    filenames = (const char **) nm_utils_strv_dup(argv, argc, FALSE);
+
+    nm_client_load_connections(nmc->client, (char **) filenames, &failures, NULL, &error);
+    if (error) {
+        g_string_printf(nmc->return_text,
+                        _("Error: failed to load connection: %s."),
+                        nmc_error_get_simple_message(error));
+        nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+        g_error_free(error);
+    }
+
+    if (failures) {
+        for (i = 0; failures[i]; i++)
+            g_printerr(_("Could not load file '%s'\n"), failures[i]);
+    }
+}
+
+#define PROMPT_IMPORT_FILE N_("File to import: ")
+
+static void
+do_connection_import(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    gs_free_error GError *error = NULL;
+    const char *          type = NULL, *filename = NULL;
+    gs_free char *        type_ask           = NULL;
+    gs_free char *        filename_ask       = NULL;
+    gs_unref_object NMConnection *connection = NULL;
+    NMVpnEditorPlugin *           plugin;
+    gs_free char *                service_type = NULL;
+    gboolean                      temporary    = FALSE;
+
+    /* Check --temporary */
+    if (next_arg(nmc, &argc, &argv, "--temporary", NULL) > 0) {
+        temporary = TRUE;
+        next_arg(nmc, &argc, &argv, NULL);
+    }
+
+    if (argc == 0) {
+        /* nmc_do_cmd() should not call this with argc=0. */
+        g_assert(!nmc->complete);
+
+        if (nmc->ask) {
+            type_ask =
+                nmc_readline(&nmc->nmc_config, "%s: ", gettext(NM_META_TEXT_PROMPT_VPN_TYPE));
+            type         = nm_strstrip(type_ask);
+            filename_ask = nmc_readline(&nmc->nmc_config, gettext(PROMPT_IMPORT_FILE));
+            filename     = nm_strstrip(filename_ask);
+        } else {
+            g_string_printf(nmc->return_text, _("Error: No arguments provided."));
+            nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+            return;
+        }
+    }
+
+    while (argc > 0) {
+        if (argc == 1 && nmc->complete) {
+            nmc_complete_strings(*argv, type ? NULL : "type", filename ? NULL : "file");
+        }
+
+        if (strcmp(*argv, "type") == 0) {
+            argc--;
+            argv++;
+            if (!argc) {
+                g_string_printf(nmc->return_text, _("Error: %s argument is missing."), *(argv - 1));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return;
+            }
+
+            if (argc == 1 && nmc->complete) {
+                nmc_complete_strings(*argv, "wireguard");
+                complete_option(nmc,
+                                (const NMMetaAbstractInfo *) nm_meta_property_info_vpn_service_type,
+                                *argv,
+                                NULL);
+            }
+
+            if (!type)
+                type = *argv;
+            else
+                g_printerr(_("Warning: 'type' already specified, ignoring extra one.\n"));
+
+        } else if (strcmp(*argv, "file") == 0) {
+            argc--;
+            argv++;
+            if (!argc) {
+                g_string_printf(nmc->return_text, _("Error: %s argument is missing."), *(argv - 1));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return;
+            }
+            if (argc == 1 && nmc->complete)
+                nmc->return_value = NMC_RESULT_COMPLETE_FILE;
+            if (!filename)
+                filename = *argv;
+            else
+                g_printerr(_("Warning: 'file' already specified, ignoring extra one.\n"));
+        } else {
+            g_string_printf(nmc->return_text, _("Error: invalid extra argument '%s'."), *argv);
+            nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+            return;
+        }
+
+        next_arg(nmc, &argc, &argv, NULL);
+    }
+
+    if (nmc->complete)
+        return;
+
+    if (!type) {
+        g_string_printf(nmc->return_text, _("Error: 'type' argument is required."));
+        nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+        return;
+    }
+    if (!filename) {
+        g_string_printf(nmc->return_text, _("Error: 'file' argument is required."));
+        nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+        return;
+    }
+
+    if (nm_streq(type, "wireguard"))
+        connection = nm_vpn_wireguard_import(filename, &error);
+    else {
+        service_type = nm_vpn_plugin_info_list_find_service_type(nm_vpn_get_plugin_infos(), type);
+        if (!service_type) {
+            g_string_printf(nmc->return_text, _("Error: failed to find VPN plugin for %s."), type);
+            nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+            return;
+        }
+
+        /* Import VPN configuration */
+        plugin = nm_vpn_get_editor_plugin(service_type, &error);
+        if (!plugin) {
+            g_string_printf(nmc->return_text,
+                            _("Error: failed to load VPN plugin: %s."),
+                            error->message);
+            nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+            return;
+        }
+
+        connection = nm_vpn_editor_plugin_import(plugin, filename, &error);
+    }
+
+    if (!connection) {
+        g_string_printf(nmc->return_text,
+                        _("Error: failed to import '%s': %s."),
+                        filename,
+                        error->message);
+        nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+        return;
+    }
+
+    add_connection(nmc->client,
+                   connection,
+                   temporary,
+                   add_connection_cb,
+                   _add_connection_info_new(nmc, NULL, connection));
+    nmc->should_wait++;
+}
+
+static void
+do_connection_export(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    NMConnection *     connection   = NULL;
+    const char *       out_name     = NULL;
+    gs_free char *     out_name_ask = NULL;
+    const char *       path         = NULL;
+    const char *       type         = NULL;
+    NMVpnEditorPlugin *plugin;
+    gs_free_error GError *error     = NULL;
+    char                  tmpfile[] = "/tmp/nmcli-export-temp-XXXXXX";
+    gs_strfreev char **   arg_arr   = NULL;
+    int                   arg_num;
+    const char *const **  argv_ptr;
+    int *                 argc_ptr;
+
+    next_arg(nmc, &argc, &argv, NULL);
+    argv_ptr = &argv;
+    argc_ptr = &argc;
+
+    if (argc == 0 && nmc->ask) {
+        gs_free char *line = NULL;
+
+        /* nmc_do_cmd() should not call this with argc=0. */
+        g_assert(!nmc->complete);
+
+        line = nmc_readline(&nmc->nmc_config, PROMPT_VPN_CONNECTION);
+        nmc_string_to_arg_array(line, NULL, TRUE, &arg_arr, &arg_num);
+        argv_ptr = (const char *const **) &arg_arr;
+        argc_ptr = &arg_num;
+    }
+
+    connection = get_connection(nmc, argc_ptr, argv_ptr, NULL, NULL, NULL, &error);
+    if (!connection) {
+        g_string_printf(nmc->return_text, _("Error: %s."), error->message);
+        nmc->return_value = error->code;
+        goto finish;
+    }
+
+    if (nmc->complete)
+        return;
+
+    out_name = *argv;
+
+    if (next_arg(nmc->ask ? NULL : nmc, argc_ptr, argv_ptr, NULL) == 0) {
+        g_string_printf(nmc->return_text, _("Error: unknown extra argument: '%s'."), *argv);
+        nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+        goto finish;
+    }
+
+    if (!out_name && nmc->ask) {
+        out_name = out_name_ask = nmc_readline(&nmc->nmc_config, _("Output file name: "));
+    }
+
+    type = nm_connection_get_connection_type(connection);
+    if (g_strcmp0(type, NM_SETTING_VPN_SETTING_NAME) != 0) {
+        g_string_printf(nmc->return_text, _("Error: the connection is not VPN."));
+        nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+        goto finish;
+    }
+    type = nm_setting_vpn_get_service_type(nm_connection_get_setting_vpn(connection));
+
+    /* Export VPN configuration */
+    plugin = nm_vpn_get_editor_plugin(type, &error);
+    if (!plugin) {
+        g_string_printf(nmc->return_text,
+                        _("Error: failed to load VPN plugin: %s."),
+                        error->message);
+        nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+        goto finish;
+    }
+
+    if (out_name)
+        path = out_name;
+    else {
+        nm_auto_close int fd = -1;
+
+        fd = g_mkstemp_full(tmpfile, O_RDWR | O_CLOEXEC, 0600);
+        if (fd == -1) {
+            g_string_printf(nmc->return_text,
+                            _("Error: failed to create temporary file %s."),
+                            tmpfile);
+            nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+            goto finish;
+        }
+        path = tmpfile;
+    }
+
+    if (!nm_vpn_editor_plugin_export(plugin, path, connection, &error)) {
+        g_string_printf(nmc->return_text,
+                        _("Error: failed to export '%s': %s."),
+                        nm_connection_get_id(connection),
+                        error->message);
+        nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+        goto finish;
+    }
+
+    /* No output file -> copy data to stdout */
+    if (!out_name) {
+        gs_free char *contents = NULL;
+        gsize         len      = 0;
+
+        if (!g_file_get_contents(path, &contents, &len, &error)) {
+            g_string_printf(nmc->return_text,
+                            _("Error: failed to read temporary file '%s': %s."),
+                            path,
+                            error->message);
+            nmc->return_value = NMC_RESULT_ERROR_UNKNOWN;
+            goto finish;
+        }
+        g_print("%s", contents);
+    }
+
+finish:
+    if (!out_name && path)
+        unlink(path);
+}
+
+static char *
+gen_func_connection_names(const char *text, int state)
+{
+    guint            i;
+    const GPtrArray *connections;
+    const char **    connection_names;
+    char *           ret;
+
+    connections = nm_client_get_connections(nm_cli_global_readline->client);
+    if (connections->len == 0)
+        return NULL;
+
+    connection_names = g_new(const char *, connections->len + 1);
+    for (i = 0; i < connections->len; i++)
+        connection_names[i] = nm_connection_get_id(NM_CONNECTION(connections->pdata[i]));
+    connection_names[i] = NULL;
+
+    ret = nmc_rl_gen_func_basic(text, state, connection_names);
+
+    g_free(connection_names);
+    return ret;
+}
+
+static char *
+gen_func_active_connection_names(const char *text, int state)
+{
+    guint            i;
+    const GPtrArray *acs;
+    const char **    connections;
+    char *           ret;
+
+    if (!nm_cli_global_readline->client)
+        return NULL;
+
+    acs = nm_client_get_active_connections(nm_cli_global_readline->client);
+    if (!acs || acs->len == 0)
+        return NULL;
+
+    connections = g_new(const char *, acs->len + 1);
+    for (i = 0; i < acs->len; i++)
+        connections[i] = nm_active_connection_get_id(acs->pdata[i]);
+    connections[i] = NULL;
+
+    ret = nmc_rl_gen_func_basic(text, state, connections);
+
+    g_free(connections);
+    return ret;
+}
+
+static char **
+nmcli_con_tab_completion(const char *text, int start, int end)
+{
+    char **                   match_array    = NULL;
+    rl_compentry_func_t *     generator_func = NULL;
+    const NMMetaAbstractInfo *info;
+
+    /* Disable readline's default filename completion */
+    rl_attempted_completion_over = 1;
+
+    if (g_strcmp0(rl_prompt, PROMPT_CONNECTION) == 0) {
+        /* Disable appending space after completion */
+        rl_completion_append_character = '\0';
+
+        if (!is_single_word(rl_line_buffer))
+            return NULL;
+
+        generator_func = gen_func_connection_names;
+    } else if (g_strcmp0(rl_prompt, PROMPT_CONNECTIONS) == 0) {
+        generator_func = gen_func_connection_names;
+    } else if (g_strcmp0(rl_prompt, PROMPT_ACTIVE_CONNECTIONS) == 0) {
+        generator_func = gen_func_active_connection_names;
+    } else if (rl_prompt && g_str_has_prefix(rl_prompt, NM_META_TEXT_PROMPT_VPN_TYPE)) {
+        info = (const NMMetaAbstractInfo *) nm_meta_property_info_vpn_service_type;
+        nmc_tab_completion.words = _meta_abstract_complete(info, text);
+        generator_func           = _meta_abstract_generator;
+    } else if (g_strcmp0(rl_prompt, PROMPT_IMPORT_FILE) == 0) {
+        rl_attempted_completion_over     = 0;
+        rl_complete_with_tilde_expansion = 1;
+    } else if (g_strcmp0(rl_prompt, PROMPT_VPN_CONNECTION) == 0) {
+        generator_func = gen_vpn_ids;
+    }
+
+    if (generator_func)
+        match_array = rl_completion_matches(text, generator_func);
+
+    nm_clear_pointer(&nmc_tab_completion.words, g_strfreev);
+    return match_array;
+}
+
+void
+nmc_command_func_connection(const NMCCommand *cmd, NmCli *nmc, int argc, const char *const *argv)
+{
+    static const NMCCommand cmds[] = {
+        {"show", do_connections_show, usage_connection_show, TRUE, TRUE},
+        {"up", do_connection_up, usage_connection_up, TRUE, TRUE},
+        {"down", do_connection_down, usage_connection_down, TRUE, TRUE},
+        {"add", do_connection_add, usage_connection_add, TRUE, TRUE},
+        {"edit", do_connection_edit, usage_connection_edit, TRUE, TRUE},
+        {"delete", do_connection_delete, usage_connection_delete, TRUE, TRUE},
+        {"reload", do_connection_reload, usage_connection_reload, FALSE, FALSE},
+        {"load", do_connection_load, usage_connection_load, TRUE, TRUE},
+        {"modify", do_connection_modify, usage_connection_modify, TRUE, TRUE},
+        {"clone", do_connection_clone, usage_connection_clone, TRUE, TRUE},
+        {"import", do_connection_import, usage_connection_import, TRUE, TRUE},
+        {"export", do_connection_export, usage_connection_export, TRUE, TRUE},
+        {"monitor", do_connection_monitor, usage_connection_monitor, TRUE, TRUE},
+        {NULL, do_connections_show, usage, TRUE, TRUE},
+    };
+
+    next_arg(nmc, &argc, &argv, NULL);
+
+    nmc_start_polkit_agent_start_try(nmc);
+
+    /* Set completion function for 'nmcli con' */
+    rl_attempted_completion_function = nmcli_con_tab_completion;
+
+    nmc_do_cmd(nmc, cmds, *argv, argc, argv);
+}
+
+void
+monitor_connections(NmCli *nmc)
+{
+    do_connection_monitor(NULL, nmc, 0, NULL);
+}