about summary refs log tree commit diff
path: root/src/nmcli/nmcli.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nmcli/nmcli.c')
-rw-r--r--src/nmcli/nmcli.c1051
1 files changed, 1051 insertions, 0 deletions
diff --git a/src/nmcli/nmcli.c b/src/nmcli/nmcli.c
new file mode 100644
index 00000000..e592e054
--- /dev/null
+++ b/src/nmcli/nmcli.c
@@ -0,0 +1,1051 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Jiri Klimes <jklimes@redhat.com>
+ * Copyright (C) 2010 - 2018 Red Hat, Inc.
+ */
+
+#include "libnm-client-aux-extern/nm-default-client.h"
+
+#include "nmcli.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <termios.h>
+#include <unistd.h>
+#include <locale.h>
+#include <glib-unix.h>
+#include <readline/readline.h>
+#include <readline/history.h>
+
+#include "libnmc-base/nm-client-utils.h"
+
+#include "polkit-agent.h"
+#include "utils.h"
+#include "common.h"
+#include "connections.h"
+#include "devices.h"
+#include "settings.h"
+
+#if defined(NM_DIST_VERSION)
+    #define NMCLI_VERSION NM_DIST_VERSION
+#else
+    #define NMCLI_VERSION VERSION
+#endif
+
+#define _NMC_COLOR_PALETTE_INIT()                              \
+    {                                                          \
+        .ansi_seq = {                                          \
+            [NM_META_COLOR_CONNECTION_ACTIVATED]     = "32",   \
+            [NM_META_COLOR_CONNECTION_ACTIVATING]    = "33",   \
+            [NM_META_COLOR_CONNECTION_DISCONNECTING] = "31",   \
+            [NM_META_COLOR_CONNECTION_INVISIBLE]     = "2",    \
+            [NM_META_COLOR_CONNECTION_EXTERNAL]      = "32;2", \
+            [NM_META_COLOR_CONNECTIVITY_FULL]        = "32",   \
+            [NM_META_COLOR_CONNECTIVITY_LIMITED]     = "33",   \
+            [NM_META_COLOR_CONNECTIVITY_NONE]        = "31",   \
+            [NM_META_COLOR_CONNECTIVITY_PORTAL]      = "33",   \
+            [NM_META_COLOR_DEVICE_ACTIVATED]         = "32",   \
+            [NM_META_COLOR_DEVICE_ACTIVATING]        = "33",   \
+            [NM_META_COLOR_DEVICE_DISCONNECTED]      = "31",   \
+            [NM_META_COLOR_DEVICE_FIRMWARE_MISSING]  = "31",   \
+            [NM_META_COLOR_DEVICE_PLUGIN_MISSING]    = "31",   \
+            [NM_META_COLOR_DEVICE_UNAVAILABLE]       = "2",    \
+            [NM_META_COLOR_DEVICE_DISABLED]          = "31",   \
+            [NM_META_COLOR_DEVICE_EXTERNAL]          = "32;2", \
+            [NM_META_COLOR_MANAGER_RUNNING]          = "32",   \
+            [NM_META_COLOR_MANAGER_STARTING]         = "33",   \
+            [NM_META_COLOR_MANAGER_STOPPED]          = "31",   \
+            [NM_META_COLOR_PERMISSION_AUTH]          = "33",   \
+            [NM_META_COLOR_PERMISSION_NO]            = "31",   \
+            [NM_META_COLOR_PERMISSION_YES]           = "32",   \
+            [NM_META_COLOR_STATE_ASLEEP]             = "31",   \
+            [NM_META_COLOR_STATE_CONNECTED_GLOBAL]   = "32",   \
+            [NM_META_COLOR_STATE_CONNECTED_LOCAL]    = "32",   \
+            [NM_META_COLOR_STATE_CONNECTED_SITE]     = "32",   \
+            [NM_META_COLOR_STATE_CONNECTING]         = "33",   \
+            [NM_META_COLOR_STATE_DISCONNECTED]       = "31",   \
+            [NM_META_COLOR_STATE_DISCONNECTING]      = "33",   \
+            [NM_META_COLOR_WIFI_SIGNAL_EXCELLENT]    = "32",   \
+            [NM_META_COLOR_WIFI_SIGNAL_FAIR]         = "35",   \
+            [NM_META_COLOR_WIFI_SIGNAL_GOOD]         = "33",   \
+            [NM_META_COLOR_WIFI_SIGNAL_POOR]         = "36",   \
+            [NM_META_COLOR_WIFI_SIGNAL_UNKNOWN]      = "2",    \
+            [NM_META_COLOR_ENABLED]                  = "32",   \
+            [NM_META_COLOR_DISABLED]                 = "31",   \
+        },                                                     \
+    }
+
+static NmCli nm_cli = {
+    .client = NULL,
+
+    .return_value = NMC_RESULT_SUCCESS,
+
+    .timeout = -1,
+
+    .secret_agent = NULL,
+    .pwds_hash    = NULL,
+    .pk_listener  = NULL,
+
+    .should_wait                 = 0,
+    .nowait_flag                 = TRUE,
+    .nmc_config.print_output     = NMC_PRINT_NORMAL,
+    .nmc_config.multiline_output = FALSE,
+    .mode_specified              = FALSE,
+    .nmc_config.escape_values    = TRUE,
+    .required_fields             = NULL,
+    .ask                         = FALSE,
+    .complete                    = FALSE,
+    .nmc_config.show_secrets     = FALSE,
+    .nmc_config.in_editor        = FALSE,
+    .nmc_config.palette          = _NMC_COLOR_PALETTE_INIT(),
+    .editor_status_line          = FALSE,
+    .editor_save_confirmation    = TRUE,
+};
+
+const NmCli *const nm_cli_global_readline   = &nm_cli;
+const NmCli *const nmc_meta_environment_arg = &nm_cli;
+
+/*****************************************************************************/
+
+typedef struct {
+    NmCli *nmc;
+    int    argc;
+    char **argv;
+} ArgsInfo;
+
+/* --- Global variables --- */
+GMainLoop *    loop = NULL;
+struct termios termios_orig;
+
+NM_CACHED_QUARK_FCN("nmcli-error-quark", nmcli_error_quark);
+
+static void
+complete_field_setting(GHashTable *h, NMMetaSettingType setting_type)
+{
+    const NMMetaSettingInfoEditor *setting_info = &nm_meta_setting_infos_editor[setting_type];
+    guint                          i;
+
+    for (i = 0; i < setting_info->properties_num; i++) {
+        g_hash_table_add(h,
+                         g_strdup_printf("%s.%s",
+                                         setting_info->general->setting_name,
+                                         setting_info->properties[i]->property_name));
+    }
+}
+
+static void
+complete_field(GHashTable *h, const NmcMetaGenericInfo *const *field)
+{
+    int i;
+
+    for (i = 0; field[i]; i++)
+        g_hash_table_add(h, g_strdup(field[i]->name));
+}
+
+static void
+complete_one(gpointer key, gpointer value, gpointer user_data)
+{
+    const char **option_with_value = user_data;
+    const char * option            = option_with_value[0];
+    const char * prefix            = option_with_value[1];
+    const char * name              = key;
+    const char * last;
+
+    last = strrchr(prefix, ',');
+    if (last)
+        last++;
+    else
+        last = prefix;
+
+    if ((!*last && !strchr(name, '.')) || matches(last, name)) {
+        if (option != prefix) {
+            /* value prefix was not a standalone argument,
+             * it was part of --option=<value> argument.
+             * Repeat the part leading to "=". */
+            g_print("%s=", option);
+        }
+        g_print("%.*s%s%s\n",
+                (int) (last - prefix),
+                prefix,
+                name,
+                strcmp(last, name) == 0 ? "," : "");
+    }
+}
+
+static void
+complete_fields(const char *option, const char *prefix)
+{
+    guint       i;
+    GHashTable *h;
+    const char *option_with_value[2] = {option, prefix};
+
+    h = g_hash_table_new_full(nm_str_hash, g_str_equal, g_free, NULL);
+
+    complete_field(h, metagen_ip4_config);
+    complete_field(h, metagen_dhcp_config);
+    complete_field(h, metagen_ip6_config);
+    complete_field(h, metagen_con_show);
+    complete_field(h, metagen_con_active_general);
+    complete_field(h, metagen_con_active_vpn);
+    complete_field(h, nmc_fields_con_active_details_groups);
+    complete_field(h, metagen_device_status);
+    complete_field(h, metagen_device_detail_general);
+    complete_field(h, metagen_device_detail_connections);
+    complete_field(h, metagen_device_detail_capabilities);
+    complete_field(h, metagen_device_detail_wired_properties);
+    complete_field(h, metagen_device_detail_wifi_properties);
+    complete_field(h, metagen_device_detail_wimax_properties);
+    complete_field(h, nmc_fields_dev_wifi_list);
+    complete_field(h, nmc_fields_dev_wimax_list);
+    complete_field(h, nmc_fields_dev_show_master_prop);
+    complete_field(h, nmc_fields_dev_show_team_prop);
+    complete_field(h, nmc_fields_dev_show_vlan_prop);
+    complete_field(h, nmc_fields_dev_show_bluetooth);
+    complete_field(h, nmc_fields_dev_show_sections);
+    complete_field(h, nmc_fields_dev_lldp_list);
+
+    for (i = 0; i < _NM_META_SETTING_TYPE_NUM; i++)
+        complete_field_setting(h, i);
+
+    g_hash_table_foreach(h, complete_one, (gpointer) &option_with_value[0]);
+    g_hash_table_destroy(h);
+}
+
+static void
+complete_option_with_value(const char *option, const char *prefix, ...)
+{
+    va_list     args;
+    const char *candidate;
+
+    va_start(args, prefix);
+    while ((candidate = va_arg(args, const char *))) {
+        if (!*prefix || matches(prefix, candidate)) {
+            if (option != prefix) {
+                /* value prefix was not a standalone argument,
+                 * it was part of --option=<value> argument.
+                 * Repeat the part leading to "=". */
+                g_print("%s=", option);
+            }
+            g_print("%s\n", candidate);
+        }
+    }
+    va_end(args);
+}
+
+static void
+usage(void)
+{
+    g_printerr(_(
+        "Usage: nmcli [OPTIONS] OBJECT { COMMAND | help }\n"
+        "\n"
+        "OPTIONS\n"
+        "  -a, --ask                                ask for missing parameters\n"
+        "  -c, --colors auto|yes|no                 whether to use colors in output\n"
+        "  -e, --escape yes|no                      escape columns separators in values\n"
+        "  -f, --fields <field,...>|all|common      specify fields to output\n"
+        "  -g, --get-values <field,...>|all|common  shortcut for -m tabular -t -f\n"
+        "  -h, --help                               print this help\n"
+        "  -m, --mode tabular|multiline             output mode\n"
+        "  -o, --overview                           overview mode\n"
+        "  -p, --pretty                             pretty output\n"
+        "  -s, --show-secrets                       allow displaying passwords\n"
+        "  -t, --terse                              terse output\n"
+        "  -v, --version                            show program version\n"
+        "  -w, --wait <seconds>                     set timeout waiting for finishing operations\n"
+        "\n"
+        "OBJECT\n"
+        "  g[eneral]       NetworkManager's general status and operations\n"
+        "  n[etworking]    overall networking control\n"
+        "  r[adio]         NetworkManager radio switches\n"
+        "  c[onnection]    NetworkManager's connections\n"
+        "  d[evice]        devices managed by NetworkManager\n"
+        "  a[gent]         NetworkManager secret agent or polkit agent\n"
+        "  m[onitor]       monitor NetworkManager changes\n"
+        "\n"));
+}
+
+static gboolean
+matches_arg(NmCli *nmc, int *argc, const char *const **argv, const char *pattern, char **arg)
+{
+    gs_free char *opt_free = NULL;
+    const char *  opt      = (*argv)[0];
+    gs_free char *arg_tmp  = NULL;
+    const char *  s;
+
+    nm_assert(opt);
+    nm_assert(opt[0] == '-');
+    nm_assert(!arg || !*arg);
+
+    if (nmc->return_value != NMC_RESULT_SUCCESS) {
+        /* Don't process further matches if there has been an error. */
+        return FALSE;
+    }
+
+    if (opt[1] == '-') {
+        /* We know one '-' was already seen by the caller.
+         * Skip it if there's a second one*/
+        opt++;
+    }
+
+    if (arg) {
+        /* If there's a "=" separator, replace it with NUL so that matches()
+         * works and consider the part after it to be the argument's value. */
+        s = strchr(opt, '=');
+        if (s) {
+            opt     = nm_strndup_a(300, opt, s - opt, &opt_free);
+            arg_tmp = g_strdup(&s[1]);
+        }
+    }
+
+    if (!matches(opt, pattern))
+        return FALSE;
+
+    if (arg) {
+        if (arg_tmp)
+            *arg = g_steal_pointer(&arg_tmp);
+        else {
+            /* We need a value, but the option didn't contain a "=<value>" part.
+             * Proceed to the next argument. */
+            if (*argc <= 1) {
+                g_string_printf(nmc->return_text,
+                                _("Error: missing argument for '%s' option."),
+                                opt);
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return FALSE;
+            }
+            (*argc)--;
+            (*argv)++;
+            *arg = g_strdup(*argv[0]);
+        }
+    }
+
+    return TRUE;
+}
+
+/*************************************************************************************/
+
+typedef enum {
+    NMC_USE_COLOR_AUTO,
+    NMC_USE_COLOR_YES,
+    NMC_USE_COLOR_NO,
+} NmcColorOption;
+
+static char *
+check_colors_construct_filename(const char *base_dir,
+                                const char *name,
+                                const char *term,
+                                const char *type)
+{
+    return g_strdup_printf("%s/terminal-colors.d/%s%s%s%s%s",
+                           base_dir,
+                           name ? name : "",
+                           term ? "@" : "",
+                           term ? term : "",
+                           (name || term) ? "." : "",
+                           type);
+}
+
+static NmcColorOption
+check_colors_check_enabled_one_file(const char *base_dir, const char *name, const char *term)
+{
+    gs_free char *filename_e = NULL;
+    gs_free char *filename_d = NULL;
+
+    filename_e = check_colors_construct_filename(base_dir, name, term, "enable");
+    if (g_file_test(filename_e, G_FILE_TEST_EXISTS))
+        return NMC_USE_COLOR_YES;
+
+    filename_d = check_colors_construct_filename(base_dir, name, term, "disable");
+    if (g_file_test(filename_d, G_FILE_TEST_EXISTS))
+        return NMC_USE_COLOR_NO;
+
+    return NMC_USE_COLOR_AUTO;
+}
+
+static char *
+check_colors_check_palette_one_file(const char *base_dir, const char *name, const char *term)
+{
+    static const char *const extensions[] = {
+        "scheme",
+        "schem",
+    };
+    guint i;
+
+    for (i = 0; i < G_N_ELEMENTS(extensions); i++) {
+        gs_free char *filename = NULL;
+        char *        contents;
+
+        filename = check_colors_construct_filename(base_dir, name, term, extensions[i]);
+        if (g_file_get_contents(filename, &contents, NULL, NULL))
+            return contents;
+    }
+
+    return NULL;
+}
+
+static gboolean
+check_colors_check_enabled(const char *base_dir_1,
+                           const char *base_dir_2,
+                           const char *name,
+                           const char *term)
+{
+    int i;
+
+    if (term && strchr(term, '/'))
+        term = NULL;
+
+#define CHECK_AND_RETURN(cmd)                          \
+    G_STMT_START                                       \
+    {                                                  \
+        NmcColorOption _color_option;                  \
+                                                       \
+        _color_option = (cmd);                         \
+        if (_color_option != NMC_USE_COLOR_AUTO)       \
+            return _color_option == NMC_USE_COLOR_YES; \
+    }                                                  \
+    G_STMT_END
+
+    for (i = 0; i < 2; i++) {
+        const char *base_dir = (i == 0 ? base_dir_1 : base_dir_2);
+
+        if (!base_dir)
+            continue;
+        if (name && term)
+            CHECK_AND_RETURN(check_colors_check_enabled_one_file(base_dir, name, term));
+        if (name)
+            CHECK_AND_RETURN(check_colors_check_enabled_one_file(base_dir, name, NULL));
+        if (term)
+            CHECK_AND_RETURN(check_colors_check_enabled_one_file(base_dir, NULL, term));
+        if (TRUE)
+            CHECK_AND_RETURN(check_colors_check_enabled_one_file(base_dir, NULL, NULL));
+    }
+#undef CHECK_AND_RETURN
+    return TRUE;
+}
+
+static char *
+check_colors_check_palette(const char *base_dir_1,
+                           const char *base_dir_2,
+                           const char *name,
+                           const char *term)
+{
+    int i;
+
+    if (term && strchr(term, '/'))
+        term = NULL;
+
+#define CHECK_AND_RETURN(cmd) \
+    G_STMT_START              \
+    {                         \
+        char *_palette;       \
+                              \
+        _palette = (cmd);     \
+        if (_palette)         \
+            return _palette;  \
+    }                         \
+    G_STMT_END
+
+    for (i = 0; i < 2; i++) {
+        const char *base_dir = (i == 0 ? base_dir_1 : base_dir_2);
+
+        if (!base_dir)
+            continue;
+        if (name && term)
+            CHECK_AND_RETURN(check_colors_check_palette_one_file(base_dir, name, term));
+        if (name)
+            CHECK_AND_RETURN(check_colors_check_palette_one_file(base_dir, name, NULL));
+        if (term)
+            CHECK_AND_RETURN(check_colors_check_palette_one_file(base_dir, NULL, term));
+        if (TRUE)
+            CHECK_AND_RETURN(check_colors_check_palette_one_file(base_dir, NULL, NULL));
+    }
+#undef CHECK_AND_RETURN
+    return NULL;
+}
+
+static gboolean
+check_colors(NmcColorOption color_option, char **out_palette_str)
+{
+    const char *      base_dir_1, *base_dir_2;
+    const char *const NAME = "nmcli";
+    const char *      term;
+
+    *out_palette_str = NULL;
+
+    if (!NM_IN_SET(color_option, NMC_USE_COLOR_AUTO, NMC_USE_COLOR_YES)) {
+        /* nothing to do. Colors are disabled. */
+        return FALSE;
+    }
+
+    if (color_option == NMC_USE_COLOR_AUTO && g_getenv("NO_COLOR")) {
+        /* https://no-color.org/ */
+        return FALSE;
+    }
+
+    term = g_getenv("TERM");
+
+    if (color_option == NMC_USE_COLOR_AUTO) {
+        if (nm_streq0(term, "dumb") || !isatty(STDOUT_FILENO))
+            return FALSE;
+    }
+
+    base_dir_1 = g_get_user_config_dir();
+    base_dir_2 = "" SYSCONFDIR;
+
+    if (base_dir_1) {
+        if (nm_streq(base_dir_1, base_dir_2) || !g_file_test(base_dir_1, G_FILE_TEST_EXISTS))
+            base_dir_1 = NULL;
+    }
+    if (!g_file_test(base_dir_2, G_FILE_TEST_EXISTS))
+        base_dir_2 = NULL;
+
+    if (color_option == NMC_USE_COLOR_AUTO
+        && !check_colors_check_enabled(base_dir_1, base_dir_2, NAME, term))
+        return FALSE;
+
+    *out_palette_str = check_colors_check_palette(base_dir_1, base_dir_2, NAME, term);
+    return TRUE;
+}
+
+static NM_UTILS_STRING_TABLE_LOOKUP_DEFINE(
+    _resolve_color_alias,
+    const char *,
+    { nm_assert(name); },
+    { return NULL; },
+    {"black", "30"},
+    {"blink", "5"},
+    {"blue", "34"},
+    {"bold", "1"},
+    {"brown", "33"},
+    {"cyan", "36"},
+    {"darkgray", "90"},
+    {"gray", "37"},
+    {"green", "32"},
+    {"halfbright", "2"},
+    {"lightblue", "94"},
+    {"lightcyan", "96"},
+    {"lightgray", "97"},
+    {"lightgreen", "92"},
+    {"lightmagenta", "95"},
+    {"lightred", "91"},
+    {"magenta", "35"},
+    {"red", "31"},
+    {"reset", "0"},
+    {"reverse", "7"},
+    {"underscore", "4"},
+    {"white", "1;37"},
+    {"yellow", "33" /* well, yellow */}, );
+
+static NM_UTILS_STRING_TABLE_LOOKUP_DEFINE(
+    _nm_meta_color_from_name,
+    NMMetaColor,
+    { nm_assert(name); },
+    { return NM_META_COLOR_NONE; },
+    {"connection-activated", NM_META_COLOR_CONNECTION_ACTIVATED},
+    {"connection-activating", NM_META_COLOR_CONNECTION_ACTIVATING},
+    {"connection-disconnecting", NM_META_COLOR_CONNECTION_DISCONNECTING},
+    {"connection-external", NM_META_COLOR_CONNECTION_EXTERNAL},
+    {"connection-invisible", NM_META_COLOR_CONNECTION_INVISIBLE},
+    {"connection-unknown", NM_META_COLOR_CONNECTION_UNKNOWN},
+    {"connectivity-full", NM_META_COLOR_CONNECTIVITY_FULL},
+    {"connectivity-limited", NM_META_COLOR_CONNECTIVITY_LIMITED},
+    {"connectivity-none", NM_META_COLOR_CONNECTIVITY_NONE},
+    {"connectivity-portal", NM_META_COLOR_CONNECTIVITY_PORTAL},
+    {"connectivity-unknown", NM_META_COLOR_CONNECTIVITY_UNKNOWN},
+    {"device-activated", NM_META_COLOR_DEVICE_ACTIVATED},
+    {"device-activating", NM_META_COLOR_DEVICE_ACTIVATING},
+    {"device-disabled", NM_META_COLOR_DEVICE_DISABLED},
+    {"device-disconnected", NM_META_COLOR_DEVICE_DISCONNECTED},
+    {"device-external", NM_META_COLOR_DEVICE_EXTERNAL},
+    {"device-firmware-missing", NM_META_COLOR_DEVICE_FIRMWARE_MISSING},
+    {"device-plugin-missing", NM_META_COLOR_DEVICE_PLUGIN_MISSING},
+    {"device-unavailable", NM_META_COLOR_DEVICE_UNAVAILABLE},
+    {"device-unknown", NM_META_COLOR_DEVICE_UNKNOWN},
+    {"disabled", NM_META_COLOR_DISABLED},
+    {"enabled", NM_META_COLOR_ENABLED},
+    {"manager-running", NM_META_COLOR_MANAGER_RUNNING},
+    {"manager-starting", NM_META_COLOR_MANAGER_STARTING},
+    {"manager-stopped", NM_META_COLOR_MANAGER_STOPPED},
+    {"permission-auth", NM_META_COLOR_PERMISSION_AUTH},
+    {"permission-no", NM_META_COLOR_PERMISSION_NO},
+    {"permission-unknown", NM_META_COLOR_PERMISSION_UNKNOWN},
+    {"permission-yes", NM_META_COLOR_PERMISSION_YES},
+    {"prompt", NM_META_COLOR_PROMPT},
+    {"state-asleep", NM_META_COLOR_STATE_ASLEEP},
+    {"state-connected-global", NM_META_COLOR_STATE_CONNECTED_GLOBAL},
+    {"state-connected-local", NM_META_COLOR_STATE_CONNECTED_LOCAL},
+    {"state-connected-site", NM_META_COLOR_STATE_CONNECTED_SITE},
+    {"state-connecting", NM_META_COLOR_STATE_CONNECTING},
+    {"state-disconnected", NM_META_COLOR_STATE_DISCONNECTED},
+    {"state-disconnecting", NM_META_COLOR_STATE_DISCONNECTING},
+    {"state-unknown", NM_META_COLOR_STATE_UNKNOWN},
+    {"wifi-signal-excellent", NM_META_COLOR_WIFI_SIGNAL_EXCELLENT},
+    {"wifi-signal-fair", NM_META_COLOR_WIFI_SIGNAL_FAIR},
+    {"wifi-signal-good", NM_META_COLOR_WIFI_SIGNAL_GOOD},
+    {"wifi-signal-poor", NM_META_COLOR_WIFI_SIGNAL_POOR},
+    {"wifi-signal-unknown", NM_META_COLOR_WIFI_SIGNAL_UNKNOWN}, );
+
+static gboolean
+parse_color_scheme(char *palette_buffer, NmcColorPalette *out_palette, GError **error)
+{
+    char *p = palette_buffer;
+
+    nm_assert(out_palette);
+
+    *out_palette = (NmcColorPalette) _NMC_COLOR_PALETTE_INIT();
+
+    /* This reads through the raw color scheme file contents, identifying the
+     * color names and sequences, putting in terminating NULs in place, so that
+     * pointers into the buffer can readily be used as strings in the palette. */
+    while (1) {
+        NMMetaColor name_idx;
+        const char *name;
+        const char *color;
+
+        /* Leading whitespace. */
+        while (nm_utils_is_separator(*p) || *p == '\n')
+            p++;
+
+        if (*p == '\0')
+            break;
+
+        /* Comments. */
+        if (*p == '#') {
+            while (*p != '\n' && *p != '\0')
+                p++;
+            continue;
+        }
+
+        /* Color name. */
+        name = p;
+        while (g_ascii_isgraph(*p))
+            p++;
+        if (*p == '\0') {
+            g_set_error(error, NMCLI_ERROR, 0, _("Unexpected end of file following '%s'\n"), name);
+            return FALSE;
+        }
+
+        /* Separating whitespace. */
+        if (!nm_utils_is_separator(*p)) {
+            *p = '\0';
+            g_set_error(error, NMCLI_ERROR, 0, _("Expected whitespace following '%s'\n"), name);
+            return FALSE;
+        }
+        while (nm_utils_is_separator(*p)) {
+            *p = '\0';
+            p++;
+        }
+
+        /* Color sequence. */
+        color = p;
+        if (!g_ascii_isgraph(*p)) {
+            g_set_error(error, NMCLI_ERROR, 0, _("Expected a value for '%s'\n"), name);
+            return FALSE;
+        }
+        while (g_ascii_isgraph(*p))
+            p++;
+
+        /* Trailing whitespace. */
+        while (nm_utils_is_separator(*p)) {
+            *p = '\0';
+            p++;
+        }
+        if (*p != '\0') {
+            if (*p != '\n') {
+                g_set_error(error,
+                            NMCLI_ERROR,
+                            0,
+                            _("Expected a line break following '%s'\n"),
+                            color);
+                return FALSE;
+            }
+            *p = '\0';
+            p++;
+        }
+
+        name_idx = _nm_meta_color_from_name(name);
+        if (name_idx == NM_META_COLOR_NONE) {
+            g_debug("Ignoring an unrecognized color: '%s'\n", name);
+            continue;
+        }
+
+        out_palette->ansi_seq[name_idx] = _resolve_color_alias(color) ?: color;
+    }
+
+    return TRUE;
+}
+
+static void
+set_colors(NmcColorOption   color_option,
+           bool *           out_use_colors,
+           char **          out_palette_buffer,
+           NmcColorPalette *out_palette)
+{
+    gs_free char *palette_str = NULL;
+    gboolean      use_colors;
+    gboolean      palette_set = FALSE;
+
+    nm_assert(out_use_colors);
+    nm_assert(out_palette);
+    nm_assert(out_palette_buffer && !*out_palette_buffer);
+
+    use_colors = check_colors(color_option, &palette_str);
+
+    *out_use_colors = use_colors;
+
+    if (use_colors && palette_str) {
+        gs_free_error GError *error = NULL;
+        NmcColorPalette       palette;
+
+        if (!parse_color_scheme(palette_str, &palette, &error))
+            g_debug("Error parsing color scheme: %s", error->message);
+        else {
+            *out_palette_buffer = g_steal_pointer(&palette_str);
+            *out_palette        = palette;
+            palette_set         = TRUE;
+        }
+    }
+
+    if (!palette_set)
+        *out_palette = (NmcColorPalette) _NMC_COLOR_PALETTE_INIT();
+}
+
+/*************************************************************************************/
+
+static gboolean
+process_command_line(NmCli *nmc, int argc, char **argv_orig)
+{
+    static const NMCCommand nmcli_cmds[] = {
+        {"general", nmc_command_func_general, NULL, FALSE, FALSE},
+        {"monitor", nmc_command_func_monitor, NULL, TRUE, FALSE},
+        {"networking", nmc_command_func_networking, NULL, FALSE, FALSE},
+        {"radio", nmc_command_func_radio, NULL, FALSE, FALSE},
+        {"connection", nmc_command_func_connection, NULL, FALSE, FALSE},
+        {"device", nmc_command_func_device, NULL, FALSE, FALSE},
+        {"agent", nmc_command_func_agent, NULL, FALSE, FALSE},
+        {NULL, nmc_command_func_overview, usage, TRUE, TRUE},
+    };
+    NmcColorOption     colors = NMC_USE_COLOR_AUTO;
+    const char *       base;
+    const char *const *argv;
+
+    base = strrchr(argv_orig[0], '/');
+    if (base == NULL)
+        base = argv_orig[0];
+    else
+        base++;
+
+    if (argc > 1 && nm_streq(argv_orig[1], "--complete-args")) {
+        nmc->complete = TRUE;
+        argv_orig[1]  = argv_orig[0];
+        argc--;
+        argv_orig++;
+    }
+
+    argv = (const char *const *) argv_orig;
+
+    next_arg(nmc, &argc, &argv, NULL);
+
+    /* parse options */
+    while (argc) {
+        gs_free char *value = NULL;
+
+        if (argv[0][0] != '-')
+            break;
+
+        if (argc == 1 && nmc->complete) {
+            nmc_complete_strings(argv[0],
+                                 "--terse",
+                                 "--pretty",
+                                 "--mode",
+                                 "--overview",
+                                 "--colors",
+                                 "--escape",
+                                 "--fields",
+                                 "--nocheck",
+                                 "--get-values",
+                                 "--wait",
+                                 "--version",
+                                 "--help");
+        }
+
+        if (argv[0][1] == '-' && argv[0][2] == '\0') {
+            /* '--' ends options */
+            next_arg(nmc, &argc, &argv, NULL);
+            break;
+        }
+
+        if (matches_arg(nmc, &argc, &argv, "-overview", NULL)) {
+            nmc->nmc_config_mutable.overview = TRUE;
+        } else if (matches_arg(nmc, &argc, &argv, "-terse", NULL)) {
+            if (nmc->nmc_config.print_output == NMC_PRINT_TERSE) {
+                g_string_printf(nmc->return_text,
+                                _("Error: Option '--terse' is specified the second time."));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return FALSE;
+            } else if (nmc->nmc_config.print_output == NMC_PRINT_PRETTY) {
+                g_string_printf(
+                    nmc->return_text,
+                    _("Error: Option '--terse' is mutually exclusive with '--pretty'."));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return FALSE;
+            } else
+                nmc->nmc_config_mutable.print_output = NMC_PRINT_TERSE;
+        } else if (matches_arg(nmc, &argc, &argv, "-pretty", NULL)) {
+            if (nmc->nmc_config.print_output == NMC_PRINT_PRETTY) {
+                g_string_printf(nmc->return_text,
+                                _("Error: Option '--pretty' is specified the second time."));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return FALSE;
+            } else if (nmc->nmc_config.print_output == NMC_PRINT_TERSE) {
+                g_string_printf(
+                    nmc->return_text,
+                    _("Error: Option '--pretty' is mutually exclusive with '--terse'."));
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return FALSE;
+            } else
+                nmc->nmc_config_mutable.print_output = NMC_PRINT_PRETTY;
+        } else if (matches_arg(nmc, &argc, &argv, "-mode", &value)) {
+            nmc->mode_specified = TRUE;
+            if (argc == 1 && nmc->complete)
+                complete_option_with_value(argv[0], value, "tabular", "multiline", NULL);
+            if (matches(value, "tabular"))
+                nmc->nmc_config_mutable.multiline_output = FALSE;
+            else if (matches(value, "multiline"))
+                nmc->nmc_config_mutable.multiline_output = TRUE;
+            else {
+                g_string_printf(nmc->return_text,
+                                _("Error: '%s' is not a valid argument for '%s' option."),
+                                value,
+                                argv[0]);
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return FALSE;
+            }
+        } else if (matches_arg(nmc, &argc, &argv, "-colors", &value)) {
+            if (argc == 1 && nmc->complete)
+                complete_option_with_value(argv[0], value, "yes", "no", "auto", NULL);
+            if (matches(value, "auto"))
+                colors = NMC_USE_COLOR_AUTO;
+            else if (matches(value, "yes"))
+                colors = NMC_USE_COLOR_YES;
+            else if (matches(value, "no"))
+                colors = NMC_USE_COLOR_NO;
+            else {
+                g_string_printf(nmc->return_text,
+                                _("Error: '%s' is not valid argument for '%s' option."),
+                                value,
+                                argv[0]);
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return FALSE;
+            }
+        } else if (matches_arg(nmc, &argc, &argv, "-escape", &value)) {
+            if (argc == 1 && nmc->complete)
+                complete_option_with_value(argv[0], value, "yes", "no", NULL);
+            if (matches(value, "yes"))
+                nmc->nmc_config_mutable.escape_values = TRUE;
+            else if (matches(value, "no"))
+                nmc->nmc_config_mutable.escape_values = FALSE;
+            else {
+                g_string_printf(nmc->return_text,
+                                _("Error: '%s' is not valid argument for '%s' option."),
+                                value,
+                                argv[0]);
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return FALSE;
+            }
+        } else if (matches_arg(nmc, &argc, &argv, "-fields", &value)) {
+            if (argc == 1 && nmc->complete)
+                complete_fields(argv[0], value);
+            nmc->required_fields = g_strdup(value);
+        } else if (matches_arg(nmc, &argc, &argv, "-get-values", &value)) {
+            if (argc == 1 && nmc->complete)
+                complete_fields(argv[0], value);
+            nmc->required_fields                 = g_strdup(value);
+            nmc->nmc_config_mutable.print_output = NMC_PRINT_TERSE;
+            /* We want fixed tabular mode here, but just set the mode specified and rely on defaults:
+             * in this way we allow use of "-m multiline" to swap the output mode also if placed
+             * before the "-g <field>" option (-g may be still more practical and easy to remember than -t -f).
+             */
+            nmc->mode_specified = TRUE;
+        } else if (matches_arg(nmc, &argc, &argv, "-nocheck", NULL)) {
+            /* ignore for backward compatibility */
+        } else if (matches_arg(nmc, &argc, &argv, "-wait", &value)) {
+            unsigned long timeout;
+
+            if (!nmc_string_to_uint(value, TRUE, 0, G_MAXINT, &timeout)) {
+                g_string_printf(nmc->return_text, _("Error: '%s' is not a valid timeout."), value);
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+                return FALSE;
+            }
+            nmc->timeout = (int) timeout;
+        } else if (matches_arg(nmc, &argc, &argv, "-version", NULL)) {
+            if (!nmc->complete)
+                g_print(_("nmcli tool, version %s\n"), NMCLI_VERSION);
+            return NMC_RESULT_SUCCESS;
+        } else if (matches_arg(nmc, &argc, &argv, "-help", NULL)) {
+            if (!nmc->complete)
+                usage();
+            return NMC_RESULT_SUCCESS;
+        } else {
+            if (nmc->return_value == NMC_RESULT_SUCCESS) {
+                g_string_printf(nmc->return_text,
+                                _("Error: Option '%s' is unknown, try 'nmcli -help'."),
+                                argv[0]);
+                nmc->return_value = NMC_RESULT_ERROR_USER_INPUT;
+            }
+            return FALSE;
+        }
+
+        next_arg(nmc, &argc, &argv, NULL);
+    }
+
+    /* Ignore --overview when fields are set explicitly */
+    if (nmc->required_fields)
+        nmc->nmc_config_mutable.overview = FALSE;
+
+    set_colors(colors,
+               &nmc->nmc_config_mutable.use_colors,
+               &nmc->palette_buffer,
+               &nmc->nmc_config_mutable.palette);
+
+    /* Now run the requested command */
+    nmc_do_cmd(nmc, nmcli_cmds, *argv, argc, argv);
+
+    return TRUE;
+}
+
+static gboolean nmcli_sigint = FALSE;
+
+gboolean
+nmc_seen_sigint(void)
+{
+    return nmcli_sigint;
+}
+
+void
+nmc_clear_sigint(void)
+{
+    nmcli_sigint = FALSE;
+}
+
+void
+nmc_exit(void)
+{
+    tcsetattr(STDIN_FILENO, TCSADRAIN, &termios_orig);
+    nmc_cleanup_readline();
+    exit(1);
+}
+
+static gboolean
+signal_handler(gpointer user_data)
+{
+    int signo = GPOINTER_TO_INT(user_data);
+
+    switch (signo) {
+    case SIGINT:
+        if (nmc_get_in_readline()) {
+            nmcli_sigint = TRUE;
+        } else {
+            nm_cli.return_value = 0x80 + signo;
+            g_string_printf(nm_cli.return_text,
+                            _("Error: nmcli terminated by signal %s (%d)"),
+                            strsignal(signo),
+                            signo);
+            g_main_loop_quit(loop);
+        }
+        break;
+    case SIGTERM:
+        nm_cli.return_value = 0x80 + signo;
+        g_string_printf(nm_cli.return_text,
+                        _("Error: nmcli terminated by signal %s (%d)"),
+                        strsignal(signo),
+                        signo);
+        nmc_exit();
+        break;
+    }
+
+    return G_SOURCE_CONTINUE;
+}
+
+void
+nm_cli_spawn_pager(const NmcConfig *nmc_config, NmcPagerData *pager_data)
+{
+    if (pager_data->pid != 0)
+        return;
+    pager_data->pid = nmc_terminal_spawn_pager(nmc_config);
+}
+
+static void
+nmc_cleanup(NmCli *nmc)
+{
+    pid_t ret;
+
+    g_clear_object(&nmc->client);
+
+    if (nmc->return_text)
+        g_string_free(g_steal_pointer(&nmc->return_text), TRUE);
+
+    if (nmc->secret_agent) {
+        nm_secret_agent_old_unregister(NM_SECRET_AGENT_OLD(nmc->secret_agent), NULL, NULL);
+        g_clear_object(&nmc->secret_agent);
+    }
+
+    nm_clear_pointer(&nmc->pwds_hash, g_hash_table_destroy);
+
+    nm_clear_g_free(&nmc->required_fields);
+
+    if (nmc->pager_data.pid != 0) {
+        pid_t pid = nm_steal_int(&nmc->pager_data.pid);
+
+        fclose(stdout);
+        fclose(stderr);
+        do {
+            ret = waitpid(pid, NULL, 0);
+        } while (ret == -1 && errno == EINTR);
+    }
+
+    nm_clear_g_free(&nmc->palette_buffer);
+
+    nmc_polkit_agent_fini(nmc);
+}
+
+int
+main(int argc, char *argv[])
+{
+    /* Set locale to use environment variables */
+    setlocale(LC_ALL, "");
+
+#ifdef GETTEXT_PACKAGE
+    /* Set i18n stuff */
+    bindtextdomain(GETTEXT_PACKAGE, NMLOCALEDIR);
+    bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+    textdomain(GETTEXT_PACKAGE);
+#endif
+
+    /* Save terminal settings */
+    tcgetattr(STDIN_FILENO, &termios_orig);
+
+    nm_cli.return_text = g_string_new(_("Success"));
+    loop               = g_main_loop_new(NULL, FALSE);
+
+    g_unix_signal_add(SIGTERM, signal_handler, GINT_TO_POINTER(SIGTERM));
+    g_unix_signal_add(SIGINT, signal_handler, GINT_TO_POINTER(SIGINT));
+
+    if (process_command_line(&nm_cli, argc, argv))
+        g_main_loop_run(loop);
+
+    if (nm_cli.complete) {
+        /* Remove error statuses from command completion runs. */
+        if (nm_cli.return_value < NMC_RESULT_COMPLETE_FILE)
+            nm_cli.return_value = NMC_RESULT_SUCCESS;
+    } else if (nm_cli.return_value != NMC_RESULT_SUCCESS) {
+        /* Print result descripting text */
+        g_printerr("%s\n", nm_cli.return_text->str);
+    }
+
+    nmc_cleanup(&nm_cli);
+    g_main_loop_unref(loop);
+
+    return nm_cli.return_value;
+}