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