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