]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/shared/pretty-print.c
Merge pull request #30480 from keszybz/kernel-install-more-paths
[thirdparty/systemd.git] / src / shared / pretty-print.c
1 /* SPDX-License-Identifier: LGPL-2.1-or-later */
2
3 #include <sys/utsname.h>
4 #include <errno.h>
5 #include <stdio.h>
6
7 #include "alloc-util.h"
8 #include "color-util.h"
9 #include "conf-files.h"
10 #include "constants.h"
11 #include "env-util.h"
12 #include "fd-util.h"
13 #include "fileio.h"
14 #include "pager.h"
15 #include "path-util.h"
16 #include "pretty-print.h"
17 #include "string-util.h"
18 #include "strv.h"
19 #include "terminal-util.h"
20
21 void draw_cylon(char buffer[], size_t buflen, unsigned width, unsigned pos) {
22 char *p = buffer;
23
24 assert(buflen >= CYLON_BUFFER_EXTRA + width + 1);
25 assert(pos <= width+1); /* 0 or width+1 mean that the center light is behind the corner */
26
27 if (pos > 1) {
28 if (pos > 2)
29 p = mempset(p, ' ', pos-2);
30 if (log_get_show_color())
31 p = stpcpy(p, ANSI_RED);
32 *p++ = '*';
33 }
34
35 if (pos > 0 && pos <= width) {
36 if (log_get_show_color())
37 p = stpcpy(p, ANSI_HIGHLIGHT_RED);
38 *p++ = '*';
39 }
40
41 if (log_get_show_color())
42 p = stpcpy(p, ANSI_NORMAL);
43
44 if (pos < width) {
45 if (log_get_show_color())
46 p = stpcpy(p, ANSI_RED);
47 *p++ = '*';
48 if (pos < width-1)
49 p = mempset(p, ' ', width-1-pos);
50 if (log_get_show_color())
51 p = stpcpy(p, ANSI_NORMAL);
52 }
53
54 *p = '\0';
55 }
56
57 bool urlify_enabled(void) {
58 #if ENABLE_URLIFY
59 static int cached_urlify_enabled = -1;
60
61 if (cached_urlify_enabled < 0) {
62 int val;
63
64 val = getenv_bool("SYSTEMD_URLIFY");
65 if (val >= 0)
66 cached_urlify_enabled = val;
67 else
68 cached_urlify_enabled = colors_enabled();
69 }
70
71 return cached_urlify_enabled;
72 #else
73 return 0;
74 #endif
75 }
76
77 int terminal_urlify(const char *url, const char *text, char **ret) {
78 char *n;
79
80 assert(url);
81
82 /* Takes a URL and a pretty string and formats it as clickable link for the terminal. See
83 * https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda for details. */
84
85 if (isempty(text))
86 text = url;
87
88 if (urlify_enabled())
89 n = strjoin("\x1B]8;;", url, "\a", text, "\x1B]8;;\a");
90 else
91 n = strdup(text);
92 if (!n)
93 return -ENOMEM;
94
95 *ret = n;
96 return 0;
97 }
98
99 int file_url_from_path(const char *path, char **ret) {
100 _cleanup_free_ char *absolute = NULL;
101 struct utsname u;
102 char *url = NULL;
103 int r;
104
105 if (uname(&u) < 0)
106 return -errno;
107
108 if (!path_is_absolute(path)) {
109 r = path_make_absolute_cwd(path, &absolute);
110 if (r < 0)
111 return r;
112
113 path = absolute;
114 }
115
116 /* As suggested by https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda, let's include the local
117 * hostname here. Note that we don't use gethostname_malloc() or gethostname_strict() since we are interested
118 * in the raw string the kernel has set, whatever it may be, under the assumption that terminals are not overly
119 * careful with validating the strings either. */
120
121 url = strjoin("file://", u.nodename, path);
122 if (!url)
123 return -ENOMEM;
124
125 *ret = url;
126 return 0;
127 }
128
129 int terminal_urlify_path(const char *path, const char *text, char **ret) {
130 _cleanup_free_ char *url = NULL;
131 int r;
132
133 assert(path);
134
135 /* Much like terminal_urlify() above, but takes a file system path as input
136 * and turns it into a proper file:// URL first. */
137
138 if (isempty(path))
139 return -EINVAL;
140
141 if (isempty(text))
142 text = path;
143
144 if (!urlify_enabled()) {
145 char *n;
146
147 n = strdup(text);
148 if (!n)
149 return -ENOMEM;
150
151 *ret = n;
152 return 0;
153 }
154
155 r = file_url_from_path(path, &url);
156 if (r < 0)
157 return r;
158
159 return terminal_urlify(url, text, ret);
160 }
161
162 int terminal_urlify_man(const char *page, const char *section, char **ret) {
163 const char *url, *text;
164
165 url = strjoina("man:", page, "(", section, ")");
166 text = strjoina(page, "(", section, ") man page");
167
168 return terminal_urlify(url, text, ret);
169 }
170
171 typedef enum {
172 LINE_SECTION,
173 LINE_COMMENT,
174 LINE_NORMAL,
175 } LineType;
176
177 static LineType classify_line_type(const char *line, CatFlags flags) {
178 const char *t = skip_leading_chars(line, WHITESPACE);
179
180 if ((flags & CAT_FORMAT_HAS_SECTIONS) && *t == '[')
181 return LINE_SECTION;
182 if (IN_SET(*t, '#', ';', '\0'))
183 return LINE_COMMENT;
184 return LINE_NORMAL;
185 }
186
187 static int cat_file(const char *filename, bool newline, CatFlags flags) {
188 _cleanup_fclose_ FILE *f = NULL;
189 _cleanup_free_ char *urlified = NULL, *section = NULL, *old_section = NULL;
190 int r;
191
192 f = fopen(filename, "re");
193 if (!f)
194 return -errno;
195
196 r = terminal_urlify_path(filename, NULL, &urlified);
197 if (r < 0)
198 return r;
199
200 printf("%s%s# %s%s\n",
201 newline ? "\n" : "",
202 ansi_highlight_blue(),
203 urlified,
204 ansi_normal());
205 fflush(stdout);
206
207 for (;;) {
208 _cleanup_free_ char *line = NULL;
209
210 r = read_line(f, LONG_LINE_MAX, &line);
211 if (r < 0)
212 return log_error_errno(r, "Failed to read \"%s\": %m", filename);
213 if (r == 0)
214 break;
215
216 LineType line_type = classify_line_type(line, flags);
217 if (FLAGS_SET(flags, CAT_TLDR)) {
218 if (line_type == LINE_SECTION) {
219 /* The start of a section, let's not print it yet. */
220 free_and_replace(section, line);
221 continue;
222 }
223
224 if (line_type == LINE_COMMENT)
225 continue;
226
227 /* Before we print the actual line, print the last section header */
228 if (section) {
229 /* Do not print redundant section headers */
230 if (!streq_ptr(section, old_section))
231 printf("%s%s%s\n",
232 ansi_highlight_cyan(),
233 section,
234 ansi_normal());
235
236 free_and_replace(old_section, section);
237 }
238 }
239
240 /* Highlight the left side (directive) of a Foo=bar assignment */
241 if (FLAGS_SET(flags, CAT_FORMAT_HAS_SECTIONS) && line_type == LINE_NORMAL) {
242 const char *p = strchr(line, '=');
243 if (p) {
244 _cleanup_free_ char *highlighted = NULL, *directive = NULL;
245
246 directive = strndup(line, p - line);
247 if (!directive)
248 return log_oom();
249
250 highlighted = strjoin(ansi_highlight_green(),
251 directive,
252 "=",
253 ansi_normal(),
254 p + 1);
255 if (!highlighted)
256 return log_oom();
257
258 free_and_replace(line, highlighted);
259 }
260 }
261
262 printf("%s%s%s\n",
263 line_type == LINE_SECTION ? ansi_highlight_cyan() :
264 line_type == LINE_COMMENT ? ansi_highlight_grey() :
265 "",
266 line,
267 line_type != LINE_NORMAL ? ansi_normal() : "");
268 }
269
270 return 0;
271 }
272
273 int cat_files(const char *file, char **dropins, CatFlags flags) {
274 int r;
275
276 if (file) {
277 r = cat_file(file, /* newline= */ false, flags);
278 if (r < 0)
279 return log_warning_errno(r, "Failed to cat %s: %m", file);
280 }
281
282 STRV_FOREACH(path, dropins) {
283 r = cat_file(*path, /* newline= */ file || path != dropins, flags);
284 if (r < 0)
285 return log_warning_errno(r, "Failed to cat %s: %m", *path);
286 }
287
288 return 0;
289 }
290
291 void print_separator(void) {
292
293 /* Outputs a separator line that resolves to whitespace when copied from the terminal. We do that by outputting
294 * one line filled with spaces with ANSI underline set, followed by a second (empty) line. */
295
296 if (underline_enabled()) {
297 size_t c = columns();
298
299 flockfile(stdout);
300 fputs_unlocked(ANSI_UNDERLINE, stdout);
301
302 for (size_t i = 0; i < c; i++)
303 fputc_unlocked(' ', stdout);
304
305 fputs_unlocked(ANSI_NORMAL "\n\n", stdout);
306 funlockfile(stdout);
307 } else
308 fputs("\n\n", stdout);
309 }
310
311 static int guess_type(const char **name, char ***ret_prefixes, bool *ret_is_collection, const char **ret_extension) {
312 /* Try to figure out if name is like tmpfiles.d/ or systemd/system-presets/,
313 * i.e. a collection of directories without a main config file.
314 * Incidentally, all those formats don't use sections. So we return a single
315 * is_collection boolean, which also means that the format doesn't use sections.
316 */
317
318 _cleanup_free_ char *n = NULL;
319 bool run = false, coll = false;
320 const char *ext = ".conf";
321 /* This is static so that the array doesn't get deallocated when we exit the function */
322 static const char* const std_prefixes[] = { CONF_PATHS(""), NULL };
323 static const char* const run_prefixes[] = { "/run/", NULL };
324
325 if (path_equal(*name, "environment.d"))
326 /* Special case: we need to include /etc/environment in the search path, even
327 * though the whole concept is called environment.d. */
328 *name = "environment";
329
330 n = strdup(*name);
331 if (!n)
332 return log_oom();
333
334 delete_trailing_chars(n, "/");
335
336 /* We assume systemd-style config files support the /usr-/run-/etc split and dropins. */
337
338 if (endswith(n, ".d"))
339 coll = true;
340
341 if (path_equal(n, "udev/hwdb.d"))
342 ext = ".hwdb";
343 else if (path_equal(n, "udev/rules.d"))
344 ext = ".rules";
345 else if (path_equal(n, "kernel/install.d"))
346 ext = ".install";
347 else if (path_equal(n, "systemd/ntp-units.d")) {
348 coll = true;
349 ext = ".list";
350 } else if (path_equal(n, "systemd/relabel-extra.d")) {
351 coll = run = true;
352 ext = ".relabel";
353 } else if (PATH_IN_SET(n, "systemd/system-preset", "systemd/user-preset")) {
354 coll = true;
355 ext = ".preset";
356 }
357
358 *ret_prefixes = (char**) (run ? run_prefixes : std_prefixes);
359 *ret_is_collection = coll;
360 *ret_extension = ext;
361 return 0;
362 }
363
364 int conf_files_cat(const char *root, const char *name, CatFlags flags) {
365 _cleanup_strv_free_ char **dirs = NULL, **files = NULL;
366 _cleanup_free_ char *path = NULL;
367 char **prefixes = NULL; /* explicit initialization to appease gcc */
368 bool is_collection;
369 const char *extension;
370 int r;
371
372 r = guess_type(&name, &prefixes, &is_collection, &extension);
373 if (r < 0)
374 return r;
375 assert(prefixes);
376 assert(extension);
377
378 STRV_FOREACH(prefix, prefixes) {
379 assert(endswith(*prefix, "/"));
380 r = strv_extendf(&dirs, "%s%s%s", *prefix, name,
381 is_collection ? "" : ".d");
382 if (r < 0)
383 return log_error_errno(r, "Failed to build directory list: %m");
384 }
385
386 if (DEBUG_LOGGING) {
387 log_debug("Looking for configuration in:");
388 if (!is_collection)
389 STRV_FOREACH(prefix, prefixes)
390 log_debug(" %s%s%s", strempty(root), *prefix, name);
391
392 STRV_FOREACH(t, dirs)
393 log_debug(" %s%s/*%s", strempty(root), *t, extension);
394 }
395
396 /* First locate the main config file, if any */
397 if (!is_collection) {
398 STRV_FOREACH(prefix, prefixes) {
399 path = path_join(root, *prefix, name);
400 if (!path)
401 return log_oom();
402 if (access(path, F_OK) == 0)
403 break;
404 path = mfree(path);
405 }
406
407 if (!path)
408 printf("%s# Main configuration file %s not found%s\n",
409 ansi_highlight_magenta(),
410 name,
411 ansi_normal());
412 }
413
414 /* Then locate the drop-ins, if any */
415 r = conf_files_list_strv(&files, extension, root, 0, (const char* const*) dirs);
416 if (r < 0)
417 return log_error_errno(r, "Failed to query file list: %m");
418
419 /* Show */
420 if (is_collection)
421 flags |= CAT_FORMAT_HAS_SECTIONS;
422
423 return cat_files(path, files, flags);
424 }
425
426 int terminal_tint_color(double hue, char **ret) {
427 double red, green, blue;
428 int r;
429
430 assert(ret);
431
432 r = get_default_background_color(&red, &green, &blue);
433 if (r < 0)
434 return log_debug_errno(r, "Unable to get terminal background color: %m");
435
436 double s, v;
437 rgb_to_hsv(red, green, blue, /* h= */ NULL, &s, &v);
438
439 if (v > 50) /* If the background is bright, then pull down saturation */
440 s = 25;
441 else /* otherwise pump it up */
442 s = 75;
443
444 v = MAX(20, v); /* Make sure we don't hide the color in black */
445
446 uint8_t r8, g8, b8;
447 hsv_to_rgb(hue, s, v, &r8, &g8, &b8);
448
449 if (asprintf(ret, "48;2;%u;%u;%u", r8, g8, b8) < 0)
450 return -ENOMEM;
451
452 return 0;
453 }
454
455 void draw_progress_bar(const char *prefix, double percentage) {
456
457 fputc('\r', stderr);
458 if (prefix)
459 fputs(prefix, stderr);
460
461 if (!terminal_is_dumb()) {
462 size_t cols = columns();
463 size_t prefix_length = strlen_ptr(prefix);
464 size_t length = cols > prefix_length + 6 ? cols - prefix_length - 6 : 0;
465
466 if (length > 5 && percentage >= 0.0 && percentage <= 100.0) {
467 size_t p = (size_t) (length * percentage / 100.0);
468 bool separator_done = false;
469
470 fputs(ansi_highlight_green(), stderr);
471
472 for (size_t i = 0; i < length; i++) {
473
474 if (i <= p) {
475 if (get_color_mode() == COLOR_24BIT) {
476 uint8_t r8, g8, b8;
477 double z = i == 0 ? 0 : (((double) i / p) * 100);
478 hsv_to_rgb(145 /* green */, z, 33 + z*2/3, &r8, &g8, &b8);
479 fprintf(stderr, "\x1B[38;2;%u;%u;%um", r8, g8, b8);
480 }
481
482 fputs(special_glyph(SPECIAL_GLYPH_HORIZONTAL_FAT), stderr);
483 } else if (i+1 < length && !separator_done) {
484 fputs(ansi_normal(), stderr);
485 fputc(' ', stderr);
486 separator_done = true;
487 fputs(ansi_grey(), stderr);
488 } else
489 fputs(special_glyph(SPECIAL_GLYPH_HORIZONTAL_DOTTED), stderr);
490 }
491
492 fputs(ansi_normal(), stderr);
493 fputc(' ', stderr);
494 }
495 }
496
497 fprintf(stderr,
498 "%s%3.0f%%%s",
499 ansi_highlight(),
500 percentage,
501 ansi_normal());
502
503 if (!terminal_is_dumb())
504 fputs(ANSI_ERASE_TO_END_OF_LINE, stderr);
505
506 fputc('\r', stderr);
507 fflush(stderr);
508 }
509
510 void clear_progress_bar(const char *prefix) {
511
512 fputc('\r', stderr);
513
514 if (terminal_is_dumb())
515 fputs(strrepa(" ", strlen_ptr(prefix) + 4), /* 4: %3.0f%% */
516 stderr);
517 else
518 fputs(ANSI_ERASE_TO_END_OF_LINE, stderr);
519
520 fputc('\r', stderr);
521 fflush(stderr);
522 }