Line data Source code
1 : /* Output colorization.
2 : Copyright (C) 2011-2026 Free Software Foundation, Inc.
3 :
4 : This program is free software; you can redistribute it and/or modify
5 : it under the terms of the GNU General Public License as published by
6 : the Free Software Foundation; either version 3, or (at your option)
7 : any later version.
8 :
9 : This program is distributed in the hope that it will be useful,
10 : but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : GNU General Public License for more details.
13 :
14 : You should have received a copy of the GNU General Public License
15 : along with this program; if not, write to the Free Software
16 : Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA
17 : 02110-1301, USA. */
18 :
19 : #include "config.h"
20 : #define INCLUDE_VECTOR
21 : #include "system.h"
22 : #include "diagnostics/color.h"
23 : #include "diagnostics/url.h"
24 : #include "label-text.h"
25 :
26 : #ifdef __MINGW32__
27 : # define WIN32_LEAN_AND_MEAN
28 : # include <windows.h>
29 : #endif
30 :
31 : #include "color-macros.h"
32 : #include "selftest.h"
33 :
34 : /* The context and logic for choosing default --color screen attributes
35 : (foreground and background colors, etc.) are the following.
36 : -- There are eight basic colors available, each with its own
37 : nominal luminosity to the human eye and foreground/background
38 : codes (black [0 %, 30/40], blue [11 %, 34/44], red [30 %, 31/41],
39 : magenta [41 %, 35/45], green [59 %, 32/42], cyan [70 %, 36/46],
40 : yellow [89 %, 33/43], and white [100 %, 37/47]).
41 : -- Sometimes, white as a background is actually implemented using
42 : a shade of light gray, so that a foreground white can be visible
43 : on top of it (but most often not).
44 : -- Sometimes, black as a foreground is actually implemented using
45 : a shade of dark gray, so that it can be visible on top of a
46 : background black (but most often not).
47 : -- Sometimes, more colors are available, as extensions.
48 : -- Other attributes can be selected/deselected (bold [1/22],
49 : underline [4/24], standout/inverse [7/27], blink [5/25], and
50 : invisible/hidden [8/28]). They are sometimes implemented by
51 : using colors instead of what their names imply; e.g., bold is
52 : often achieved by using brighter colors. In practice, only bold
53 : is really available to us, underline sometimes being mapped by
54 : the terminal to some strange color choice, and standout best
55 : being left for use by downstream programs such as less(1).
56 : -- We cannot assume that any of the extensions or special features
57 : are available for the purpose of choosing defaults for everyone.
58 : -- The most prevalent default terminal backgrounds are pure black
59 : and pure white, and are not necessarily the same shades of
60 : those as if they were selected explicitly with SGR sequences.
61 : Some terminals use dark or light pictures as default background,
62 : but those are covered over by an explicit selection of background
63 : color with an SGR sequence; their users will appreciate their
64 : background pictures not be covered like this, if possible.
65 : -- Some uses of colors attributes is to make some output items
66 : more understated (e.g., context lines); this cannot be achieved
67 : by changing the background color.
68 : -- For these reasons, the GCC color defaults should strive not
69 : to change the background color from its default, unless it's
70 : for a short item that should be highlighted, not understated.
71 : -- The GCC foreground color defaults (without an explicitly set
72 : background) should provide enough contrast to be readable on any
73 : terminal with either a black (dark) or white (light) background.
74 : This only leaves red, magenta, green, and cyan (and their bold
75 : counterparts) and possibly bold blue. */
76 : /* Default colors. The user can overwrite them using environment
77 : variable GCC_COLORS. */
78 : struct color_default
79 : {
80 : const char *m_name;
81 : const char *m_val;
82 : };
83 :
84 : /* For GCC_COLORS. */
85 : static const color_default gcc_color_defaults[] =
86 : {
87 : { "error", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_RED) },
88 : { "warning", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_MAGENTA) },
89 : { "note", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_CYAN) },
90 : { "range1", SGR_SEQ (COLOR_FG_GREEN) },
91 : { "range2", SGR_SEQ (COLOR_FG_BLUE) },
92 : { "locus", SGR_SEQ (COLOR_BOLD) },
93 : { "quote", SGR_SEQ (COLOR_BOLD) },
94 : { "path", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_CYAN) },
95 : { "fnname", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_GREEN) },
96 : { "targs", SGR_SEQ (COLOR_FG_MAGENTA) },
97 : { "fixit-insert", SGR_SEQ (COLOR_FG_GREEN) },
98 : { "fixit-delete", SGR_SEQ (COLOR_FG_RED) },
99 : { "diff-filename", SGR_SEQ (COLOR_BOLD) },
100 : { "diff-hunk", SGR_SEQ (COLOR_FG_CYAN) },
101 : { "diff-delete", SGR_SEQ (COLOR_FG_RED) },
102 : { "diff-insert", SGR_SEQ (COLOR_FG_GREEN) },
103 : { "type-diff", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_GREEN) },
104 : { "valid", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_GREEN) },
105 : { "invalid", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_RED) },
106 : { "highlight-a", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_GREEN) },
107 : { "highlight-b", SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_BLUE) }
108 : };
109 :
110 24 : class diagnostic_color_dict
111 : {
112 : public:
113 : diagnostic_color_dict (const color_default *default_values,
114 : size_t num_default_values);
115 :
116 : bool parse_envvar_value (const char *const envvar_value);
117 :
118 : const char *get_start_by_name (const char *name, size_t name_len) const;
119 32 : const char *get_start_by_name (const char *name) const
120 : {
121 32 : return get_start_by_name (name, strlen (name));
122 : }
123 :
124 : private:
125 12381419 : struct entry
126 : {
127 12381243 : entry (const color_default &d)
128 12381243 : : m_name (d.m_name),
129 12381243 : m_name_len (strlen (d.m_name)),
130 12381243 : m_val (label_text::borrow (d.m_val))
131 : {
132 : }
133 :
134 : const char *m_name;
135 : size_t m_name_len;
136 : label_text m_val;
137 : };
138 :
139 : const entry *get_entry_by_name (const char *name, size_t name_len) const;
140 : entry *get_entry_by_name (const char *name, size_t name_len);
141 :
142 : std::vector<entry> m_entries;
143 : };
144 :
145 : static diagnostic_color_dict *g_color_dict;
146 :
147 : const char *
148 103010106 : colorize_start (bool show_color, const char *name, size_t name_len)
149 : {
150 103010106 : if (!show_color)
151 : return "";
152 :
153 1259 : if (!g_color_dict)
154 : return "";
155 :
156 1259 : return g_color_dict->get_start_by_name (name, name_len);
157 : }
158 :
159 : /* Look for an entry named NAME of length NAME_LEN within this
160 : diagnostic_color_dict, or nullptr if there isn't one. */
161 :
162 : const diagnostic_color_dict::entry *
163 1291 : diagnostic_color_dict::get_entry_by_name (const char *name,
164 : size_t name_len) const
165 : {
166 12248 : for (auto &iter : m_entries)
167 12228 : if (iter.m_name_len == name_len
168 3251 : && memcmp (iter.m_name, name, name_len) == 0)
169 1291 : return &iter;
170 : return nullptr;
171 : }
172 :
173 : /* Non-const version of the above. */
174 :
175 : diagnostic_color_dict::entry *
176 12 : diagnostic_color_dict::get_entry_by_name (const char *name,
177 : size_t name_len)
178 : {
179 100 : for (auto &iter : m_entries)
180 96 : if (iter.m_name_len == name_len
181 12 : && memcmp (iter.m_name, name, name_len) == 0)
182 12 : return &iter;
183 : return nullptr;
184 : }
185 :
186 : /* Return the SGR codes to start a color entry named NAME of length
187 : NAME_LEN within this diagnostic_color_dict, or the empty string if
188 : there isn't one. */
189 :
190 : const char *
191 1291 : diagnostic_color_dict::get_start_by_name (const char *name,
192 : size_t name_len) const
193 : {
194 1291 : if (const entry *e = get_entry_by_name (name, name_len))
195 1271 : return e->m_val.get ();
196 :
197 : return "";
198 : }
199 :
200 : const char *
201 102865174 : colorize_stop (bool show_color)
202 : {
203 102865174 : return show_color ? SGR_RESET : "";
204 : }
205 :
206 : /* diagnostic_color_dict's ctor. Initialize it from the given array
207 : of color_default values. */
208 :
209 589587 : diagnostic_color_dict::
210 : diagnostic_color_dict (const color_default *default_values,
211 589587 : size_t num_default_values)
212 : {
213 589587 : m_entries.reserve (num_default_values);
214 12970830 : for (size_t idx = 0; idx < num_default_values; idx++)
215 12381243 : m_entries.push_back (entry (default_values[idx]));
216 589587 : }
217 :
218 : /* Parse a list of color definitions from an environment variable
219 : value (such as that of GCC_COLORS).
220 : No character escaping is needed or supported. */
221 :
222 : bool
223 489175 : diagnostic_color_dict::parse_envvar_value (const char *const envvar_value)
224 : {
225 : /* envvar not set: use the default colors. */
226 489175 : if (envvar_value == nullptr)
227 : return true;
228 :
229 : /* envvar set to empty string: disable colorization. */
230 52 : if (*envvar_value == '\0')
231 : return false;
232 :
233 : const char *q, *name, *val;
234 : size_t name_len = 0, val_len = 0;
235 :
236 : name = q = envvar_value;
237 : val = nullptr;
238 : /* From now on, be well-formed or you're gone. */
239 184 : for (;;)
240 184 : if (*q == ':' || *q == '\0')
241 : {
242 12 : if (val)
243 12 : val_len = q - val;
244 : else
245 0 : name_len = q - name;
246 : /* Empty name without val (empty cap)
247 : won't match and will be ignored. */
248 12 : entry *e = get_entry_by_name (name, name_len);
249 : /* If name unknown, go on for forward compatibility. */
250 12 : if (e && val)
251 : {
252 8 : char *b = XNEWVEC (char, val_len + sizeof (SGR_SEQ ("")));
253 8 : memcpy (b, SGR_START, strlen (SGR_START));
254 8 : memcpy (b + strlen (SGR_START), val, val_len);
255 8 : memcpy (b + strlen (SGR_START) + val_len, SGR_END,
256 : sizeof (SGR_END));
257 8 : e->m_val = label_text::take (b);
258 : }
259 12 : if (*q == '\0')
260 : return true;
261 8 : name = ++q;
262 8 : val = nullptr;
263 : }
264 172 : else if (*q == '=')
265 : {
266 12 : if (q == name || val)
267 : return true;
268 :
269 12 : name_len = q - name;
270 12 : val = ++q; /* Can be the empty string. */
271 : }
272 160 : else if (val == nullptr)
273 100 : q++; /* Accumulate name. */
274 60 : else if (*q == ';' || (*q >= '0' && *q <= '9'))
275 60 : q++; /* Accumulate val. Protect the terminal from being sent
276 : garbage. */
277 : else
278 : return true;
279 : }
280 :
281 : /* Parse GCC_COLORS. The default would look like:
282 : GCC_COLORS='error=01;31:warning=01;35:note=01;36:\
283 : range1=32:range2=34:locus=01:quote=01:path=01;36:\
284 : fixit-insert=32:fixit-delete=31:'\
285 : diff-filename=01:diff-hunk=32:diff-delete=31:diff-insert=32:\
286 : type-diff=01;32'. */
287 : static bool
288 489171 : parse_gcc_colors ()
289 : {
290 489171 : if (!g_color_dict)
291 : return false;
292 489171 : return g_color_dict->parse_envvar_value (getenv ("GCC_COLORS")); /* Plural! */
293 : }
294 :
295 : /* Return true if we should use color when in auto mode, false otherwise. */
296 : static bool
297 1183818 : should_colorize (void)
298 : {
299 : #ifdef __MINGW32__
300 : /* For consistency reasons, one should check the handle returned by
301 : _get_osfhandle(_fileno(stderr)) because the function
302 : pp_write_text_to_stream() in pretty-print.cc calls fputs() on
303 : that stream. However, the code below for non-Windows doesn't seem
304 : to care about it either... */
305 : HANDLE handle;
306 : DWORD mode;
307 : BOOL isconsole = false;
308 :
309 : handle = GetStdHandle (STD_ERROR_HANDLE);
310 :
311 : if ((handle != INVALID_HANDLE_VALUE) && (handle != nullptr))
312 : isconsole = GetConsoleMode (handle, &mode);
313 :
314 : #ifdef ENABLE_VIRTUAL_TERMINAL_PROCESSING
315 : if (isconsole)
316 : {
317 : /* Try to enable processing of VT100 escape sequences */
318 : mode |= ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
319 : SetConsoleMode (handle, mode);
320 : }
321 : #endif
322 :
323 : return isconsole;
324 : #else
325 1183818 : char const *t = getenv ("TERM");
326 : /* emacs M-x shell sets TERM="dumb". */
327 1183818 : return t && strcmp (t, "dumb") != 0 && isatty (STDERR_FILENO);
328 : #endif
329 : }
330 :
331 : bool
332 1400680 : colorize_init (diagnostic_color_rule_t rule)
333 : {
334 1400680 : if (!g_color_dict)
335 589575 : g_color_dict = new diagnostic_color_dict (gcc_color_defaults,
336 589575 : ARRAY_SIZE (gcc_color_defaults));
337 :
338 1400680 : switch (rule)
339 : {
340 : case DIAGNOSTICS_COLOR_NO:
341 : return false;
342 133 : case DIAGNOSTICS_COLOR_YES:
343 133 : return parse_gcc_colors ();
344 591909 : case DIAGNOSTICS_COLOR_AUTO:
345 591909 : if (should_colorize ())
346 489038 : return parse_gcc_colors ();
347 : else
348 : return false;
349 0 : default:
350 0 : gcc_unreachable ();
351 : }
352 : }
353 :
354 : /* Return URL_FORMAT_XXX which tells how we should emit urls
355 : when in always mode.
356 : We use GCC_URLS and if that is not defined TERM_URLS.
357 : If neither is defined the feature is enabled by default. */
358 :
359 : static diagnostic_url_format
360 0 : parse_env_vars_for_urls ()
361 : {
362 0 : const char *p;
363 :
364 0 : p = getenv ("GCC_URLS"); /* Plural! */
365 0 : if (p == nullptr)
366 0 : p = getenv ("TERM_URLS");
367 :
368 0 : if (p == nullptr)
369 : return URL_FORMAT_DEFAULT;
370 :
371 0 : if (*p == '\0')
372 : return URL_FORMAT_NONE;
373 :
374 0 : if (!strcmp (p, "no"))
375 : return URL_FORMAT_NONE;
376 :
377 0 : if (!strcmp (p, "st"))
378 : return URL_FORMAT_ST;
379 :
380 : if (!strcmp (p, "bel"))
381 : return URL_FORMAT_BEL;
382 :
383 : return URL_FORMAT_DEFAULT;
384 : }
385 :
386 : /* Return true if we should use urls when in auto mode, false otherwise. */
387 :
388 : static bool
389 591909 : auto_enable_urls ()
390 : {
391 591909 : const char *term, *colorterm;
392 :
393 : /* First check the terminal is capable of printing color escapes,
394 : if not URLs won't work either. */
395 591909 : if (!should_colorize ())
396 : return false;
397 :
398 : #ifdef __MINGW32__
399 : HANDLE handle;
400 : DWORD mode;
401 :
402 : handle = GetStdHandle (STD_ERROR_HANDLE);
403 : if ((handle == INVALID_HANDLE_VALUE) || (handle == nullptr))
404 : return false;
405 :
406 : /* If ansi escape sequences aren't supported by the console, then URLs will
407 : print mangled from mingw_ansi_fputs's console API translation. It wouldn't
408 : be useful even if this weren't the case. */
409 : if (GetConsoleMode (handle, &mode)
410 : #ifdef ENABLE_VIRTUAL_TERMINAL_PROCESSING
411 : && !(mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING)
412 : #endif
413 : )
414 : return false;
415 : #endif
416 :
417 : /* xfce4-terminal is known to not implement URLs at this time.
418 : Recently new installations (0.8) will safely ignore the URL escape
419 : sequences, but a large number of legacy installations (0.6.3) print
420 : garbage when URLs are printed. Therefore we lose nothing by
421 : disabling this feature for that specific terminal type. */
422 489038 : colorterm = getenv ("COLORTERM");
423 489038 : if (colorterm && !strcmp (colorterm, "xfce4-terminal"))
424 : return false;
425 :
426 : /* Old versions of gnome-terminal where URL escapes cause screen
427 : corruptions set COLORTERM="gnome-terminal", recent versions
428 : with working URL support set this to "truecolor". */
429 0 : if (colorterm && !strcmp (colorterm, "gnome-terminal"))
430 : return false;
431 :
432 : /* Since the following checks are less specific than the ones
433 : above, let GCC_URLS and TERM_URLS override the decision. */
434 489038 : if (getenv ("GCC_URLS") || getenv ("TERM_URLS"))
435 0 : return true;
436 :
437 : /* In an ssh session the COLORTERM is not there, but TERM=xterm
438 : can be used as an indication of a incompatible terminal while
439 : TERM=xterm-256color appears to be a working terminal. */
440 489038 : term = getenv ("TERM");
441 489038 : if (!colorterm && term && !strcmp (term, "xterm"))
442 : return false;
443 :
444 : /* When logging in a linux over serial line, we see TERM=linux
445 : and no COLORTERM, it is unlikely that the URL escapes will
446 : work in that environmen either. */
447 0 : if (!colorterm && term && !strcmp (term, "linux"))
448 : return false;
449 :
450 : return true;
451 : }
452 :
453 : /* Determine if URLs should be enabled, based on RULE,
454 : and, if so, which format to use.
455 : This reuses the logic for colorization. */
456 :
457 : diagnostic_url_format
458 1391074 : determine_url_format (diagnostic_url_rule_t rule)
459 : {
460 1391074 : switch (rule)
461 : {
462 : case DIAGNOSTICS_URL_NO:
463 : return URL_FORMAT_NONE;
464 0 : case DIAGNOSTICS_URL_YES:
465 0 : return parse_env_vars_for_urls ();
466 591909 : case DIAGNOSTICS_URL_AUTO:
467 591909 : if (auto_enable_urls ())
468 0 : return parse_env_vars_for_urls ();
469 : else
470 : return URL_FORMAT_NONE;
471 0 : default:
472 0 : gcc_unreachable ();
473 : }
474 : }
475 :
476 : #if CHECKING_P
477 :
478 : namespace diagnostics {
479 : namespace selftest {
480 :
481 : /* Test of an empty diagnostic_color_dict. */
482 :
483 : static void
484 4 : test_empty_color_dict ()
485 : {
486 4 : diagnostic_color_dict d (nullptr, 0);
487 4 : ASSERT_STREQ (d.get_start_by_name ("warning"), "");
488 4 : ASSERT_STREQ (d.get_start_by_name ("should-not-be-found"), "");
489 4 : }
490 :
491 : /* Test of a diagnostic_color_dict with GCC's defaults. */
492 :
493 : static void
494 4 : test_default_color_dict ()
495 : {
496 4 : diagnostic_color_dict d (gcc_color_defaults,
497 4 : ARRAY_SIZE (gcc_color_defaults));
498 4 : ASSERT_STREQ (d.get_start_by_name ("warning"),
499 : SGR_SEQ (COLOR_BOLD COLOR_SEPARATOR COLOR_FG_MAGENTA));
500 4 : ASSERT_STREQ (d.get_start_by_name ("should-not-be-found"), "");
501 4 : }
502 :
503 : /* Test of a diagnostic_color_dict with GCC's defaults plus overrides from
504 : an environment variable. */
505 :
506 : static void
507 4 : test_color_dict_envvar_parsing ()
508 : {
509 4 : diagnostic_color_dict d (gcc_color_defaults,
510 4 : ARRAY_SIZE (gcc_color_defaults));
511 :
512 4 : d.parse_envvar_value ("error=01;37:warning=01;42:unknown-value=01;36");
513 :
514 4 : ASSERT_STREQ (d.get_start_by_name ("error"),
515 : SGR_SEQ ("01;37"));
516 4 : ASSERT_STREQ (d.get_start_by_name ("warning"),
517 : SGR_SEQ ("01;42"));
518 4 : ASSERT_STREQ (d.get_start_by_name ("unknown-value"), "");
519 4 : ASSERT_STREQ (d.get_start_by_name ("should-not-be-found"), "");
520 4 : }
521 :
522 :
523 : /* Run all of the selftests within this file. */
524 :
525 : void
526 4 : color_cc_tests ()
527 : {
528 4 : test_empty_color_dict ();
529 4 : test_default_color_dict ();
530 4 : test_color_dict_envvar_parsing ();
531 4 : }
532 :
533 : } // namespace selftest
534 : } // namespace diagnostics
535 :
536 : #endif /* #if CHECKING_P */
|