From: Lennart Poettering Date: Thu, 28 Aug 2025 11:41:24 +0000 (+0200) Subject: prompt-util: add generic prompt loop implementation X-Git-Tag: v259-rc1~451^2~7 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=fa350969ab10669ff2467359ebf785ce0be39fd9;p=thirdparty%2Fsystemd.git prompt-util: add generic prompt loop implementation This is a generalization of the logic in systemd-firstboot. This also ports over firstboot.c to make use of the new generalization. --- diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c index 5ed6d3a9d2d..e311cb9fa7d 100644 --- a/src/firstboot/firstboot.c +++ b/src/firstboot/firstboot.c @@ -45,6 +45,7 @@ #include "path-util.h" #include "pretty-print.h" #include "proc-cmdline.h" +#include "prompt-util.h" #include "runtime-scope.h" #include "smack-util.h" #include "stat-util.h" @@ -144,102 +145,6 @@ static void print_welcome(int rfd) { done = true; } -static int get_completions( - const char *key, - char ***ret_list, - void *userdata) { - - int r; - - if (!userdata) { - *ret_list = NULL; - return 0; - } - - _cleanup_strv_free_ char **copy = strv_copy(userdata); - if (!copy) - return -ENOMEM; - - r = strv_extend(©, "list"); - if (r < 0) - return r; - - *ret_list = TAKE_PTR(copy); - return 0; -} - -static int prompt_loop( - int rfd, - const char *text, - char **l, - unsigned ellipsize_percentage, - bool (*is_valid)(int rfd, const char *name), - char **ret) { - - int r; - - assert(text); - assert(is_valid); - assert(ret); - - for (;;) { - _cleanup_free_ char *p = NULL; - - r = ask_string_full( - &p, - get_completions, - l, - strv_isempty(l) ? "%s %s (empty to skip): " - : "%s %s (empty to skip, \"list\" to list options): ", - glyph(GLYPH_TRIANGULAR_BULLET), text); - if (r < 0) - return log_error_errno(r, "Failed to query user: %m"); - - if (isempty(p)) { - log_info("No data entered, skipping."); - return 0; - } - - if (!strv_isempty(l)) { - if (streq(p, "list")) { - r = show_menu(l, - /* n_columns= */ 3, - /* column_width= */ 20, - ellipsize_percentage, - /* grey_prefix= */ NULL, - /* with_numbers= */ true); - if (r < 0) - return log_error_errno(r, "Failed to show menu: %m"); - - putchar('\n'); - continue; - } - - unsigned u; - r = safe_atou(p, &u); - if (r >= 0) { - if (u <= 0 || u > strv_length(l)) { - log_error("Specified entry number out of range."); - continue; - } - - log_info("Selected '%s'.", l[u-1]); - return free_and_strdup_warn(ret, l[u-1]); - } - } - - if (is_valid(rfd, p)) - return free_and_replace(*ret, p); - - /* Be more helpful to the user, and give a hint what the user might have wanted to type. */ - const char *best_match = strv_find_closest(l, p); - if (best_match) - log_error("Invalid data '%s', did you mean '%s'?", p, best_match); - else - log_error("Invalid data '%s'.", p); - } -} - static int should_configure(int dir_fd, const char *filename) { _cleanup_fclose_ FILE *passwd = NULL, *shadow = NULL; int r; @@ -309,20 +214,14 @@ static int should_configure(int dir_fd, const char *filename) { return true; } -static bool locale_is_installed_bool(const char *name) { - return locale_is_installed(name) > 0; -} - -static bool locale_is_ok(int rfd, const char *name) { - int r; - - assert(rfd >= 0); +static int locale_is_ok(const char *name, void *userdata) { + int rfd = ASSERT_FD(PTR_TO_FD(userdata)), r; r = dir_fd_is_root(rfd); if (r < 0) log_debug_errno(r, "Unable to determine if operating on host root directory, assuming we are: %m"); - return r != 0 ? locale_is_installed_bool(name) : locale_is_valid(name); + return r != 0 ? locale_is_installed(name) > 0 : locale_is_valid(name); } static int prompt_locale(int rfd) { @@ -379,16 +278,35 @@ static int prompt_locale(int rfd) { } else { print_welcome(rfd); - r = prompt_loop(rfd, "Please enter the new system locale name or number", - locales, 60, locale_is_ok, &arg_locale); + r = prompt_loop("Please enter the new system locale name or number", + GLYPH_WORLD, + locales, + /* accepted= */ NULL, + /* ellipsize_percentage= */ 60, + /* n_columns= */ 3, + /* column_width= */ 20, + locale_is_ok, + /* refresh= */ NULL, + FD_TO_PTR(rfd), + PROMPT_MAY_SKIP|PROMPT_SHOW_MENU, + &arg_locale); if (r < 0) return r; - if (isempty(arg_locale)) return 0; - r = prompt_loop(rfd, "Please enter the new system message locale name or number", - locales, 60, locale_is_ok, &arg_locale_messages); + r = prompt_loop("Please enter the new system message locale name or number", + GLYPH_WORLD, + locales, + /* accepted= */ NULL, + /* ellipsize_percentage= */ 60, + /* n_columns= */ 3, + /* column_width= */ 20, + locale_is_ok, + /* refresh= */ NULL, + FD_TO_PTR(rfd), + PROMPT_MAY_SKIP|PROMPT_SHOW_MENU, + &arg_locale_messages); if (r < 0) return r; @@ -463,20 +381,14 @@ static int process_locale(int rfd) { return 1; } -static bool keymap_exists_bool(const char *name) { - return keymap_exists(name) > 0; -} - -static bool keymap_is_ok(int rfd, const char* name) { - int r; - - assert(rfd >= 0); +static int keymap_is_ok(const char* name, void *userdata) { + int rfd = ASSERT_FD(PTR_TO_FD(userdata)), r; r = dir_fd_is_root(rfd); if (r < 0) log_debug_errno(r, "Unable to determine if operating on host root directory, assuming we are: %m"); - return r != 0 ? keymap_exists_bool(name) : keymap_is_valid(name); + return r != 0 ? keymap_exists(name) > 0 : keymap_is_valid(name); } static int prompt_keymap(int rfd) { @@ -509,8 +421,19 @@ static int prompt_keymap(int rfd) { print_welcome(rfd); - return prompt_loop(rfd, "Please enter the new keymap name or number", - kmaps, 60, keymap_is_ok, &arg_keymap); + return prompt_loop( + "Please enter the new keymap name or number", + GLYPH_KEYBOARD, + kmaps, + /* accepted= */ NULL, + /* ellipsize_percentage= */ 60, + /* n_columns= */ 3, + /* column_width= */ 20, + keymap_is_ok, + /* refresh= */ NULL, + FD_TO_PTR(rfd), + PROMPT_MAY_SKIP|PROMPT_SHOW_MENU, + &arg_keymap); } static int process_keymap(int rfd) { @@ -578,9 +501,7 @@ static int process_keymap(int rfd) { return 1; } -static bool timezone_is_ok(int rfd, const char *name) { - assert(rfd >= 0); - +static int timezone_is_ok(const char *name, void *userdata) { return timezone_is_valid(name, LOG_DEBUG); } @@ -612,8 +533,19 @@ static int prompt_timezone(int rfd) { print_welcome(rfd); - return prompt_loop(rfd, "Please enter the new timezone name or number", - zones, 30, timezone_is_ok, &arg_timezone); + return prompt_loop( + "Please enter the new timezone name or number", + GLYPH_CLOCK, + zones, + /* accepted= */ NULL, + /* ellipsize_percentage= */ 30, + /* n_columns= */ 3, + /* column_width= */ 20, + timezone_is_ok, + /* refresh= */ NULL, + FD_TO_PTR(rfd), + PROMPT_MAY_SKIP|PROMPT_SHOW_MENU, + &arg_timezone); } static int process_timezone(int rfd) { @@ -677,9 +609,7 @@ static int process_timezone(int rfd) { return 0; } -static bool hostname_is_ok(int rfd, const char *name) { - assert(rfd >= 0); - +static int hostname_is_ok(const char *name, void *userdata) { return hostname_is_valid(name, VALID_HOSTNAME_TRAILING_DOT); } @@ -698,8 +628,18 @@ static int prompt_hostname(int rfd) { print_welcome(rfd); - r = prompt_loop(rfd, "Please enter the new hostname", - NULL, 0, hostname_is_ok, &arg_hostname); + r = prompt_loop("Please enter the new hostname", + GLYPH_LABEL, + /* menu= */ NULL, + /* accepted= */ NULL, + /* ellipsize_percentage= */ 100, + /* n_columns= */ 3, + /* column_width= */ 20, + hostname_is_ok, + /* refresh= */ NULL, + FD_TO_PTR(rfd), + PROMPT_MAY_SKIP, + &arg_hostname); if (r < 0) return r; @@ -796,8 +736,8 @@ static int prompt_root_password(int rfd) { print_welcome(rfd); - msg1 = strjoina(glyph(GLYPH_TRIANGULAR_BULLET), " Please enter the new root password (empty to skip):"); - msg2 = strjoina(glyph(GLYPH_TRIANGULAR_BULLET), " Please enter the new root password again:"); + msg1 = strjoina("Please enter the new root password (empty to skip):"); + msg2 = strjoina("Please enter the new root password again:"); suggest_passwords(); @@ -868,8 +808,8 @@ static int find_shell(int rfd, const char *path) { return 0; } -static bool shell_is_ok(int rfd, const char *path) { - assert(rfd >= 0); +static int shell_is_ok(const char *path, void *userdata) { + int rfd = ASSERT_FD(PTR_TO_FD(userdata)); return find_shell(rfd, path) >= 0; } @@ -897,8 +837,19 @@ static int prompt_root_shell(int rfd) { print_welcome(rfd); - return prompt_loop(rfd, "Please enter the new root shell", - NULL, 0, shell_is_ok, &arg_root_shell); + return prompt_loop( + "Please enter the new root shell", + GLYPH_SHELL, + /* menu= */ NULL, + /* accepted= */ NULL, + /* ellipsize_percentage= */ 0, + /* n_columns= */ 3, + /* column_width= */ 20, + shell_is_ok, + /* refresh= */ NULL, + FD_TO_PTR(rfd), + PROMPT_MAY_SKIP, + &arg_root_shell); } static int write_root_passwd(int rfd, int etc_fd, const char *password, const char *shell) { @@ -1727,11 +1678,11 @@ static int run(int argc, char *argv[]) { /* We check these conditions here instead of in parse_argv() so that we can take the root directory * into account. */ - if (arg_keymap && !keymap_is_ok(rfd, arg_keymap)) + if (arg_keymap && !keymap_is_ok(arg_keymap, FD_TO_PTR(rfd))) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Keymap %s is not installed.", arg_keymap); - if (arg_locale && !locale_is_ok(rfd, arg_locale)) + if (arg_locale && !locale_is_ok(arg_locale, FD_TO_PTR(rfd))) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale %s is not installed.", arg_locale); - if (arg_locale_messages && !locale_is_ok(rfd, arg_locale_messages)) + if (arg_locale_messages && !locale_is_ok(arg_locale_messages, FD_TO_PTR(rfd))) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Locale %s is not installed.", arg_locale_messages); if (arg_root_shell) { diff --git a/src/shared/meson.build b/src/shared/meson.build index a734c868913..9bf9f9b6bf3 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -154,6 +154,7 @@ shared_sources = files( 'polkit-agent.c', 'portable-util.c', 'pretty-print.c', + 'prompt-util.c', 'ptyfwd.c', 'qrcode-util.c', 'quota-util.c', diff --git a/src/shared/prompt-util.c b/src/shared/prompt-util.c new file mode 100644 index 00000000000..42ddf191895 --- /dev/null +++ b/src/shared/prompt-util.c @@ -0,0 +1,191 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "glyph-util.h" +#include "log.h" +#include "macro.h" +#include "parse-util.h" +#include "prompt-util.h" +#include "string-util.h" +#include "strv.h" +#include "terminal-util.h" + +static int get_completions( + const char *key, + char ***ret_list, + void *userdata) { + + int r; + + assert(ret_list); + + if (!userdata) { + *ret_list = NULL; + return 0; + } + + _cleanup_strv_free_ char **copy = strv_copy(userdata); + if (!copy) + return -ENOMEM; + + r = strv_extend(©, "list"); + if (r < 0) + return r; + + *ret_list = TAKE_PTR(copy); + return 0; +} + +int prompt_loop( + const char *text, + Glyph emoji, + char **menu, /* if non-NULL: choices to suggest */ + char **accepted, /* if non-NULL: choices to accept (should be a superset of 'menu') */ + unsigned ellipsize_percentage, + size_t n_columns, + size_t column_width, + int (*is_valid)(const char *name, void *userdata), + int (*refresh)(char ***ret_menu, char ***ret_accepted, void *userdata), + void *userdata, + PromptFlags flags, + char **ret) { + + _cleanup_strv_free_ char **refreshed_menu = NULL, **refreshed_accepted = NULL; + int r; + + assert(text); + assert(ret); + + if (!emoji_enabled()) /* If emojis aren't available, simpler unicode chars might still be around, + * hence try to downgrade. (Consider the Linux Console!) */ + emoji = GLYPH_TRIANGULAR_BULLET; + + /* If requested show menu right-away */ + if (FLAGS_SET(flags, PROMPT_SHOW_MENU_NOW) && !strv_isempty(menu)) { + r = show_menu(menu, + n_columns, + column_width, + ellipsize_percentage, + /* grey_prefix= */ NULL, + /* with_numbers= */ true); + if (r < 0) + return log_error_errno(r, "Failed to show menu: %m"); + + putchar('\n'); + } + + for (;;) { + _cleanup_free_ char *a = NULL; + + if (!FLAGS_SET(flags, PROMPT_HIDE_MENU_HINT) && !strv_isempty(menu)) + if (!strextend_with_separator(&a, ", ", "\"list\" to list options")) + return log_oom(); + if (!FLAGS_SET(flags, PROMPT_HIDE_SKIP_HINT) && FLAGS_SET(flags, PROMPT_MAY_SKIP)) + if (!strextend_with_separator(&a, ", ", "empty to skip")) + return log_oom(); + + if (a) { + char *b = strjoin(" (", a, ")"); + if (!b) + return log_oom(); + + free_and_replace(a, b); + } + + _cleanup_free_ char *p = NULL; + r = ask_string_full( + &p, + get_completions, + accepted ?: menu, + "%s%s%s%s: ", + emoji >= 0 ? glyph(emoji) : "", + emoji >= 0 ? " " : "", + text, + strempty(a)); + if (r < 0) + return log_error_errno(r, "Failed to query user: %m"); + + if (isempty(p)) { + if (FLAGS_SET(flags, PROMPT_MAY_SKIP)) { + log_info("No data entered, skipping."); + *ret = NULL; + return 0; + } + + log_info("No data entered, try again."); + continue; + } + + /* NB: here we treat non-NULL but empty list different from NULL list. In the former case we + * support the "list" command, in the latter we don't. */ + if (FLAGS_SET(flags, PROMPT_SHOW_MENU) && streq(p, "list")) { + putchar('\n'); + + if (refresh) { + _cleanup_strv_free_ char **rm = NULL, **ra = NULL; + + /* If a refresh method is provided, then use it now to refresh the menu + * before redisplaying it. */ + r = refresh(&rm, &ra, userdata); + if (r < 0) + return r; + + strv_free_and_replace(refreshed_menu, rm); + strv_free_and_replace(refreshed_accepted, ra); + + menu = refreshed_menu; + accepted = refreshed_accepted; + } + + if (strv_isempty(menu)) { + log_warning("No entries known."); + continue; + } + + r = show_menu(menu, + n_columns, + column_width, + ellipsize_percentage, + /* grey_prefix= */ NULL, + /* with_numbers= */ true); + if (r < 0) + return log_error_errno(r, "Failed to show menu: %m"); + + putchar('\n'); + continue; + } + + unsigned u; + if (safe_atou(p, &u) >= 0) { + if (u <= 0 || u > strv_length(menu)) { + log_error("Specified entry number out of range."); + continue; + } + + log_info("Selected '%s'.", menu[u-1]); + return strdup_to_full(ret, menu[u-1]); + } + + bool good = accepted ? strv_contains(accepted, p) : true; + if (good && is_valid) { + r = is_valid(p, userdata); + if (r < 0) + return r; + + good = good && r; + } + if (good) { + *ret = TAKE_PTR(p); + return 1; + } + + if (!FLAGS_SET(flags, PROMPT_SILENT_VALIDATE)) { + /* Be more helpful to the user, and give a hint what the user might have wanted to type. */ + const char *best_match = strv_find_closest(accepted ?: menu, p); + if (best_match) + log_error("Invalid input '%s', did you mean '%s'?", p, best_match); + else + log_error("Invalid input '%s'.", p); + } + } +} diff --git a/src/shared/prompt-util.h b/src/shared/prompt-util.h new file mode 100644 index 00000000000..179f056b53f --- /dev/null +++ b/src/shared/prompt-util.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "forward.h" + +typedef enum PromptFlags { + PROMPT_MAY_SKIP = 1 << 0, /* Question may be skipped */ + PROMPT_SHOW_MENU = 1 << 1, /* Show menu list on "list" */ + PROMPT_SHOW_MENU_NOW = 1 << 2, /* Show menu list right away, rather than only on request */ + PROMPT_HIDE_MENU_HINT = 1 << 3, /* Don't show hint regarding "list" */ + PROMPT_HIDE_SKIP_HINT = 1 << 4, /* Don't show hint regarding skipping */ + PROMPT_SILENT_VALIDATE = 1 << 5, /* The validation log message logs on its own, don't log again */ +} PromptFlags; + +int prompt_loop(const char *text, + Glyph emoji, + char **menu, + char **accepted, + unsigned ellipsize_percentage, + size_t n_columns, + size_t column_width, + int (*is_valid)(const char *name, void *userdata), + int (*refresh)(char ***ret_menu, char ***ret_accepted, void *userdata), + void *userdata, + PromptFlags flags, + char **ret);