mirror of
https://github.com/morgan9e/systemd
synced 2026-04-14 08:25:20 +09:00
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.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
191
src/shared/prompt-util.c
Normal file
191
src/shared/prompt-util.c
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/shared/prompt-util.h
Normal file
28
src/shared/prompt-util.h
Normal file
@@ -0,0 +1,28 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#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);
|
||||
Reference in New Issue
Block a user