mirror of
https://github.com/systemd/systemd.git
synced 2025-03-28 02:50:16 +03:00
firstboot: add tab completion for interactive questions (#36271)
let's make it easier and quicker to go through firstboot questions by providing TAB completion
This commit is contained in:
commit
78367395a1
@ -596,6 +596,13 @@ SYSTEMD_HOME_DEBUG_SUFFIX=foo \
|
||||
parts of the session continue running. Thus, we highly recommend that this variable
|
||||
isn't used unless necessary. Defaults to true.
|
||||
|
||||
`homectl`:
|
||||
|
||||
* `$SYSTEMD_HOME_FIRSTBOOT_OVERRIDE` – if set to "1" will make `homectl
|
||||
firstboot --prompt-new-user` interactively ask for user creation, even if
|
||||
there already exists at least one regular user on the system. If set to "0"
|
||||
will make the tool skip any such query.
|
||||
|
||||
`kernel-install`:
|
||||
|
||||
* `$KERNEL_INSTALL_BYPASS` – If set to "1", execution of kernel-install is skipped
|
||||
|
@ -1470,3 +1470,19 @@ char* strrstr(const char *haystack, const char *needle) {
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t str_common_prefix(const char *a, const char *b) {
|
||||
assert(a);
|
||||
assert(b);
|
||||
|
||||
/* Returns the length of the common prefix of the two specified strings, or SIZE_MAX in case the
|
||||
* strings are fully identical. */
|
||||
|
||||
for (size_t n = 0;; n++) {
|
||||
char c = a[n];
|
||||
if (c != b[n])
|
||||
return n;
|
||||
if (c == 0)
|
||||
return SIZE_MAX;
|
||||
}
|
||||
}
|
||||
|
@ -308,3 +308,5 @@ bool version_is_valid_versionspec(const char *s);
|
||||
ssize_t strlevenshtein(const char *x, const char *y);
|
||||
|
||||
char* strrstr(const char *haystack, const char *needle);
|
||||
|
||||
size_t str_common_prefix(const char *a, const char *b);
|
||||
|
@ -1238,3 +1238,24 @@ int strv_rebreak_lines(char **l, size_t width, char ***ret) {
|
||||
*ret = TAKE_PTR(broken);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char** strv_filter_prefix(char *const*l, const char *prefix) {
|
||||
_cleanup_strv_free_ char **f = NULL;
|
||||
|
||||
/* Allocates a copy of 'l', but only copies over entries starting with 'prefix' */
|
||||
|
||||
if (isempty(prefix))
|
||||
return strv_copy(l);
|
||||
|
||||
size_t sz = 0;
|
||||
|
||||
STRV_FOREACH(i, l) {
|
||||
if (!startswith(*i, prefix))
|
||||
continue;
|
||||
|
||||
if (strv_extend_with_size(&f, &sz, *i) < 0)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return TAKE_PTR(f);
|
||||
}
|
||||
|
@ -268,3 +268,5 @@ int _string_strv_ordered_hashmap_put(OrderedHashmap **h, const char *key, const
|
||||
#define string_strv_ordered_hashmap_put(h, k, v) _string_strv_ordered_hashmap_put(h, k, v HASHMAP_DEBUG_SRC_ARGS)
|
||||
|
||||
int strv_rebreak_lines(char **l, size_t width, char ***ret);
|
||||
|
||||
char** strv_filter_prefix(char *const*l, const char *prefix);
|
||||
|
@ -26,6 +26,7 @@
|
||||
#include "constants.h"
|
||||
#include "devnum-util.h"
|
||||
#include "env-util.h"
|
||||
#include "errno-list.h"
|
||||
#include "fd-util.h"
|
||||
#include "fileio.h"
|
||||
#include "fs-util.h"
|
||||
@ -103,22 +104,25 @@ int chvt(int vt) {
|
||||
return RET_NERRNO(ioctl(fd, VT_ACTIVATE, vt));
|
||||
}
|
||||
|
||||
int read_one_char(FILE *f, char *ret, usec_t t, bool *need_nl) {
|
||||
int read_one_char(FILE *f, char *ret, usec_t t, bool echo, bool *need_nl) {
|
||||
_cleanup_free_ char *line = NULL;
|
||||
struct termios old_termios;
|
||||
int r, fd;
|
||||
|
||||
assert(f);
|
||||
assert(ret);
|
||||
|
||||
if (!f)
|
||||
f = stdin;
|
||||
|
||||
/* If this is a terminal, then switch canonical mode off, so that we can read a single
|
||||
* character. (Note that fmemopen() streams do not have an fd associated with them, let's handle that
|
||||
* nicely.) */
|
||||
* nicely.) If 'echo' is false we'll also disable ECHO mode so that the pressed key is not made
|
||||
* visible to the user. */
|
||||
fd = fileno(f);
|
||||
if (fd >= 0 && tcgetattr(fd, &old_termios) >= 0) {
|
||||
struct termios new_termios = old_termios;
|
||||
|
||||
new_termios.c_lflag &= ~ICANON;
|
||||
new_termios.c_lflag &= ~(ICANON|(echo ? 0 : ECHO));
|
||||
new_termios.c_cc[VMIN] = 1;
|
||||
new_termios.c_cc[VTIME] = 0;
|
||||
|
||||
@ -201,7 +205,7 @@ int ask_char(char *ret, const char *replies, const char *fmt, ...) {
|
||||
|
||||
fflush(stdout);
|
||||
|
||||
r = read_one_char(stdin, &c, DEFAULT_ASK_REFRESH_USEC, &need_nl);
|
||||
r = read_one_char(stdin, &c, DEFAULT_ASK_REFRESH_USEC, /* echo= */ true, &need_nl);
|
||||
if (r < 0) {
|
||||
|
||||
if (r == -ETIMEDOUT)
|
||||
@ -228,94 +232,365 @@ int ask_char(char *ret, const char *replies, const char *fmt, ...) {
|
||||
}
|
||||
}
|
||||
|
||||
int ask_string(char **ret, const char *text, ...) {
|
||||
_cleanup_free_ char *line = NULL;
|
||||
typedef enum CompletionResult{
|
||||
COMPLETION_ALREADY, /* the input string is already complete */
|
||||
COMPLETION_FULL, /* completed the input string to be complete now */
|
||||
COMPLETION_PARTIAL, /* completed the input string so that is still incomplete */
|
||||
COMPLETION_NONE, /* found no matching completion */
|
||||
_COMPLETION_RESULT_MAX,
|
||||
_COMPLETION_RESULT_INVALID = -EINVAL,
|
||||
_COMPLETION_RESULT_ERRNO_MAX = -ERRNO_MAX,
|
||||
} CompletionResult;
|
||||
|
||||
static CompletionResult pick_completion(const char *string, char *const*completions, char **ret) {
|
||||
_cleanup_free_ char *found = NULL;
|
||||
bool partial = false;
|
||||
|
||||
string = strempty(string);
|
||||
|
||||
STRV_FOREACH(c, completions) {
|
||||
|
||||
/* Ignore entries that are not actually completions */
|
||||
if (!startswith(*c, string))
|
||||
continue;
|
||||
|
||||
/* Store first completion that matches */
|
||||
if (!found) {
|
||||
found = strdup(*c);
|
||||
if (!found)
|
||||
return -ENOMEM;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/* If there's another completion that works truncate the one we already found by common
|
||||
* prefix */
|
||||
size_t n = str_common_prefix(found, *c);
|
||||
if (n == SIZE_MAX)
|
||||
continue;
|
||||
|
||||
found[n] = 0;
|
||||
partial = true;
|
||||
}
|
||||
|
||||
*ret = TAKE_PTR(found);
|
||||
|
||||
if (!*ret)
|
||||
return COMPLETION_NONE;
|
||||
if (partial)
|
||||
return COMPLETION_PARTIAL;
|
||||
|
||||
return streq(string, *ret) ? COMPLETION_ALREADY : COMPLETION_FULL;
|
||||
}
|
||||
|
||||
static void clear_by_backspace(size_t n) {
|
||||
/* Erase the specified number of character cells backwards on the terminal */
|
||||
for (size_t i = 0; i < n; i++)
|
||||
fputs("\b \b", stdout);
|
||||
}
|
||||
|
||||
int ask_string_full(
|
||||
char **ret,
|
||||
GetCompletionsCallback get_completions,
|
||||
void *userdata,
|
||||
const char *text, ...) {
|
||||
|
||||
va_list ap;
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
assert(text);
|
||||
|
||||
/* Output the prompt */
|
||||
fputs(ansi_highlight(), stdout);
|
||||
|
||||
va_start(ap, text);
|
||||
vprintf(text, ap);
|
||||
va_end(ap);
|
||||
|
||||
fputs(ansi_normal(), stdout);
|
||||
|
||||
fflush(stdout);
|
||||
|
||||
r = read_line(stdin, LONG_LINE_MAX, &line);
|
||||
_cleanup_free_ char *string = NULL;
|
||||
size_t n = 0;
|
||||
|
||||
/* Do interactive logic only if stdin + stdout are connected to the same place. And yes, we could use
|
||||
* STDIN_FILENO and STDOUT_FILENO here, but let's be overly correct for once, after all libc allows
|
||||
* swapping out stdin/stdout. */
|
||||
int fd_input = fileno(stdin);
|
||||
int fd_output = fileno(stdout);
|
||||
if (fd_input < 0 || fd_output < 0 || same_fd(fd_input, fd_output) <= 0)
|
||||
goto fallback;
|
||||
|
||||
/* Try to disable echo, which also tells us if this even is a terminal */
|
||||
struct termios old_termios;
|
||||
if (tcgetattr(fd_input, &old_termios) < 0)
|
||||
goto fallback;
|
||||
|
||||
struct termios new_termios = old_termios;
|
||||
termios_disable_echo(&new_termios);
|
||||
if (tcsetattr(fd_input, TCSADRAIN, &new_termios) < 0)
|
||||
return -errno;
|
||||
|
||||
for (;;) {
|
||||
int c = fgetc(stdin);
|
||||
|
||||
/* On EOF or NUL, end the request, don't output anything anymore */
|
||||
if (IN_SET(c, EOF, 0))
|
||||
break;
|
||||
|
||||
/* On Return also end the request, but make this visible */
|
||||
if (IN_SET(c, '\n', '\r')) {
|
||||
fputc('\n', stdout);
|
||||
break;
|
||||
}
|
||||
|
||||
if (c == '\t') {
|
||||
/* Tab */
|
||||
|
||||
_cleanup_strv_free_ char **completions = NULL;
|
||||
if (get_completions) {
|
||||
r = get_completions(string, &completions, userdata);
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
}
|
||||
|
||||
_cleanup_free_ char *new_string = NULL;
|
||||
CompletionResult cr = pick_completion(string, completions, &new_string);
|
||||
if (cr < 0) {
|
||||
r = cr;
|
||||
goto fail;
|
||||
}
|
||||
if (IN_SET(cr, COMPLETION_PARTIAL, COMPLETION_FULL)) {
|
||||
/* Output the new suffix we learned */
|
||||
fputs(ASSERT_PTR(startswith(new_string, strempty(string))), stdout);
|
||||
|
||||
/* And update the whole string */
|
||||
free_and_replace(string, new_string);
|
||||
n = strlen(string);
|
||||
}
|
||||
if (cr == COMPLETION_NONE)
|
||||
fputc('\a', stdout); /* BEL */
|
||||
|
||||
if (IN_SET(cr, COMPLETION_PARTIAL, COMPLETION_ALREADY)) {
|
||||
/* If this worked only partially, or if the user hit TAB even though we were
|
||||
* complete already, then show the remaining options (in the latter case just
|
||||
* the one). */
|
||||
fputc('\n', stdout);
|
||||
|
||||
_cleanup_strv_free_ char **filtered = strv_filter_prefix(completions, string);
|
||||
if (!filtered) {
|
||||
r = -ENOMEM;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
r = show_menu(filtered,
|
||||
/* n_columns= */ SIZE_MAX,
|
||||
/* column_width= */ SIZE_MAX,
|
||||
/* ellipsize_percentage= */ 0,
|
||||
/* grey_prefix=*/ string,
|
||||
/* with_numbers= */ false);
|
||||
if (r < 0)
|
||||
goto fail;
|
||||
|
||||
/* Show the prompt again */
|
||||
fputs(ansi_highlight(), stdout);
|
||||
va_start(ap, text);
|
||||
vprintf(text, ap);
|
||||
va_end(ap);
|
||||
fputs(ansi_normal(), stdout);
|
||||
fputs(string, stdout);
|
||||
}
|
||||
|
||||
} else if (IN_SET(c, '\b', 127)) {
|
||||
/* Backspace */
|
||||
|
||||
if (n == 0)
|
||||
fputc('\a', stdout); /* BEL */
|
||||
else {
|
||||
size_t m = utf8_last_length(string, n);
|
||||
|
||||
char *e = string + n - m;
|
||||
clear_by_backspace(utf8_console_width(e));
|
||||
|
||||
*e = 0;
|
||||
n -= m;
|
||||
}
|
||||
|
||||
} else if (c == 21) {
|
||||
/* Ctrl-u → erase all input */
|
||||
|
||||
clear_by_backspace(utf8_console_width(string));
|
||||
string[n = 0] = 0;
|
||||
|
||||
} else if (c == 4) {
|
||||
/* Ctrl-d → cancel this field input */
|
||||
|
||||
r = -ECANCELED;
|
||||
goto fail;
|
||||
|
||||
} else if (char_is_cc(c) || n >= LINE_MAX)
|
||||
/* refuse control characters and too long strings */
|
||||
fputc('\a', stdout); /* BEL */
|
||||
else {
|
||||
/* Regular char */
|
||||
|
||||
if (!GREEDY_REALLOC(string, n+2)) {
|
||||
r = -ENOMEM;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
string[n++] = (char) c;
|
||||
string[n] = 0;
|
||||
|
||||
fputc(c, stdout);
|
||||
}
|
||||
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
if (tcsetattr(fd_input, TCSADRAIN, &old_termios) < 0)
|
||||
return -errno;
|
||||
|
||||
if (!string) {
|
||||
string = strdup("");
|
||||
if (!string)
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
*ret = TAKE_PTR(string);
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
(void) tcsetattr(fd_input, TCSADRAIN, &old_termios);
|
||||
return r;
|
||||
|
||||
fallback:
|
||||
/* A simple fallback without TTY magic */
|
||||
r = read_line(stdin, LONG_LINE_MAX, &string);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0)
|
||||
return -EIO;
|
||||
|
||||
*ret = TAKE_PTR(line);
|
||||
*ret = TAKE_PTR(string);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool any_key_to_proceed(void) {
|
||||
|
||||
/* Insert a new line here as well as to when the user inputs, as this is also used during the boot up
|
||||
* sequence when status messages may be interleaved with the current program output. This ensures
|
||||
* that the status messages aren't appended on the same line as this message. */
|
||||
|
||||
fputc('\n', stdout);
|
||||
fputs(ansi_highlight_magenta(), stdout);
|
||||
fputs("-- Press any key to proceed --", stdout);
|
||||
fputs(ansi_normal(), stdout);
|
||||
fflush(stdout);
|
||||
|
||||
char key = 0;
|
||||
bool need_nl = true;
|
||||
(void) read_one_char(stdin, &key, USEC_INFINITY, /* echo= */ false, /* need_nl= */ NULL);
|
||||
|
||||
/*
|
||||
* Insert a new line here as well as to when the user inputs, as this is also used during the
|
||||
* boot up sequence when status messages may be interleaved with the current program output.
|
||||
* This ensures that the status messages aren't appended on the same line as this message.
|
||||
*/
|
||||
puts("-- Press any key to proceed --");
|
||||
|
||||
(void) read_one_char(stdin, &key, USEC_INFINITY, &need_nl);
|
||||
|
||||
if (need_nl)
|
||||
putchar('\n');
|
||||
fputc('\n', stdout);
|
||||
fputc('\n', stdout);
|
||||
fflush(stdout);
|
||||
|
||||
return key != 'q';
|
||||
}
|
||||
|
||||
int show_menu(char **x, unsigned n_columns, unsigned width, unsigned percentage) {
|
||||
unsigned break_lines, break_modulo;
|
||||
size_t n, per_column, i, j;
|
||||
static size_t widest_list_element(char *const*l) {
|
||||
size_t w = 0;
|
||||
|
||||
/* Returns the largest console width of all elements in 'l' */
|
||||
|
||||
STRV_FOREACH(i, l)
|
||||
w = MAX(w, utf8_console_width(*i));
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
int show_menu(char **x,
|
||||
size_t n_columns,
|
||||
size_t column_width,
|
||||
unsigned ellipsize_percentage,
|
||||
const char *grey_prefix,
|
||||
bool with_numbers) {
|
||||
|
||||
assert(n_columns > 0);
|
||||
|
||||
n = strv_length(x);
|
||||
per_column = DIV_ROUND_UP(n, n_columns);
|
||||
if (n_columns == SIZE_MAX)
|
||||
n_columns = 3;
|
||||
|
||||
break_lines = lines();
|
||||
if (column_width == SIZE_MAX) {
|
||||
size_t widest = widest_list_element(x);
|
||||
|
||||
/* If not specified, derive column width from screen width */
|
||||
size_t column_max = (columns()-1) / n_columns;
|
||||
|
||||
/* Subtract room for numbers */
|
||||
if (with_numbers && column_max > 6)
|
||||
column_max -= 6;
|
||||
|
||||
/* If columns would get too tight let's make this a linear list instead. */
|
||||
if (column_max < 10 && widest > 10) {
|
||||
n_columns = 1;
|
||||
column_max = columns()-1;
|
||||
|
||||
if (with_numbers && column_max > 6)
|
||||
column_max -= 6;
|
||||
}
|
||||
|
||||
column_width = CLAMP(widest+1, 10U, column_max);
|
||||
}
|
||||
|
||||
size_t n = strv_length(x);
|
||||
size_t per_column = DIV_ROUND_UP(n, n_columns);
|
||||
|
||||
size_t break_lines = lines();
|
||||
if (break_lines > 2)
|
||||
break_lines--;
|
||||
|
||||
/* The first page gets two extra lines, since we want to show
|
||||
* a title */
|
||||
break_modulo = break_lines;
|
||||
size_t break_modulo = break_lines;
|
||||
if (break_modulo > 3)
|
||||
break_modulo -= 3;
|
||||
|
||||
for (i = 0; i < per_column; i++) {
|
||||
for (size_t i = 0; i < per_column; i++) {
|
||||
|
||||
for (j = 0; j < n_columns; j++) {
|
||||
for (size_t j = 0; j < n_columns; j++) {
|
||||
_cleanup_free_ char *e = NULL;
|
||||
|
||||
if (j * per_column + i >= n)
|
||||
break;
|
||||
|
||||
e = ellipsize(x[j * per_column + i], width, percentage);
|
||||
e = ellipsize(x[j * per_column + i], column_width, ellipsize_percentage);
|
||||
if (!e)
|
||||
return log_oom();
|
||||
return -ENOMEM;
|
||||
|
||||
printf("%4zu) %-*s", j * per_column + i + 1, (int) width, e);
|
||||
if (with_numbers)
|
||||
printf("%s%4zu)%s ",
|
||||
ansi_grey(),
|
||||
j * per_column + i + 1,
|
||||
ansi_normal());
|
||||
|
||||
if (grey_prefix && startswith(e, grey_prefix)) {
|
||||
size_t k = MIN(strlen(grey_prefix), column_width);
|
||||
printf("%s%.*s%s",
|
||||
ansi_grey(),
|
||||
(int) k, e,
|
||||
ansi_normal());
|
||||
printf("%-*s",
|
||||
(int) (column_width - k), e+k);
|
||||
} else
|
||||
printf("%-*s", (int) column_width, e);
|
||||
}
|
||||
|
||||
putchar('\n');
|
||||
|
||||
/* on the first screen we reserve 2 extra lines for the title */
|
||||
if (i % break_lines == break_modulo) {
|
||||
if (i % break_lines == break_modulo)
|
||||
if (!any_key_to_proceed())
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
@ -80,11 +80,15 @@ int proc_cmdline_tty_size(const char *tty, unsigned *ret_rows, unsigned *ret_col
|
||||
|
||||
int chvt(int vt);
|
||||
|
||||
int read_one_char(FILE *f, char *ret, usec_t timeout, bool *need_nl);
|
||||
int read_one_char(FILE *f, char *ret, usec_t timeout, bool echo, bool *need_nl);
|
||||
int ask_char(char *ret, const char *replies, const char *text, ...) _printf_(3, 4);
|
||||
int ask_string(char **ret, const char *text, ...) _printf_(2, 3);
|
||||
|
||||
typedef int (*GetCompletionsCallback)(const char *key, char ***ret_list, void *userdata);
|
||||
int ask_string_full(char **ret, GetCompletionsCallback cb, void *userdata, const char *text, ...) _printf_(4, 5);
|
||||
#define ask_string(ret, text, ...) ask_string_full(ret, NULL, NULL, text, ##__VA_ARGS__)
|
||||
|
||||
bool any_key_to_proceed(void);
|
||||
int show_menu(char **x, unsigned n_columns, unsigned width, unsigned percentage);
|
||||
int show_menu(char **x, size_t n_columns, size_t column_width, unsigned ellipsize_percentage, const char *grey_prefix, bool with_numbers);
|
||||
|
||||
int vt_disallocate(const char *name);
|
||||
|
||||
|
@ -609,3 +609,26 @@ size_t utf8_console_width(const char *str) {
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
size_t utf8_last_length(const char *s, size_t n) {
|
||||
int r;
|
||||
|
||||
if (n == SIZE_MAX)
|
||||
n = strlen(s);
|
||||
|
||||
/* Determines length in bytes of last UTF-8 codepoint in string. If the string is empty, returns
|
||||
* zero. Treats invalid UTF-8 codepoints as 1 sized ones. */
|
||||
|
||||
for (size_t last = 0;;) {
|
||||
if (n == 0)
|
||||
return last;
|
||||
|
||||
r = utf8_encoded_valid_unichar(s, n);
|
||||
if (r <= 0)
|
||||
r = 1; /* treat invalid UTF-8 as byte-wide */
|
||||
|
||||
s += r;
|
||||
n -= r;
|
||||
last = r;
|
||||
}
|
||||
}
|
||||
|
@ -62,3 +62,5 @@ static inline char32_t utf16_surrogate_pair_to_unichar(char16_t lead, char16_t t
|
||||
size_t utf8_n_codepoints(const char *str);
|
||||
int utf8_char_console_width(const char *str);
|
||||
size_t utf8_console_width(const char *str);
|
||||
|
||||
size_t utf8_last_length(const char *s, size_t n);
|
||||
|
@ -954,7 +954,7 @@ static int loop(const char *root) {
|
||||
if (arg_batch)
|
||||
(void) usleep_safe(usec_add(usec_sub_unsigned(last_refresh, t), arg_delay));
|
||||
else {
|
||||
r = read_one_char(stdin, &key, usec_add(usec_sub_unsigned(last_refresh, t), arg_delay), NULL);
|
||||
r = read_one_char(stdin, &key, usec_add(usec_sub_unsigned(last_refresh, t), arg_delay), /* echo= */ false, /* need_nl= */ NULL);
|
||||
if (r == -ETIMEDOUT)
|
||||
continue;
|
||||
if (r < 0)
|
||||
|
@ -128,14 +128,45 @@ static void print_welcome(int rfd) {
|
||||
else
|
||||
printf("\nWelcome to your new installation of %s!\n", pn);
|
||||
|
||||
printf("\nPlease configure your system!\n\n");
|
||||
printf("\nPlease configure your system!\n");
|
||||
|
||||
any_key_to_proceed();
|
||||
|
||||
done = true;
|
||||
}
|
||||
|
||||
static int prompt_loop(int rfd, const char *text, char **l, unsigned percentage, bool (*is_valid)(int rfd, const char *name), char **ret) {
|
||||
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);
|
||||
@ -144,11 +175,14 @@ static int prompt_loop(int rfd, const char *text, char **l, unsigned percentage,
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *p = NULL;
|
||||
unsigned u;
|
||||
|
||||
r = ask_string(&p, strv_isempty(l) ? "%s %s (empty to skip): "
|
||||
: "%s %s (empty to skip, \"list\" to list options): ",
|
||||
special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET), text);
|
||||
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): ",
|
||||
special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET), text);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to query user: %m");
|
||||
|
||||
@ -159,14 +193,20 @@ static int prompt_loop(int rfd, const char *text, char **l, unsigned percentage,
|
||||
|
||||
if (!strv_isempty(l)) {
|
||||
if (streq(p, "list")) {
|
||||
r = show_menu(l, 3, 20, percentage);
|
||||
r = show_menu(l,
|
||||
/* n_columns= */ 3,
|
||||
/* column_width= */ 20,
|
||||
ellipsize_percentage,
|
||||
/* grey_prefix= */ NULL,
|
||||
/* with_numbers= */ true);
|
||||
if (r < 0)
|
||||
return r;
|
||||
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)) {
|
||||
|
@ -2476,6 +2476,28 @@ static int acquire_group_list(char ***ret) {
|
||||
return !!*ret;
|
||||
}
|
||||
|
||||
static int group_completion_callback(const char *key, char ***ret_list, void *userdata) {
|
||||
char ***available = userdata;
|
||||
int r;
|
||||
|
||||
if (!*available) {
|
||||
r = acquire_group_list(available);
|
||||
if (r < 0)
|
||||
log_debug_errno(r, "Failed to enumerate available groups, ignoring: %m");
|
||||
}
|
||||
|
||||
_cleanup_strv_free_ char **l = strv_copy(*available);
|
||||
if (!l)
|
||||
return -ENOMEM;
|
||||
|
||||
r = strv_extend(&l, "list");
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret_list = TAKE_PTR(l);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int create_interactively(void) {
|
||||
_cleanup_free_ char *username = NULL;
|
||||
int r;
|
||||
@ -2485,7 +2507,12 @@ static int create_interactively(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
any_key_to_proceed();
|
||||
printf("\nPlease create your user account!\n");
|
||||
|
||||
if (!any_key_to_proceed()) {
|
||||
log_notice("Skipping.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
(void) terminal_reset_defensive_locked(STDOUT_FILENO, /* switch_to_text= */ false);
|
||||
|
||||
@ -2522,12 +2549,21 @@ static int create_interactively(void) {
|
||||
return log_error_errno(r, "Failed to set userName field: %m");
|
||||
|
||||
_cleanup_strv_free_ char **available = NULL, **groups = NULL;
|
||||
|
||||
for (;;) {
|
||||
_cleanup_free_ char *s = NULL;
|
||||
unsigned u;
|
||||
|
||||
r = ask_string(&s,
|
||||
strv_sort_uniq(groups);
|
||||
|
||||
if (!strv_isempty(groups)) {
|
||||
_cleanup_free_ char *j = strv_join(groups, ", ");
|
||||
if (!j)
|
||||
return log_oom();
|
||||
|
||||
log_info("Currently selected groups: %s", j);
|
||||
}
|
||||
|
||||
r = ask_string_full(&s,
|
||||
group_completion_callback, &available,
|
||||
"%s Please enter an auxiliary group for user %s (empty to continue, \"list\" to list available groups): ",
|
||||
special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET), username);
|
||||
if (r < 0)
|
||||
@ -2547,15 +2583,21 @@ static int create_interactively(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
r = show_menu(available, /*n_columns=*/ 3, /*width=*/ 20, /*percentage=*/ 60);
|
||||
r = show_menu(available,
|
||||
/* n_columns= */ 3,
|
||||
/* column_width= */ 20,
|
||||
/* ellipsize_percentage= */ 60,
|
||||
/* grey_prefix= */ NULL,
|
||||
/* with_numbers= */ true);
|
||||
if (r < 0)
|
||||
return r;
|
||||
return log_error_errno(r, "Failed to show menu: %m");
|
||||
|
||||
putchar('\n');
|
||||
continue;
|
||||
};
|
||||
|
||||
if (!strv_isempty(available)) {
|
||||
unsigned u;
|
||||
r = safe_atou(s, &u);
|
||||
if (r >= 0) {
|
||||
if (u <= 0 || u > strv_length(available)) {
|
||||
@ -2607,13 +2649,13 @@ static int create_interactively(void) {
|
||||
shell = mfree(shell);
|
||||
|
||||
r = ask_string(&shell,
|
||||
"%s Please enter the shell to use for user %s (empty to skip): ",
|
||||
"%s Please enter the shell to use for user %s (empty for default): ",
|
||||
special_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET), username);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to query user for username: %m");
|
||||
|
||||
if (isempty(shell)) {
|
||||
log_info("No data entered, skipping.");
|
||||
log_info("No data entered, leaving at default.");
|
||||
break;
|
||||
}
|
||||
|
||||
@ -2664,12 +2706,20 @@ static int verb_firstboot(int argc, char *argv[], void *userdata) {
|
||||
if (r > 0) /* Already created users from credentials */
|
||||
return 0;
|
||||
|
||||
r = has_regular_user();
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0) {
|
||||
log_info("Regular user already present in user database, skipping user creation.");
|
||||
r = getenv_bool("SYSTEMD_HOME_FIRSTBOOT_OVERRIDE");
|
||||
if (r == 0)
|
||||
return 0;
|
||||
if (r < 0) {
|
||||
if (r != -ENXIO)
|
||||
log_warning_errno(r, "Failed to parse $SYSTEMD_HOME_FIRSTBOOT_OVERRIDE, ignoring: %m");
|
||||
|
||||
r = has_regular_user();
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0) {
|
||||
log_info("Regular user already present in user database, skipping user creation.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return create_interactively();
|
||||
|
@ -228,9 +228,9 @@ static int display_emergency_message_fullscreen(const char *message) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
r = read_one_char(f, &read_character_buffer, USEC_INFINITY, NULL);
|
||||
r = read_one_char(f, &read_character_buffer, USEC_INFINITY, /* echo= */ true, /* need_nl= */ NULL);
|
||||
if (r < 0 && r != -EINTR)
|
||||
log_error_errno(r, "Failed to read character: %m");
|
||||
log_warning_errno(r, "Failed to read character, ignoring: %m");
|
||||
|
||||
r = 0;
|
||||
|
||||
|
@ -745,9 +745,8 @@ int ask_password_tty(
|
||||
if (ttyfd >= 0)
|
||||
(void) loop_write(ttyfd, NO_ECHO, SIZE_MAX);
|
||||
|
||||
} else if (p >= sizeof(passphrase)-1) {
|
||||
|
||||
/* Reached the size limit */
|
||||
} else if (char_is_cc(c) || p >= sizeof(passphrase)-1) {
|
||||
/* Don't accept control chars or overly long passphrases */
|
||||
if (ttyfd >= 0)
|
||||
(void) loop_write(ttyfd, "\a", 1);
|
||||
|
||||
|
@ -1408,4 +1408,23 @@ TEST(strrstr) {
|
||||
assert_se(!strrstr(p, "xx"));
|
||||
}
|
||||
|
||||
TEST(str_common_prefix) {
|
||||
ASSERT_EQ(str_common_prefix("", ""), SIZE_MAX);
|
||||
ASSERT_EQ(str_common_prefix("a", "a"), SIZE_MAX);
|
||||
ASSERT_EQ(str_common_prefix("aa", "aa"), SIZE_MAX);
|
||||
ASSERT_EQ(str_common_prefix("aa", "bb"), 0U);
|
||||
ASSERT_EQ(str_common_prefix("bb", "aa"), 0U);
|
||||
ASSERT_EQ(str_common_prefix("aa", "ab"), 1U);
|
||||
ASSERT_EQ(str_common_prefix("ab", "aa"), 1U);
|
||||
ASSERT_EQ(str_common_prefix("systemd-resolved", "systemd-networkd"), 8U);
|
||||
ASSERT_EQ(str_common_prefix("systemd-", "systemd-networkd"), 8U);
|
||||
ASSERT_EQ(str_common_prefix("systemd-networkd", "systemd-"), 8U);
|
||||
ASSERT_EQ(str_common_prefix("syst", "systemd-networkd"), 4U);
|
||||
ASSERT_EQ(str_common_prefix("systemd-networkd", "syst"), 4U);
|
||||
ASSERT_EQ(str_common_prefix("s", "systemd-networkd"), 1U);
|
||||
ASSERT_EQ(str_common_prefix("systemd-networkd", "s"), 1U);
|
||||
ASSERT_EQ(str_common_prefix("", "systemd-networkd"), 0U);
|
||||
ASSERT_EQ(str_common_prefix("systemd-networkd", ""), 0U);
|
||||
}
|
||||
|
||||
DEFINE_TEST_MAIN(LOG_DEBUG);
|
||||
|
@ -1277,4 +1277,23 @@ TEST(strv_equal_ignore_order) {
|
||||
ASSERT_TRUE(strv_equal_ignore_order(STRV_MAKE("bar", "foo"), STRV_MAKE("bar", "foo", "bar", "foo", "foo")));
|
||||
}
|
||||
|
||||
TEST(strv_filter_prefix) {
|
||||
char **base = STRV_MAKE("foo", "bar", "baz", "foox", "zzz", "farb", "foerb");
|
||||
|
||||
_cleanup_strv_free_ char **x = ASSERT_PTR(strv_filter_prefix(base, "fo"));
|
||||
ASSERT_TRUE(strv_equal(x, STRV_MAKE("foo", "foox", "foerb")));
|
||||
x = strv_free(x);
|
||||
|
||||
x = ASSERT_PTR(strv_filter_prefix(base, ""));
|
||||
ASSERT_TRUE(strv_equal(x, base));
|
||||
x = strv_free(x);
|
||||
|
||||
x = ASSERT_PTR(strv_filter_prefix(base, "z"));
|
||||
ASSERT_TRUE(strv_equal(x, STRV_MAKE("zzz")));
|
||||
x = strv_free(x);
|
||||
|
||||
x = ASSERT_PTR(strv_filter_prefix(base, "zzz"));
|
||||
ASSERT_TRUE(strv_equal(x, STRV_MAKE("zzz")));
|
||||
}
|
||||
|
||||
DEFINE_TEST_MAIN(LOG_INFO);
|
||||
|
@ -55,20 +55,20 @@ TEST(read_one_char) {
|
||||
|
||||
assert_se(fputs("c\n", file) >= 0);
|
||||
rewind(file);
|
||||
assert_se(read_one_char(file, &r, 1000000, &need_nl) >= 0);
|
||||
assert_se(read_one_char(file, &r, 1000000, /* echo= */ true, &need_nl) >= 0);
|
||||
assert_se(!need_nl);
|
||||
assert_se(r == 'c');
|
||||
assert_se(read_one_char(file, &r, 1000000, &need_nl) < 0);
|
||||
assert_se(read_one_char(file, &r, 1000000, /* echo= */ true, &need_nl) < 0);
|
||||
|
||||
rewind(file);
|
||||
assert_se(fputs("foobar\n", file) >= 0);
|
||||
rewind(file);
|
||||
assert_se(read_one_char(file, &r, 1000000, &need_nl) < 0);
|
||||
assert_se(read_one_char(file, &r, 1000000, /* echo= */ true, &need_nl) < 0);
|
||||
|
||||
rewind(file);
|
||||
assert_se(fputs("\n", file) >= 0);
|
||||
rewind(file);
|
||||
assert_se(read_one_char(file, &r, 1000000, &need_nl) < 0);
|
||||
assert_se(read_one_char(file, &r, 1000000, /* echo= */ true, &need_nl) < 0);
|
||||
}
|
||||
|
||||
TEST(getttyname_malloc) {
|
||||
|
@ -227,6 +227,18 @@ TEST(utf8_to_utf16) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(utf8_last_length) {
|
||||
ASSERT_EQ(utf8_last_length("", 0), 0U);
|
||||
ASSERT_EQ(utf8_last_length("", SIZE_MAX), 0U);
|
||||
ASSERT_EQ(utf8_last_length("a", 1), 1U);
|
||||
ASSERT_EQ(utf8_last_length("a", SIZE_MAX), 1U);
|
||||
ASSERT_EQ(utf8_last_length("ä", SIZE_MAX), strlen("ä"));
|
||||
ASSERT_EQ(utf8_last_length("👊", SIZE_MAX), strlen("👊"));
|
||||
ASSERT_EQ(utf8_last_length("koffa", SIZE_MAX), 1U);
|
||||
ASSERT_EQ(utf8_last_length("koffä", SIZE_MAX), strlen("ä"));
|
||||
ASSERT_EQ(utf8_last_length("koff👊", SIZE_MAX), strlen("👊"));
|
||||
}
|
||||
|
||||
static int intro(void) {
|
||||
log_show_color(true);
|
||||
return EXIT_SUCCESS;
|
||||
|
Loading…
x
Reference in New Issue
Block a user