1 /* dircolors - output commands to set the LS_COLOR environment variable
2 Copyright (C) 1996-2023 Free Software Foundation, Inc.
3 Copyright (C) 1994, 1995, 1997, 1998, 1999, 2000 H. Peter Anvin
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <https://www.gnu.org/licenses/>. */
17
18 #include <config.h>
19
20 #include <sys/types.h>
21 #include <fnmatch.h>
22 #include <getopt.h>
23
24 #include "system.h"
25 #include "dircolors.h"
26 #include "c-strcase.h"
27 #include "obstack.h"
28 #include "quote.h"
29 #include "stdio--.h"
30
31 /* The official name of this program (e.g., no 'g' prefix). */
32 #define PROGRAM_NAME "dircolors"
33
34 #define AUTHORS proper_name ("H. Peter Anvin")
35
36 #define obstack_chunk_alloc malloc
37 #define obstack_chunk_free free
38
39 enum Shell_syntax
40 {
41 SHELL_SYNTAX_BOURNE,
42 SHELL_SYNTAX_C,
43 SHELL_SYNTAX_UNKNOWN
44 };
45
46 #define APPEND_CHAR(C) obstack_1grow (&lsc_obstack, C)
47
48 /* Accumulate in this obstack the value for the LS_COLORS environment
49 variable. */
50 static struct obstack lsc_obstack;
51
52 static char const *const slack_codes[] =
53 {
54 "NORMAL", "NORM", "FILE", "RESET", "DIR", "LNK", "LINK",
55 "SYMLINK", "ORPHAN", "MISSING", "FIFO", "PIPE", "SOCK", "BLK", "BLOCK",
56 "CHR", "CHAR", "DOOR", "EXEC", "LEFT", "LEFTCODE", "RIGHT", "RIGHTCODE",
57 "END", "ENDCODE", "SUID", "SETUID", "SGID", "SETGID", "STICKY",
58 "OTHER_WRITABLE", "OWR", "STICKY_OTHER_WRITABLE", "OWT", "CAPABILITY",
59 "MULTIHARDLINK", "CLRTOEOL", nullptr
60 };
61
62 static char const *const ls_codes[] =
63 {
64 "no", "no", "fi", "rs", "di", "ln", "ln", "ln", "or", "mi", "pi", "pi",
65 "so", "bd", "bd", "cd", "cd", "do", "ex", "lc", "lc", "rc", "rc", "ec", "ec",
66 "su", "su", "sg", "sg", "st", "ow", "ow", "tw", "tw", "ca", "mh", "cl",
67 nullptr
68 };
69 static_assert (ARRAY_CARDINALITY (slack_codes) == ARRAY_CARDINALITY (ls_codes));
70
71 /* Whether to output escaped ls color codes for display. */
72 static bool print_ls_colors;
73
74 /* For long options that have no equivalent short option, use a
75 non-character as a pseudo short option, starting with CHAR_MAX + 1. */
76 enum
77 {
78 PRINT_LS_COLORS_OPTION = CHAR_MAX + 1,
79 };
80
81 static struct option const long_options[] =
82 {
83 {"bourne-shell", no_argument, nullptr, 'b'},
84 {"sh", no_argument, nullptr, 'b'},
85 {"csh", no_argument, nullptr, 'c'},
86 {"c-shell", no_argument, nullptr, 'c'},
87 {"print-database", no_argument, nullptr, 'p'},
88 {"print-ls-colors", no_argument, nullptr, PRINT_LS_COLORS_OPTION},
89 {GETOPT_HELP_OPTION_DECL},
90 {GETOPT_VERSION_OPTION_DECL},
91 {nullptr, 0, nullptr, 0}
92 };
93
94 void
usage(int status)95 usage (int status)
96 {
97 if (status != EXIT_SUCCESS)
98 emit_try_help ();
99 else
100 {
101 printf (_("Usage: %s [OPTION]... [FILE]\n"), program_name);
102 fputs (_("\
103 Output commands to set the LS_COLORS environment variable.\n\
104 \n\
105 Determine format of output:\n\
106 -b, --sh, --bourne-shell output Bourne shell code to set LS_COLORS\n\
107 -c, --csh, --c-shell output C shell code to set LS_COLORS\n\
108 -p, --print-database output defaults\n\
109 --print-ls-colors output fully escaped colors for display\n\
110 "), stdout);
111 fputs (HELP_OPTION_DESCRIPTION, stdout);
112 fputs (VERSION_OPTION_DESCRIPTION, stdout);
113 fputs (_("\
114 \n\
115 If FILE is specified, read it to determine which colors to use for which\n\
116 file types and extensions. Otherwise, a precompiled database is used.\n\
117 For details on the format of these files, run 'dircolors --print-database'.\n\
118 "), stdout);
119 emit_ancillary_info (PROGRAM_NAME);
120 }
121
122 exit (status);
123 }
124
125 /* If the SHELL environment variable is set to 'csh' or 'tcsh,'
126 assume C shell. Else Bourne shell. */
127
128 static enum Shell_syntax
guess_shell_syntax(void)129 guess_shell_syntax (void)
130 {
131 char *shell;
132
133 shell = getenv ("SHELL");
134 if (shell == nullptr || *shell == '\0')
135 return SHELL_SYNTAX_UNKNOWN;
136
137 shell = last_component (shell);
138
139 if (STREQ (shell, "csh") || STREQ (shell, "tcsh"))
140 return SHELL_SYNTAX_C;
141
142 return SHELL_SYNTAX_BOURNE;
143 }
144
145 static void
parse_line(char const * line,char ** keyword,char ** arg)146 parse_line (char const *line, char **keyword, char **arg)
147 {
148 char const *p;
149 char const *keyword_start;
150 char const *arg_start;
151
152 *keyword = nullptr;
153 *arg = nullptr;
154
155 for (p = line; isspace (to_uchar (*p)); ++p)
156 continue;
157
158 /* Ignore blank lines and shell-style comments. */
159 if (*p == '\0' || *p == '#')
160 return;
161
162 keyword_start = p;
163
164 while (!isspace (to_uchar (*p)) && *p != '\0')
165 {
166 ++p;
167 }
168
169 *keyword = ximemdup0 (keyword_start, p - keyword_start);
170 if (*p == '\0')
171 return;
172
173 do
174 {
175 ++p;
176 }
177 while (isspace (to_uchar (*p)));
178
179 if (*p == '\0' || *p == '#')
180 return;
181
182 arg_start = p;
183
184 while (*p != '\0' && *p != '#')
185 ++p;
186
187 for (--p; isspace (to_uchar (*p)); --p)
188 continue;
189 ++p;
190
191 *arg = ximemdup0 (arg_start, p - arg_start);
192 }
193
194 /* Accumulate STR to LS_COLORS data.
195 If outputting shell syntax, then escape appropriately. */
196
197 static void
append_quoted(char const * str)198 append_quoted (char const *str)
199 {
200 bool need_backslash = true;
201
202 while (*str != '\0')
203 {
204 if (! print_ls_colors)
205 switch (*str)
206 {
207 case '\'':
208 APPEND_CHAR ('\'');
209 APPEND_CHAR ('\\');
210 APPEND_CHAR ('\'');
211 need_backslash = true;
212 break;
213
214 case '\\':
215 case '^':
216 need_backslash = !need_backslash;
217 break;
218
219 case ':':
220 case '=':
221 if (need_backslash)
222 APPEND_CHAR ('\\');
223 FALLTHROUGH;
224
225 default:
226 need_backslash = true;
227 break;
228 }
229
230 APPEND_CHAR (*str);
231 ++str;
232 }
233 }
234
235 /* Accumulate entry to LS_COLORS data.
236 Use shell syntax unless PRINT_LS_COLORS is set. */
237
238 static void
append_entry(char prefix,char const * item,char const * arg)239 append_entry (char prefix, char const *item, char const *arg)
240 {
241 if (print_ls_colors)
242 {
243 append_quoted ("\x1B[");
244 append_quoted (arg);
245 APPEND_CHAR ('m');
246 }
247 if (prefix)
248 APPEND_CHAR (prefix);
249 append_quoted (item);
250 APPEND_CHAR (print_ls_colors ? '\t' : '=');
251 append_quoted (arg);
252 if (print_ls_colors)
253 append_quoted ("\x1B[0m");
254 APPEND_CHAR (print_ls_colors ? '\n' : ':');
255 }
256
257 /* Read the file open on FP (with name FILENAME). First, look for a
258 'TERM name' directive where name matches the current terminal type.
259 Once found, translate and accumulate the associated directives onto
260 the global obstack LSC_OBSTACK. Give a diagnostic
261 upon failure (unrecognized keyword is the only way to fail here).
262 Return true if successful. */
263
264 static bool
dc_parse_stream(FILE * fp,char const * filename)265 dc_parse_stream (FILE *fp, char const *filename)
266 {
267 idx_t line_number = 0;
268 char const *next_G_line = G_line;
269 char *input_line = nullptr;
270 size_t input_line_size = 0;
271 char const *line;
272 char const *term;
273 char const *colorterm;
274 bool ok = true;
275
276 /* State for the parser. */
277 enum { ST_TERMNO, ST_TERMYES, ST_TERMSURE, ST_GLOBAL } state = ST_GLOBAL;
278
279 /* Get terminal type */
280 term = getenv ("TERM");
281 if (term == nullptr || *term == '\0')
282 term = "none";
283
284 /* Also match $COLORTERM. */
285 colorterm = getenv ("COLORTERM");
286 if (colorterm == nullptr)
287 colorterm = ""; /* Doesn't match default "?*" */
288
289 while (true)
290 {
291 char *keywd, *arg;
292 bool unrecognized;
293
294 ++line_number;
295
296 if (fp)
297 {
298 if (getline (&input_line, &input_line_size, fp) <= 0)
299 {
300 if (ferror (fp))
301 {
302 error (0, errno, _("%s: read error"), quotef (filename));
303 ok = false;
304 }
305 free (input_line);
306 break;
307 }
308 line = input_line;
309 }
310 else
311 {
312 if (next_G_line == G_line + sizeof G_line)
313 break;
314 line = next_G_line;
315 next_G_line += strlen (next_G_line) + 1;
316 }
317
318 parse_line (line, &keywd, &arg);
319
320 if (keywd == nullptr)
321 continue;
322
323 if (arg == nullptr)
324 {
325 error (0, 0, _("%s:%td: invalid line; missing second token"),
326 quotef (filename), line_number);
327 ok = false;
328 free (keywd);
329 continue;
330 }
331
332 unrecognized = false;
333 if (c_strcasecmp (keywd, "TERM") == 0)
334 {
335 if (state != ST_TERMSURE)
336 state = fnmatch (arg, term, 0) == 0 ? ST_TERMSURE : ST_TERMNO;
337 }
338 else if (c_strcasecmp (keywd, "COLORTERM") == 0)
339 {
340 if (state != ST_TERMSURE)
341 state = fnmatch (arg, colorterm, 0) == 0 ? ST_TERMSURE : ST_TERMNO;
342 }
343 else
344 {
345 if (state == ST_TERMSURE)
346 state = ST_TERMYES; /* Another {COLOR,}TERM can cancel. */
347
348 if (state != ST_TERMNO)
349 {
350 if (keywd[0] == '.')
351 append_entry ('*', keywd, arg);
352 else if (keywd[0] == '*')
353 append_entry (0, keywd, arg);
354 else if (c_strcasecmp (keywd, "OPTIONS") == 0
355 || c_strcasecmp (keywd, "COLOR") == 0
356 || c_strcasecmp (keywd, "EIGHTBIT") == 0)
357 {
358 /* Ignore. */
359 }
360 else
361 {
362 int i;
363
364 for (i = 0; slack_codes[i] != nullptr; ++i)
365 if (c_strcasecmp (keywd, slack_codes[i]) == 0)
366 break;
367
368 if (slack_codes[i] != nullptr)
369 append_entry (0, ls_codes[i], arg);
370 else
371 unrecognized = true;
372 }
373 }
374 else
375 unrecognized = true;
376 }
377
378 if (unrecognized && (state == ST_TERMSURE || state == ST_TERMYES))
379 {
380 error (0, 0, _("%s:%td: unrecognized keyword %s"),
381 (filename ? quotef (filename) : _("<internal>")),
382 line_number, keywd);
383 ok = false;
384 }
385
386 free (keywd);
387 free (arg);
388 }
389
390 return ok;
391 }
392
393 static bool
dc_parse_file(char const * filename)394 dc_parse_file (char const *filename)
395 {
396 bool ok;
397
398 if (! STREQ (filename, "-") && freopen (filename, "r", stdin) == nullptr)
399 {
400 error (0, errno, "%s", quotef (filename));
401 return false;
402 }
403
404 ok = dc_parse_stream (stdin, filename);
405
406 if (fclose (stdin) != 0)
407 {
408 error (0, errno, "%s", quotef (filename));
409 return false;
410 }
411
412 return ok;
413 }
414
415 int
main(int argc,char ** argv)416 main (int argc, char **argv)
417 {
418 bool ok = true;
419 int optc;
420 enum Shell_syntax syntax = SHELL_SYNTAX_UNKNOWN;
421 bool print_database = false;
422
423 initialize_main (&argc, &argv);
424 set_program_name (argv[0]);
425 setlocale (LC_ALL, "");
426 bindtextdomain (PACKAGE, LOCALEDIR);
427 textdomain (PACKAGE);
428
429 atexit (close_stdout);
430
431 while ((optc = getopt_long (argc, argv, "bcp", long_options, nullptr)) != -1)
432 switch (optc)
433 {
434 case 'b': /* Bourne shell syntax. */
435 syntax = SHELL_SYNTAX_BOURNE;
436 break;
437
438 case 'c': /* C shell syntax. */
439 syntax = SHELL_SYNTAX_C;
440 break;
441
442 case 'p':
443 print_database = true;
444 break;
445
446 case PRINT_LS_COLORS_OPTION:
447 print_ls_colors = true;
448 break;
449
450 case_GETOPT_HELP_CHAR;
451
452 case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
453
454 default:
455 usage (EXIT_FAILURE);
456 }
457
458 argc -= optind;
459 argv += optind;
460
461 /* It doesn't make sense to use --print with either of
462 --bourne or --c-shell. */
463 if ((print_database | print_ls_colors) && syntax != SHELL_SYNTAX_UNKNOWN)
464 {
465 error (0, 0,
466 _("the options to output non shell syntax,\n"
467 "and to select a shell syntax are mutually exclusive"));
468 usage (EXIT_FAILURE);
469 }
470
471 if (print_database && print_ls_colors)
472 {
473 error (0, 0,
474 _("options --print-database and --print-ls-colors "
475 "are mutually exclusive"));
476 usage (EXIT_FAILURE);
477 }
478
479 if ((!print_database) < argc)
480 {
481 error (0, 0, _("extra operand %s"),
482 quote (argv[!print_database]));
483 if (print_database)
484 fprintf (stderr, "%s\n",
485 _("file operands cannot be combined with "
486 "--print-database (-p)"));
487 usage (EXIT_FAILURE);
488 }
489
490 if (print_database)
491 {
492 char const *p = G_line;
493 while (p - G_line < sizeof G_line)
494 {
495 puts (p);
496 p += strlen (p) + 1;
497 }
498 }
499 else
500 {
501 /* If shell syntax was not explicitly specified, try to guess it. */
502 if (syntax == SHELL_SYNTAX_UNKNOWN && ! print_ls_colors)
503 {
504 syntax = guess_shell_syntax ();
505 if (syntax == SHELL_SYNTAX_UNKNOWN)
506 error (EXIT_FAILURE, 0,
507 _("no SHELL environment variable,"
508 " and no shell type option given"));
509 }
510
511 obstack_init (&lsc_obstack);
512 if (argc == 0)
513 ok = dc_parse_stream (nullptr, nullptr);
514 else
515 ok = dc_parse_file (argv[0]);
516
517 if (ok)
518 {
519 size_t len = obstack_object_size (&lsc_obstack);
520 char *s = obstack_finish (&lsc_obstack);
521 char const *prefix;
522 char const *suffix;
523
524 if (syntax == SHELL_SYNTAX_BOURNE)
525 {
526 prefix = "LS_COLORS='";
527 suffix = "';\nexport LS_COLORS\n";
528 }
529 else
530 {
531 prefix = "setenv LS_COLORS '";
532 suffix = "'\n";
533 }
534 if (! print_ls_colors)
535 fputs (prefix, stdout);
536 fwrite (s, 1, len, stdout);
537 if (! print_ls_colors)
538 fputs (suffix, stdout);
539 }
540 }
541
542 return ok ? EXIT_SUCCESS : EXIT_FAILURE;
543 }
544