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