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