1 /* env - run a program in a modified environment
2    Copyright (C) 1986-2018 Free Software Foundation, Inc.
3 
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8 
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13 
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <https://www.gnu.org/licenses/>.  */
16 
17 /* Richard Mlynarik and David MacKenzie */
18 
19 #include <config.h>
20 #include <stdio.h>
21 #include <sys/types.h>
22 #include <getopt.h>
23 #include <c-ctype.h>
24 
25 #include <assert.h>
26 #include "system.h"
27 #include "die.h"
28 #include "error.h"
29 #include "quote.h"
30 
31 /* The official name of this program (e.g., no 'g' prefix).  */
32 #define PROGRAM_NAME "env"
33 
34 #define AUTHORS \
35   proper_name ("Richard Mlynarik"), \
36   proper_name ("David MacKenzie"), \
37   proper_name ("Assaf Gordon")
38 
39 /* array of envvars to unset. */
40 static const char** usvars;
41 static size_t usvars_alloc;
42 static size_t usvars_used;
43 
44 /* Annotate the output with extra info to aid the user.  */
45 static bool dev_debug;
46 
47 /* buffer and length of extracted envvars in -S strings. */
48 static char *varname;
49 static size_t vnlen;
50 
51 static char const shortopts[] = "+C:iS:u:v0 \t";
52 
53 static struct option const longopts[] =
54 {
55   {"ignore-environment", no_argument, NULL, 'i'},
56   {"null", no_argument, NULL, '0'},
57   {"unset", required_argument, NULL, 'u'},
58   {"chdir", required_argument, NULL, 'C'},
59   {"debug", no_argument, NULL, 'v'},
60   {"split-string", required_argument, NULL, 'S'},
61   {GETOPT_HELP_OPTION_DECL},
62   {GETOPT_VERSION_OPTION_DECL},
63   {NULL, 0, NULL, 0}
64 };
65 
66 void
usage(int status)67 usage (int status)
68 {
69   if (status != EXIT_SUCCESS)
70     emit_try_help ();
71   else
72     {
73       printf (_("\
74 Usage: %s [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]\n"),
75               program_name);
76       fputs (_("\
77 Set each NAME to VALUE in the environment and run COMMAND.\n\
78 "), stdout);
79 
80       emit_mandatory_arg_note ();
81 
82       fputs (_("\
83   -i, --ignore-environment  start with an empty environment\n\
84   -0, --null           end each output line with NUL, not newline\n\
85   -u, --unset=NAME     remove variable from the environment\n\
86 "), stdout);
87       fputs (_("\
88   -C, --chdir=DIR      change working directory to DIR\n\
89 "), stdout);
90       fputs (_("\
91   -S, --split-string=S  process and split S into separate arguments;\n\
92                         used to pass multiple arguments on shebang lines\n\
93   -v, --debug          print verbose information for each processing step\n\
94 "), stdout);
95       fputs (HELP_OPTION_DESCRIPTION, stdout);
96       fputs (VERSION_OPTION_DESCRIPTION, stdout);
97       fputs (_("\
98 \n\
99 A mere - implies -i.  If no COMMAND, print the resulting environment.\n\
100 "), stdout);
101       emit_ancillary_info (PROGRAM_NAME);
102     }
103   exit (status);
104 }
105 
106 static void
append_unset_var(const char * var)107 append_unset_var (const char *var)
108 {
109   if (usvars_used == usvars_alloc)
110     usvars = x2nrealloc (usvars, &usvars_alloc, sizeof *usvars);
111   usvars[usvars_used++] = var;
112 }
113 
114 static void
unset_envvars(void)115 unset_envvars (void)
116 {
117   for (size_t i = 0; i < usvars_used; ++i)
118     {
119       devmsg ("unset:    %s\n", usvars[i]);
120 
121       if (unsetenv (usvars[i]))
122         die (EXIT_CANCELED, errno, _("cannot unset %s"),
123              quote (usvars[i]));
124     }
125 
126   IF_LINT (free (usvars));
127   IF_LINT (usvars = NULL);
128   IF_LINT (usvars_used = 0);
129   IF_LINT (usvars_alloc = 0);
130 }
131 
132 static bool _GL_ATTRIBUTE_PURE
valid_escape_sequence(const char c)133 valid_escape_sequence (const char c)
134 {
135   return (c == 'c' || c == 'f' || c == 'n' || c == 'r' || c == 't' || c == 'v' \
136           || c == '#' || c == '$' || c == '_' || c == '"' || c == '\'' \
137           || c == '\\');
138 }
139 
140 static char _GL_ATTRIBUTE_PURE
escape_char(const char c)141 escape_char (const char c)
142 {
143   switch (c)
144     {
145     /* \a,\b not supported by FreeBSD's env. */
146     case 'f': return '\f';
147     case 'n': return '\n';
148     case 'r': return '\r';
149     case 't': return '\t';
150     case 'v': return '\v';
151     default:  assert (0);                           /* LCOV_EXCL_LINE */
152     }
153 }
154 
155 /* Return a pointer to the end of a valid ${VARNAME} string, or NULL.
156    'str' should point to the '$' character.
157    First letter in VARNAME must be alpha or underscore,
158    rest of letters are alnum or underscore. Any other character is an error. */
159 static const char* _GL_ATTRIBUTE_PURE
scan_varname(const char * str)160 scan_varname (const char* str)
161 {
162   assert (str && *str == '$');                      /* LCOV_EXCL_LINE */
163   if ( *(str+1) == '{' && (c_isalpha (*(str+2)) || *(str+2) == '_'))
164     {
165       const char* end = str+3;
166       while (c_isalnum (*end) || *end == '_')
167         ++end;
168       if (*end == '}')
169         return end;
170     }
171 
172   return NULL;
173 }
174 
175 /* Return a pointer to a static buffer containing the VARNAME as
176    extracted from a '${VARNAME}' string.
177    The returned string will be NUL terminated.
178    The returned pointer should not be freed.
179    Return NULL if not a valid ${VARNAME} syntax. */
180 static char*
extract_varname(const char * str)181 extract_varname (const char* str)
182 {
183   ptrdiff_t i;
184   const char* p;
185 
186   p = scan_varname (str);
187   if (!p)
188     return NULL;
189 
190   /* -2 and +2 (below) account for the '${' prefix. */
191   i = p - str - 2;
192 
193   if (i >= vnlen)
194     {
195       vnlen = i + 1;
196       varname = xrealloc (varname, vnlen);
197     }
198 
199   memcpy (varname, str+2, i);
200   varname[i]=0;
201 
202   return varname;
203 }
204 
205 /* Validate the "-S" parameter, according to the syntax defined by FreeBSD's
206    env(1). Terminate with an error message if not valid.
207 
208    Calculate and set two values:
209    bufsize - the size (in bytes) required to hold the resulting string
210              after ENVVAR expansion (the value is overestimated).
211    maxargc - the maximum number of arguments (the size of the new argv). */
212 static void
validate_split_str(const char * str,size_t * bufsize,int * maxargc)213 validate_split_str (const char* str, size_t* /*out*/ bufsize,
214                     int* /*out*/ maxargc)
215 {
216   bool dq, sq, sp;
217   const char *pch;
218   size_t buflen;
219   int cnt = 1;
220 
221   assert (str && str[0] && !isspace (str[0]));     /* LCOV_EXCL_LINE */
222 
223   dq = sq = sp = false;
224   buflen = strlen (str)+1;
225 
226   while (*str)
227     {
228       const char next = *(str+1);
229 
230       if (isspace (*str) && !dq && !sq)
231         {
232           sp = true;
233         }
234       else
235         {
236           if (sp)
237             ++cnt;
238           sp = false;
239         }
240 
241       switch (*str)
242         {
243         case '\'':
244           assert (!(sq && dq));                           /* LCOV_EXCL_LINE */
245           sq = !sq && !dq;
246           break;
247 
248         case '"':
249           assert (!(sq && dq));                           /* LCOV_EXCL_LINE */
250           dq = !sq && !dq;
251           break;
252 
253         case '\\':
254           if (dq && next == 'c')
255             die (EXIT_CANCELED, 0,
256                  _("'\\c' must not appear in double-quoted -S string"));
257 
258           if (next == '\0')
259             die (EXIT_CANCELED, 0,
260                  _("invalid backslash at end of string in -S"));
261 
262           if (!valid_escape_sequence (next))
263             die (EXIT_CANCELED, 0, _("invalid sequence '\\%c' in -S"), next);
264 
265           if (next == '_')
266             ++cnt;
267 
268           ++str;
269           break;
270 
271 
272         case '$':
273           if (sq)
274             break;
275 
276           if (!(pch = extract_varname (str)))
277             die (EXIT_CANCELED, 0, _("only ${VARNAME} expansion is supported,"\
278                                      " error at: %s"), str);
279 
280           if ((pch = getenv (pch)))
281             buflen += strlen (pch);
282           break;
283         }
284       ++str;
285     }
286 
287   if (dq || sq)
288     die (EXIT_CANCELED, 0, _("no terminating quote in -S string"));
289 
290   *maxargc = cnt;
291   *bufsize = buflen;
292 }
293 
294 /* Return a newly-allocated *arg[]-like array,
295    by parsing and splitting the input 'str'.
296    'extra_argc' is the number of additional elements to allocate
297    in the array (on top of the number of args required to split 'str').
298 
299    Example:
300      char **argv = build_argv ("A=B uname -k', 3)
301    Results in:
302      argv[0] = "DUMMY" - dummy executable name, can be replaced later.
303      argv[1] = "A=B"
304      argv[2] = "uname"
305      argv[3] = "-k"
306      argv[4] = NULL
307      argv[5,6,7] = [allocated due to extra_argc, but not initialized]
308 
309    The strings are stored in an allocated buffer, pointed by argv[0].
310    To free allocated memory:
311      free (argv[0]);
312      free (argv); */
313 static char**
build_argv(const char * str,int extra_argc)314 build_argv (const char* str, int extra_argc)
315 {
316   bool dq = false, sq = false, sep = true;
317   char *dest;    /* buffer to hold the new argv values. allocated as one buffer,
318                     but will contain multiple NUL-terminate strings. */
319   char **newargv, **nextargv;
320   int newargc = 0;
321   size_t buflen = 0;
322 
323   /* This macro is called before inserting any characters to the output
324      buffer. It checks if the previous character was a separator
325      and if so starts a new argv element. */
326 #define CHECK_START_NEW_ARG                     \
327   do {                                          \
328     if (sep)                                    \
329       {                                         \
330         *dest++ = '\0';                         \
331         *nextargv++ = dest;                     \
332         sep = false;                            \
333       }                                         \
334   } while (0)
335 
336   assert (str && str[0] && !isspace (str[0]));             /* LCOV_EXCL_LINE */
337 
338   validate_split_str (str, &buflen, &newargc);
339 
340   /* allocate buffer. +6 for the "DUMMY\0" executable name, +1 for NUL. */
341   dest = xmalloc (buflen + 6 + 1);
342 
343   /* allocate the argv array.
344      +2 for the program name (argv[0]) and the last NULL pointer. */
345   nextargv = newargv = xmalloc ((newargc + extra_argc + 2) * sizeof (char *));
346 
347   /* argv[0] = executable's name  - will be replaced later. */
348   strcpy (dest, "DUMMY");
349   *nextargv++ = dest;
350   dest += 6;
351 
352   /* In the following loop,
353      'break' causes the character 'newc' to be added to *dest,
354      'continue' skips the character. */
355   while (*str)
356     {
357       char newc = *str; /* default: add the next character. */
358 
359       switch (*str)
360         {
361         case '\'':
362           if (dq)
363             break;
364           sq = !sq;
365           CHECK_START_NEW_ARG;
366           ++str;
367           continue;
368 
369         case '"':
370           if (sq)
371             break;
372           dq = !dq;
373           CHECK_START_NEW_ARG;
374           ++str;
375           continue;
376 
377         case ' ':
378         case '\t':
379           /* space/tab outside quotes starts a new argument. */
380           if (sq || dq)
381             break;
382           sep = true;
383           str += strspn (str, " \t"); /* skip whitespace. */
384           continue;
385 
386         case '#':
387           if (!sep)
388             break;
389           goto eos; /* '#' as first char terminates the string. */
390 
391         case '\\':
392           /* backslash inside single-quotes is not special, except \\ and \'. */
393           if (sq && *(str+1) != '\\' && *(str+1) != '\'')
394             break;
395 
396           /* skip the backslash and examine the next character. */
397           newc = *(++str);
398           if ((newc == '\\' || newc == '\'')
399               || (!sq && (newc == '#' || newc == '$' || newc == '"')))
400             {
401               /* Pass escaped character as-is. */
402             }
403           else if (newc == '_')
404             {
405               if (!dq)
406                 {
407                   ++str;  /* '\_' outside double-quotes is arg separator. */
408                   sep = true;
409                   continue;
410                 }
411               else
412                   newc = ' ';  /* '\_' inside double-quotes is space. */
413             }
414           else if (newc == 'c')
415               goto eos; /* '\c' terminates the string. */
416           else
417               newc = escape_char (newc); /* other characters (e.g. '\n'). */
418           break;
419 
420         case '$':
421           /* ${VARNAME} are not expanded inside single-quotes. */
422           if (sq)
423             break;
424 
425           /* Store the ${VARNAME} value. Error checking omitted as
426              the ${VARNAME} was already validated. */
427           {
428             char *n = extract_varname (str);
429             char *v = getenv (n);
430             if (v)
431               {
432                 CHECK_START_NEW_ARG;
433                 devmsg ("expanding ${%s} into %s\n", n, quote (v));
434                 dest = stpcpy (dest, v);
435               }
436             else
437               devmsg ("replacing ${%s} with null string\n", n);
438 
439             str = strchr (str, '}') + 1;
440             continue;
441           }
442 
443         }
444 
445       CHECK_START_NEW_ARG;
446       *dest++ = newc;
447       ++str;
448     }
449 
450  eos:
451   *dest = '\0';
452   *nextargv = NULL; /* mark the last element in argv as NULL. */
453 
454   return newargv;
455 }
456 
457 /* Process an "-S" string and create the corresponding argv array.
458    Update the given argc/argv parameters with the new argv.
459 
460    Example: if executed as:
461       $ env -S"-i -C/tmp A=B" foo bar
462    The input argv is:
463       argv[0] = 'env'
464       argv[1] = "-S-i -C/tmp A=B"
465       argv[2] = foo
466       argv[3] = bar
467    This function will modify argv to be:
468       argv[0] = 'env'
469       argv[1] = "-i"
470       argv[2] = "-C/tmp"
471       argv[3] =  A=B"
472       argv[4] = foo
473       argv[5] = bar
474    argc will be updated from 4 to 6.
475    optind will be reset to 0 to force getopt_long to rescan all arguments. */
476 static void
parse_split_string(const char * str,int * orig_optind,int * orig_argc,char *** orig_argv)477 parse_split_string (const char* str, int /*out*/ *orig_optind,
478                     int /*out*/ *orig_argc, char*** /*out*/ orig_argv)
479 {
480   int i, newargc;
481   char **newargv, **nextargv;
482 
483 
484   while (isspace (*str))
485     str++;
486   if (*str == '\0')
487     return;
488 
489   newargv = build_argv (str, *orig_argc - *orig_optind);
490 
491   /* restore argv[0] - the 'env' executable name */
492   *newargv = (*orig_argv)[0];
493 
494   /* Start from argv[1] */
495   nextargv = newargv + 1;
496 
497   /* Print parsed arguments */
498   if (dev_debug && *nextargv)
499     {
500       devmsg ("split -S:  %s\n", quote (str));
501       devmsg (" into:    %s\n", quote (*nextargv++));
502       while (*nextargv)
503         devmsg ("     &    %s\n", quote (*nextargv++));
504     }
505   else
506     {
507       /* Ensure nextargv points to the last argument */
508       while (*nextargv)
509         ++nextargv;
510     }
511 
512   /* Add remaining arguments from original command line */
513   for (i = *orig_optind; i < *orig_argc; ++i)
514     *nextargv++ = (*orig_argv)[i];
515   *nextargv = NULL;
516 
517   /* Count how many new arguments we have */
518   newargc = 0;
519   for (nextargv = newargv; *nextargv; ++nextargv)
520     ++newargc;
521 
522   /* set new values for original getopt variables */
523   *orig_argc = newargc;
524   *orig_argv = newargv;
525   *orig_optind = 0; /* tell getopt to restart from first argument */
526 }
527 
528 int
main(int argc,char ** argv)529 main (int argc, char **argv)
530 {
531   int optc;
532   bool ignore_environment = false;
533   bool opt_nul_terminate_output = false;
534   char const *newdir = NULL;
535 
536   initialize_main (&argc, &argv);
537   set_program_name (argv[0]);
538   setlocale (LC_ALL, "");
539   bindtextdomain (PACKAGE, LOCALEDIR);
540   textdomain (PACKAGE);
541 
542   initialize_exit_failure (EXIT_CANCELED);
543   atexit (close_stdout);
544 
545   while ((optc = getopt_long (argc, argv, shortopts, longopts, NULL)) != -1)
546     {
547       switch (optc)
548         {
549         case 'i':
550           ignore_environment = true;
551           break;
552         case 'u':
553           append_unset_var (optarg);
554           break;
555         case 'v':
556           dev_debug = true;
557           break;
558         case '0':
559           opt_nul_terminate_output = true;
560           break;
561         case 'C':
562           newdir = optarg;
563           break;
564         case 'S':
565           parse_split_string (optarg, &optind, &argc, &argv);
566           break;
567         case ' ':
568         case '\t':
569           /* These are undocumented options. Attempt to detect
570              incorrect shebang usage with extraneous space, e.g.:
571                 #!/usr/bin/env -i command
572              In which case argv[1] == "-i command".  */
573           error (0, 0, _("invalid option -- '%c'"), optc);
574           error (0, 0, _("use -[v]S to pass options in shebang lines"));
575           usage (EXIT_CANCELED);
576 
577         case_GETOPT_HELP_CHAR;
578         case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS);
579         default:
580           usage (EXIT_CANCELED);
581         }
582     }
583 
584   if (optind < argc && STREQ (argv[optind], "-"))
585     {
586       ignore_environment = true;
587       ++optind;
588     }
589 
590   if (ignore_environment)
591     {
592       devmsg ("cleaning environ\n");
593       static char *dummy_environ[] = { NULL };
594       environ = dummy_environ;
595     }
596   else
597     unset_envvars ();
598 
599   char *eq;
600   while (optind < argc && (eq = strchr (argv[optind], '=')))
601     {
602       devmsg ("setenv:   %s\n", argv[optind]);
603 
604       if (putenv (argv[optind]))
605         {
606           *eq = '\0';
607           die (EXIT_CANCELED, errno, _("cannot set %s"),
608                quote (argv[optind]));
609         }
610       optind++;
611     }
612 
613   bool program_specified = optind < argc;
614 
615   if (opt_nul_terminate_output && program_specified)
616     {
617       error (0, 0, _("cannot specify --null (-0) with command"));
618       usage (EXIT_CANCELED);
619     }
620 
621   if (newdir && ! program_specified)
622     {
623       error (0, 0, _("must specify command with --chdir (-C)"));
624       usage (EXIT_CANCELED);
625     }
626 
627   if (! program_specified)
628     {
629       /* Print the environment and exit. */
630       char *const *e = environ;
631       while (*e)
632         printf ("%s%c", *e++, opt_nul_terminate_output ? '\0' : '\n');
633       return EXIT_SUCCESS;
634     }
635 
636   if (newdir)
637     {
638       devmsg ("chdir:    %s\n", quoteaf (newdir));
639 
640       if (chdir (newdir) != 0)
641         die (EXIT_CANCELED, errno, _("cannot change directory to %s"),
642              quoteaf (newdir));
643     }
644 
645   if (dev_debug)
646     {
647       devmsg ("executing: %s\n", argv[optind]);
648       for (int i=optind; i<argc; ++i)
649         devmsg ("   arg[%d]= %s\n", i-optind, quote (argv[i]));
650     }
651 
652   execvp (argv[optind], &argv[optind]);
653 
654   int exit_status = errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE;
655   error (0, errno, "%s", quote (argv[optind]));
656 
657   if (exit_status == EXIT_ENOENT && strchr (argv[optind], ' '))
658     error (0, 0, _("use -[v]S to pass options in shebang lines"));
659 
660   return exit_status;
661 }
662