1/* Substitution of environment variables in shell format strings.
2   Copyright (C) 2003-2006 Free Software Foundation, Inc.
3   Written by Bruno Haible <bruno@clisp.org>, 2003.
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 2, or (at your option)
8   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, write to the Free Software Foundation,
17   Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.  */
18
19#ifdef HAVE_CONFIG_H
20# include <config.h>
21#endif
22
23#include <errno.h>
24#include <getopt.h>
25#include <stdbool.h>
26#include <stdio.h>
27#include <stdlib.h>
28#include <string.h>
29#include <locale.h>
30
31#include "closeout.h"
32#include "error.h"
33#include "progname.h"
34#include "relocatable.h"
35#include "basename.h"
36#include "xalloc.h"
37#include "exit.h"
38#include "propername.h"
39#include "gettext.h"
40
41#define _(str) gettext (str)
42
43/* If true, substitution shall be performed on all variables.  */
44static bool all_variables;
45
46/* Long options.  */
47static const struct option long_options[] =
48{
49  { "help", no_argument, NULL, 'h' },
50  { "variables", no_argument, NULL, 'v' },
51  { "version", no_argument, NULL, 'V' },
52  { NULL, 0, NULL, 0 }
53};
54
55/* Forward declaration of local functions.  */
56static void usage (int status)
57#if defined __GNUC__ && ((__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || __GNUC__ > 2)
58     __attribute__ ((noreturn))
59#endif
60;
61static void print_variables (const char *string);
62static void note_variables (const char *string);
63static void subst_from_stdin (void);
64
65int
66main (int argc, char *argv[])
67{
68  /* Default values for command line options.  */
69  bool show_variables = false;
70  bool do_help = false;
71  bool do_version = false;
72
73  int opt;
74
75  /* Set program name for message texts.  */
76  set_program_name (argv[0]);
77
78#ifdef HAVE_SETLOCALE
79  /* Set locale via LC_ALL.  */
80  setlocale (LC_ALL, "");
81#endif
82
83  /* Set the text message domain.  */
84  bindtextdomain (PACKAGE, relocate (LOCALEDIR));
85  textdomain (PACKAGE);
86
87  /* Ensure that write errors on stdout are detected.  */
88  atexit (close_stdout);
89
90  /* Parse command line options.  */
91  while ((opt = getopt_long (argc, argv, "hvV", long_options, NULL)) != EOF)
92    switch (opt)
93    {
94    case '\0':		/* Long option.  */
95      break;
96    case 'h':
97      do_help = true;
98      break;
99    case 'v':
100      show_variables = true;
101      break;
102    case 'V':
103      do_version = true;
104      break;
105    default:
106      usage (EXIT_FAILURE);
107    }
108
109  /* Version information is requested.  */
110  if (do_version)
111    {
112      printf ("%s (GNU %s) %s\n", basename (program_name), PACKAGE, VERSION);
113      /* xgettext: no-wrap */
114      printf (_("Copyright (C) %s Free Software Foundation, Inc.\n\
115This is free software; see the source for copying conditions.  There is NO\n\
116warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n\
117"),
118	      "2003-2006");
119      printf (_("Written by %s.\n"), proper_name ("Bruno Haible"));
120      exit (EXIT_SUCCESS);
121    }
122
123  /* Help is requested.  */
124  if (do_help)
125    usage (EXIT_SUCCESS);
126
127  if (argc - optind > 1)
128    error (EXIT_FAILURE, 0, _("too many arguments"));
129
130  /* Distinguish the two main operation modes.  */
131  if (show_variables)
132    {
133      /* Output only the variables.  */
134      switch (argc - optind)
135	{
136	case 1:
137	  break;
138	case 0:
139	  error (EXIT_FAILURE, 0, _("missing arguments"));
140	default:
141	  abort ();
142	}
143      print_variables (argv[optind++]);
144    }
145  else
146    {
147      /* Actually perform the substitutions.  */
148      switch (argc - optind)
149	{
150	case 1:
151	  all_variables = false;
152	  note_variables (argv[optind++]);
153	  break;
154	case 0:
155	  all_variables = true;
156	  break;
157	default:
158	  abort ();
159	}
160      subst_from_stdin ();
161    }
162
163  exit (EXIT_SUCCESS);
164}
165
166
167/* Display usage information and exit.  */
168static void
169usage (int status)
170{
171  if (status != EXIT_SUCCESS)
172    fprintf (stderr, _("Try `%s --help' for more information.\n"),
173	     program_name);
174  else
175    {
176      /* xgettext: no-wrap */
177      printf (_("\
178Usage: %s [OPTION] [SHELL-FORMAT]\n\
179"), program_name);
180      printf ("\n");
181      /* xgettext: no-wrap */
182      printf (_("\
183Substitutes the values of environment variables.\n"));
184      printf ("\n");
185      /* xgettext: no-wrap */
186      printf (_("\
187Operation mode:\n"));
188      /* xgettext: no-wrap */
189      printf (_("\
190  -v, --variables             output the variables occurring in SHELL-FORMAT\n"));
191      printf ("\n");
192      /* xgettext: no-wrap */
193      printf (_("\
194Informative output:\n"));
195      /* xgettext: no-wrap */
196      printf (_("\
197  -h, --help                  display this help and exit\n"));
198      /* xgettext: no-wrap */
199      printf (_("\
200  -V, --version               output version information and exit\n"));
201      printf ("\n");
202      /* xgettext: no-wrap */
203      printf (_("\
204In normal operation mode, standard input is copied to standard output,\n\
205with references to environment variables of the form $VARIABLE or ${VARIABLE}\n\
206being replaced with the corresponding values.  If a SHELL-FORMAT is given,\n\
207only those environment variables that are referenced in SHELL-FORMAT are\n\
208substituted; otherwise all environment variables references occurring in\n\
209standard input are substituted.\n"));
210      printf ("\n");
211      /* xgettext: no-wrap */
212      printf (_("\
213When --variables is used, standard input is ignored, and the output consists\n\
214of the environment variables that are referenced in SHELL-FORMAT, one per line.\n"));
215      printf ("\n");
216      fputs (_("Report bugs to <bug-gnu-gettext@gnu.org>.\n"), stdout);
217    }
218
219  exit (status);
220}
221
222
223/* Parse the string and invoke the callback each time a $VARIABLE or
224   ${VARIABLE} construct is seen, where VARIABLE is a nonempty sequence
225   of ASCII alphanumeric/underscore characters, starting with an ASCII
226   alphabetic/underscore character.
227   We allow only ASCII characters, to avoid dependencies w.r.t. the current
228   encoding: While "${\xe0}" looks like a variable access in ISO-8859-1
229   encoding, it doesn't look like one in the BIG5, BIG5-HKSCS, GBK, GB18030,
230   SHIFT_JIS, JOHAB encodings, because \xe0\x7d is a single character in these
231   encodings.  */
232static void
233find_variables (const char *string,
234		void (*callback) (const char *var_ptr, size_t var_len))
235{
236  for (; *string != '\0';)
237    if (*string++ == '$')
238      {
239	const char *variable_start;
240	const char *variable_end;
241	bool valid;
242	char c;
243
244	if (*string == '{')
245	  string++;
246
247	variable_start = string;
248	c = *string;
249	if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')
250	  {
251	    do
252	      c = *++string;
253	    while ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
254		   || (c >= '0' && c <= '9') || c == '_');
255	    variable_end = string;
256
257	    if (variable_start[-1] == '{')
258	      {
259		if (*string == '}')
260		  {
261		    string++;
262		    valid = true;
263		  }
264		else
265		  valid = false;
266	      }
267	    else
268	      valid = true;
269
270	    if (valid)
271	      callback (variable_start, variable_end - variable_start);
272	  }
273      }
274}
275
276
277/* Print a variable to stdout, followed by a newline.  */
278static void
279print_variable (const char *var_ptr, size_t var_len)
280{
281  fwrite (var_ptr, var_len, 1, stdout);
282  putchar ('\n');
283}
284
285/* Print the variables contained in STRING to stdout, each one followed by a
286   newline.  */
287static void
288print_variables (const char *string)
289{
290  find_variables (string, &print_variable);
291}
292
293
294/* Type describing list of immutable strings,
295   implemented using a dynamic array.  */
296typedef struct string_list_ty string_list_ty;
297struct string_list_ty
298{
299  const char **item;
300  size_t nitems;
301  size_t nitems_max;
302};
303
304/* Initialize an empty list of strings.  */
305static inline void
306string_list_init (string_list_ty *slp)
307{
308  slp->item = NULL;
309  slp->nitems = 0;
310  slp->nitems_max = 0;
311}
312
313/* Append a single string to the end of a list of strings.  */
314static inline void
315string_list_append (string_list_ty *slp, const char *s)
316{
317  /* Grow the list.  */
318  if (slp->nitems >= slp->nitems_max)
319    {
320      size_t nbytes;
321
322      slp->nitems_max = slp->nitems_max * 2 + 4;
323      nbytes = slp->nitems_max * sizeof (slp->item[0]);
324      slp->item = (const char **) xrealloc (slp->item, nbytes);
325    }
326
327  /* Add the string to the end of the list.  */
328  slp->item[slp->nitems++] = s;
329}
330
331/* Compare two strings given by reference.  */
332static int
333cmp_string (const void *pstr1, const void *pstr2)
334{
335  const char *str1 = *(const char **)pstr1;
336  const char *str2 = *(const char **)pstr2;
337
338  return strcmp (str1, str2);
339}
340
341/* Sort a list of strings.  */
342static inline void
343string_list_sort (string_list_ty *slp)
344{
345  if (slp->nitems > 0)
346    qsort (slp->item, slp->nitems, sizeof (slp->item[0]), cmp_string);
347}
348
349/* Test whether a string list contains a given string.  */
350static inline int
351string_list_member (const string_list_ty *slp, const char *s)
352{
353  size_t j;
354
355  for (j = 0; j < slp->nitems; ++j)
356    if (strcmp (slp->item[j], s) == 0)
357      return 1;
358  return 0;
359}
360
361/* Test whether a sorted string list contains a given string.  */
362static int
363sorted_string_list_member (const string_list_ty *slp, const char *s)
364{
365  size_t j1, j2;
366
367  j1 = 0;
368  j2 = slp->nitems;
369  if (j2 > 0)
370    {
371      /* Binary search.  */
372      while (j2 - j1 > 1)
373	{
374	  /* Here we know that if s is in the list, it is at an index j
375	     with j1 <= j < j2.  */
376	  size_t j = (j1 + j2) >> 1;
377	  int result = strcmp (slp->item[j], s);
378
379	  if (result > 0)
380	    j2 = j;
381	  else if (result == 0)
382	    return 1;
383	  else
384	    j1 = j + 1;
385	}
386      if (j2 > j1)
387	if (strcmp (slp->item[j1], s) == 0)
388	  return 1;
389    }
390  return 0;
391}
392
393/* Destroy a list of strings.  */
394static inline void
395string_list_destroy (string_list_ty *slp)
396{
397  size_t j;
398
399  for (j = 0; j < slp->nitems; ++j)
400    free ((char *) slp->item[j]);
401  if (slp->item != NULL)
402    free (slp->item);
403}
404
405
406/* Set of variables on which to perform substitution.
407   Used only if !all_variables.  */
408static string_list_ty variables_set;
409
410/* Adds a variable to variables_set.  */
411static void
412note_variable (const char *var_ptr, size_t var_len)
413{
414  char *string = (char *) xmalloc (var_len + 1);
415  memcpy (string, var_ptr, var_len);
416  string[var_len] = '\0';
417
418  string_list_append (&variables_set, string);
419}
420
421/* Stores the variables occurring in the string in variables_set.  */
422static void
423note_variables (const char *string)
424{
425  string_list_init (&variables_set);
426  find_variables (string, &note_variable);
427  string_list_sort (&variables_set);
428}
429
430
431static int
432do_getc ()
433{
434  int c = getc (stdin);
435
436  if (c == EOF)
437    {
438      if (ferror (stdin))
439	error (EXIT_FAILURE, errno, _("\
440error while reading \"%s\""), _("standard input"));
441    }
442
443  return c;
444}
445
446static inline void
447do_ungetc (int c)
448{
449  if (c != EOF)
450    ungetc (c, stdin);
451}
452
453/* Copies stdin to stdout, performing substitutions.  */
454static void
455subst_from_stdin ()
456{
457  static char *buffer;
458  static size_t bufmax;
459  static size_t buflen;
460  int c;
461
462  for (;;)
463    {
464      c = do_getc ();
465      if (c == EOF)
466	break;
467      /* Look for $VARIABLE or ${VARIABLE}.  */
468      if (c == '$')
469	{
470	  bool opening_brace = false;
471	  bool closing_brace = false;
472
473	  c = do_getc ();
474	  if (c == '{')
475	    {
476	      opening_brace = true;
477	      c = do_getc ();
478	    }
479	  if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')
480	    {
481	      bool valid;
482
483	      /* Accumulate the VARIABLE in buffer.  */
484	      buflen = 0;
485	      do
486		{
487		  if (buflen >= bufmax)
488		    {
489		      bufmax = 2 * bufmax + 10;
490		      buffer = xrealloc (buffer, bufmax);
491		    }
492		  buffer[buflen++] = c;
493
494		  c = do_getc ();
495		}
496	      while ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
497		     || (c >= '0' && c <= '9') || c == '_');
498
499	      if (opening_brace)
500		{
501		  if (c == '}')
502		    {
503		      closing_brace = true;
504		      valid = true;
505		    }
506		  else
507		    {
508		      valid = false;
509		      do_ungetc (c);
510		    }
511		}
512	      else
513		{
514		  valid = true;
515		  do_ungetc (c);
516		}
517
518	      if (valid)
519		{
520		  /* Terminate the variable in the buffer.  */
521		  if (buflen >= bufmax)
522		    {
523		      bufmax = 2 * bufmax + 10;
524		      buffer = xrealloc (buffer, bufmax);
525		    }
526		  buffer[buflen] = '\0';
527
528		  /* Test whether the variable shall be substituted.  */
529		  if (!all_variables
530		      && !sorted_string_list_member (&variables_set, buffer))
531		    valid = false;
532		}
533
534	      if (valid)
535		{
536		  /* Substitute the variable's value from the environment.  */
537		  const char *env_value = getenv (buffer);
538
539		  if (env_value != NULL)
540		    fputs (env_value, stdout);
541		}
542	      else
543		{
544		  /* Perform no substitution at all.  Since the buffered input
545		     contains no other '$' than at the start, we can just
546		     output all the buffered contents.  */
547		  putchar ('$');
548		  if (opening_brace)
549		    putchar ('{');
550		  fwrite (buffer, buflen, 1, stdout);
551		  if (closing_brace)
552		    putchar ('}');
553		}
554	    }
555	  else
556	    {
557	      do_ungetc (c);
558	      putchar ('$');
559	      if (opening_brace)
560		putchar ('{');
561	    }
562	}
563      else
564	putchar (c);
565    }
566}
567