diff options
Diffstat (limited to 'src/env.c')
-rw-r--r-- | src/env.c | 661 |
1 files changed, 661 insertions, 0 deletions
diff --git a/src/env.c b/src/env.c new file mode 100644 index 0000000..d179787 --- /dev/null +++ b/src/env.c @@ -0,0 +1,661 @@ +/* env - run a program in a modified environment + Copyright (C) 1986-2018 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Richard Mlynarik and David MacKenzie */ + +#include <config.h> +#include <stdio.h> +#include <sys/types.h> +#include <getopt.h> +#include <c-ctype.h> + +#include <assert.h> +#include "system.h" +#include "die.h" +#include "error.h" +#include "quote.h" + +/* The official name of this program (e.g., no 'g' prefix). */ +#define PROGRAM_NAME "env" + +#define AUTHORS \ + proper_name ("Richard Mlynarik"), \ + proper_name ("David MacKenzie"), \ + proper_name ("Assaf Gordon") + +/* array of envvars to unset. */ +static const char** usvars; +static size_t usvars_alloc; +static size_t usvars_used; + +/* Annotate the output with extra info to aid the user. */ +static bool dev_debug; + +/* buffer and length of extracted envvars in -S strings. */ +static char *varname; +static size_t vnlen; + +static char const shortopts[] = "+C:iS:u:v0 \t"; + +static struct option const longopts[] = +{ + {"ignore-environment", no_argument, NULL, 'i'}, + {"null", no_argument, NULL, '0'}, + {"unset", required_argument, NULL, 'u'}, + {"chdir", required_argument, NULL, 'C'}, + {"debug", no_argument, NULL, 'v'}, + {"split-string", required_argument, NULL, 'S'}, + {GETOPT_HELP_OPTION_DECL}, + {GETOPT_VERSION_OPTION_DECL}, + {NULL, 0, NULL, 0} +}; + +void +usage (int status) +{ + if (status != EXIT_SUCCESS) + emit_try_help (); + else + { + printf (_("\ +Usage: %s [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]\n"), + program_name); + fputs (_("\ +Set each NAME to VALUE in the environment and run COMMAND.\n\ +"), stdout); + + emit_mandatory_arg_note (); + + fputs (_("\ + -i, --ignore-environment start with an empty environment\n\ + -0, --null end each output line with NUL, not newline\n\ + -u, --unset=NAME remove variable from the environment\n\ +"), stdout); + fputs (_("\ + -C, --chdir=DIR change working directory to DIR\n\ +"), stdout); + fputs (_("\ + -S, --split-string=S process and split S into separate arguments;\n\ + used to pass multiple arguments on shebang lines\n\ + -v, --debug print verbose information for each processing step\n\ +"), stdout); + fputs (HELP_OPTION_DESCRIPTION, stdout); + fputs (VERSION_OPTION_DESCRIPTION, stdout); + fputs (_("\ +\n\ +A mere - implies -i. If no COMMAND, print the resulting environment.\n\ +"), stdout); + emit_ancillary_info (PROGRAM_NAME); + } + exit (status); +} + +static void +append_unset_var (const char *var) +{ + if (usvars_used == usvars_alloc) + usvars = x2nrealloc (usvars, &usvars_alloc, sizeof *usvars); + usvars[usvars_used++] = var; +} + +static void +unset_envvars (void) +{ + for (size_t i = 0; i < usvars_used; ++i) + { + devmsg ("unset: %s\n", usvars[i]); + + if (unsetenv (usvars[i])) + die (EXIT_CANCELED, errno, _("cannot unset %s"), + quote (usvars[i])); + } + + IF_LINT (free (usvars)); + IF_LINT (usvars = NULL); + IF_LINT (usvars_used = 0); + IF_LINT (usvars_alloc = 0); +} + +static bool _GL_ATTRIBUTE_PURE +valid_escape_sequence (const char c) +{ + return (c == 'c' || c == 'f' || c == 'n' || c == 'r' || c == 't' || c == 'v' \ + || c == '#' || c == '$' || c == '_' || c == '"' || c == '\'' \ + || c == '\\'); +} + +static char _GL_ATTRIBUTE_PURE +escape_char (const char c) +{ + switch (c) + { + /* \a,\b not supported by FreeBSD's env. */ + case 'f': return '\f'; + case 'n': return '\n'; + case 'r': return '\r'; + case 't': return '\t'; + case 'v': return '\v'; + default: assert (0); /* LCOV_EXCL_LINE */ + } +} + +/* Return a pointer to the end of a valid ${VARNAME} string, or NULL. + 'str' should point to the '$' character. + First letter in VARNAME must be alpha or underscore, + rest of letters are alnum or underscore. Any other character is an error. */ +static const char* _GL_ATTRIBUTE_PURE +scan_varname (const char* str) +{ + assert (str && *str == '$'); /* LCOV_EXCL_LINE */ + if ( *(str+1) == '{' && (c_isalpha (*(str+2)) || *(str+2) == '_')) + { + const char* end = str+3; + while (c_isalnum (*end) || *end == '_') + ++end; + if (*end == '}') + return end; + } + + return NULL; +} + +/* Return a pointer to a static buffer containing the VARNAME as + extracted from a '${VARNAME}' string. + The returned string will be NUL terminated. + The returned pointer should not be freed. + Return NULL if not a valid ${VARNAME} syntax. */ +static char* +extract_varname (const char* str) +{ + ptrdiff_t i; + const char* p; + + p = scan_varname (str); + if (!p) + return NULL; + + /* -2 and +2 (below) account for the '${' prefix. */ + i = p - str - 2; + + if (i >= vnlen) + { + vnlen = i + 1; + varname = xrealloc (varname, vnlen); + } + + memcpy (varname, str+2, i); + varname[i]=0; + + return varname; +} + +/* Validate the "-S" parameter, according to the syntax defined by FreeBSD's + env(1). Terminate with an error message if not valid. + + Calculate and set two values: + bufsize - the size (in bytes) required to hold the resulting string + after ENVVAR expansion (the value is overestimated). + maxargc - the maximum number of arguments (the size of the new argv). */ +static void +validate_split_str (const char* str, size_t* /*out*/ bufsize, + int* /*out*/ maxargc) +{ + bool dq, sq, sp; + const char *pch; + size_t buflen; + int cnt = 1; + + assert (str && str[0] && !isspace (str[0])); /* LCOV_EXCL_LINE */ + + dq = sq = sp = false; + buflen = strlen (str)+1; + + while (*str) + { + const char next = *(str+1); + + if (isspace (*str) && !dq && !sq) + { + sp = true; + } + else + { + if (sp) + ++cnt; + sp = false; + } + + switch (*str) + { + case '\'': + assert (!(sq && dq)); /* LCOV_EXCL_LINE */ + sq = !sq && !dq; + break; + + case '"': + assert (!(sq && dq)); /* LCOV_EXCL_LINE */ + dq = !sq && !dq; + break; + + case '\\': + if (dq && next == 'c') + die (EXIT_CANCELED, 0, + _("'\\c' must not appear in double-quoted -S string")); + + if (next == '\0') + die (EXIT_CANCELED, 0, + _("invalid backslash at end of string in -S")); + + if (!valid_escape_sequence (next)) + die (EXIT_CANCELED, 0, _("invalid sequence '\\%c' in -S"), next); + + if (next == '_') + ++cnt; + + ++str; + break; + + + case '$': + if (sq) + break; + + if (!(pch = extract_varname (str))) + die (EXIT_CANCELED, 0, _("only ${VARNAME} expansion is supported,"\ + " error at: %s"), str); + + if ((pch = getenv (pch))) + buflen += strlen (pch); + break; + } + ++str; + } + + if (dq || sq) + die (EXIT_CANCELED, 0, _("no terminating quote in -S string")); + + *maxargc = cnt; + *bufsize = buflen; +} + +/* Return a newly-allocated *arg[]-like array, + by parsing and splitting the input 'str'. + 'extra_argc' is the number of additional elements to allocate + in the array (on top of the number of args required to split 'str'). + + Example: + char **argv = build_argv ("A=B uname -k', 3) + Results in: + argv[0] = "DUMMY" - dummy executable name, can be replaced later. + argv[1] = "A=B" + argv[2] = "uname" + argv[3] = "-k" + argv[4] = NULL + argv[5,6,7] = [allocated due to extra_argc, but not initialized] + + The strings are stored in an allocated buffer, pointed by argv[0]. + To free allocated memory: + free (argv[0]); + free (argv); */ +static char** +build_argv (const char* str, int extra_argc) +{ + bool dq = false, sq = false, sep = true; + char *dest; /* buffer to hold the new argv values. allocated as one buffer, + but will contain multiple NUL-terminate strings. */ + char **newargv, **nextargv; + int newargc = 0; + size_t buflen = 0; + + /* This macro is called before inserting any characters to the output + buffer. It checks if the previous character was a separator + and if so starts a new argv element. */ +#define CHECK_START_NEW_ARG \ + do { \ + if (sep) \ + { \ + *dest++ = '\0'; \ + *nextargv++ = dest; \ + sep = false; \ + } \ + } while (0) + + assert (str && str[0] && !isspace (str[0])); /* LCOV_EXCL_LINE */ + + validate_split_str (str, &buflen, &newargc); + + /* allocate buffer. +6 for the "DUMMY\0" executable name, +1 for NUL. */ + dest = xmalloc (buflen + 6 + 1); + + /* allocate the argv array. + +2 for the program name (argv[0]) and the last NULL pointer. */ + nextargv = newargv = xmalloc ((newargc + extra_argc + 2) * sizeof (char *)); + + /* argv[0] = executable's name - will be replaced later. */ + strcpy (dest, "DUMMY"); + *nextargv++ = dest; + dest += 6; + + /* In the following loop, + 'break' causes the character 'newc' to be added to *dest, + 'continue' skips the character. */ + while (*str) + { + char newc = *str; /* default: add the next character. */ + + switch (*str) + { + case '\'': + if (dq) + break; + sq = !sq; + CHECK_START_NEW_ARG; + ++str; + continue; + + case '"': + if (sq) + break; + dq = !dq; + CHECK_START_NEW_ARG; + ++str; + continue; + + case ' ': + case '\t': + /* space/tab outside quotes starts a new argument. */ + if (sq || dq) + break; + sep = true; + str += strspn (str, " \t"); /* skip whitespace. */ + continue; + + case '#': + if (!sep) + break; + goto eos; /* '#' as first char terminates the string. */ + + case '\\': + /* backslash inside single-quotes is not special, except \\ and \'. */ + if (sq && *(str+1) != '\\' && *(str+1) != '\'') + break; + + /* skip the backslash and examine the next character. */ + newc = *(++str); + if ((newc == '\\' || newc == '\'') + || (!sq && (newc == '#' || newc == '$' || newc == '"'))) + { + /* Pass escaped character as-is. */ + } + else if (newc == '_') + { + if (!dq) + { + ++str; /* '\_' outside double-quotes is arg separator. */ + sep = true; + continue; + } + else + newc = ' '; /* '\_' inside double-quotes is space. */ + } + else if (newc == 'c') + goto eos; /* '\c' terminates the string. */ + else + newc = escape_char (newc); /* other characters (e.g. '\n'). */ + break; + + case '$': + /* ${VARNAME} are not expanded inside single-quotes. */ + if (sq) + break; + + /* Store the ${VARNAME} value. Error checking omitted as + the ${VARNAME} was already validated. */ + { + char *n = extract_varname (str); + char *v = getenv (n); + if (v) + { + CHECK_START_NEW_ARG; + devmsg ("expanding ${%s} into %s\n", n, quote (v)); + dest = stpcpy (dest, v); + } + else + devmsg ("replacing ${%s} with null string\n", n); + + str = strchr (str, '}') + 1; + continue; + } + + } + + CHECK_START_NEW_ARG; + *dest++ = newc; + ++str; + } + + eos: + *dest = '\0'; + *nextargv = NULL; /* mark the last element in argv as NULL. */ + + return newargv; +} + +/* Process an "-S" string and create the corresponding argv array. + Update the given argc/argv parameters with the new argv. + + Example: if executed as: + $ env -S"-i -C/tmp A=B" foo bar + The input argv is: + argv[0] = 'env' + argv[1] = "-S-i -C/tmp A=B" + argv[2] = foo + argv[3] = bar + This function will modify argv to be: + argv[0] = 'env' + argv[1] = "-i" + argv[2] = "-C/tmp" + argv[3] = A=B" + argv[4] = foo + argv[5] = bar + argc will be updated from 4 to 6. + optind will be reset to 0 to force getopt_long to rescan all arguments. */ +static void +parse_split_string (const char* str, int /*out*/ *orig_optind, + int /*out*/ *orig_argc, char*** /*out*/ orig_argv) +{ + int i, newargc; + char **newargv, **nextargv; + + + while (isspace (*str)) + str++; + if (*str == '\0') + return; + + newargv = build_argv (str, *orig_argc - *orig_optind); + + /* restore argv[0] - the 'env' executable name */ + *newargv = (*orig_argv)[0]; + + /* Start from argv[1] */ + nextargv = newargv + 1; + + /* Print parsed arguments */ + if (dev_debug && *nextargv) + { + devmsg ("split -S: %s\n", quote (str)); + devmsg (" into: %s\n", quote (*nextargv++)); + while (*nextargv) + devmsg (" & %s\n", quote (*nextargv++)); + } + else + { + /* Ensure nextargv points to the last argument */ + while (*nextargv) + ++nextargv; + } + + /* Add remaining arguments from original command line */ + for (i = *orig_optind; i < *orig_argc; ++i) + *nextargv++ = (*orig_argv)[i]; + *nextargv = NULL; + + /* Count how many new arguments we have */ + newargc = 0; + for (nextargv = newargv; *nextargv; ++nextargv) + ++newargc; + + /* set new values for original getopt variables */ + *orig_argc = newargc; + *orig_argv = newargv; + *orig_optind = 0; /* tell getopt to restart from first argument */ +} + +int +main (int argc, char **argv) +{ + int optc; + bool ignore_environment = false; + bool opt_nul_terminate_output = false; + char const *newdir = NULL; + + initialize_main (&argc, &argv); + set_program_name (argv[0]); + setlocale (LC_ALL, ""); + bindtextdomain (PACKAGE, LOCALEDIR); + textdomain (PACKAGE); + + initialize_exit_failure (EXIT_CANCELED); + atexit (close_stdout); + + while ((optc = getopt_long (argc, argv, shortopts, longopts, NULL)) != -1) + { + switch (optc) + { + case 'i': + ignore_environment = true; + break; + case 'u': + append_unset_var (optarg); + break; + case 'v': + dev_debug = true; + break; + case '0': + opt_nul_terminate_output = true; + break; + case 'C': + newdir = optarg; + break; + case 'S': + parse_split_string (optarg, &optind, &argc, &argv); + break; + case ' ': + case '\t': + /* These are undocumented options. Attempt to detect + incorrect shebang usage with extraneous space, e.g.: + #!/usr/bin/env -i command + In which case argv[1] == "-i command". */ + error (0, 0, _("invalid option -- '%c'"), optc); + error (0, 0, _("use -[v]S to pass options in shebang lines")); + usage (EXIT_CANCELED); + + case_GETOPT_HELP_CHAR; + case_GETOPT_VERSION_CHAR (PROGRAM_NAME, AUTHORS); + default: + usage (EXIT_CANCELED); + } + } + + if (optind < argc && STREQ (argv[optind], "-")) + { + ignore_environment = true; + ++optind; + } + + if (ignore_environment) + { + devmsg ("cleaning environ\n"); + static char *dummy_environ[] = { NULL }; + environ = dummy_environ; + } + else + unset_envvars (); + + char *eq; + while (optind < argc && (eq = strchr (argv[optind], '='))) + { + devmsg ("setenv: %s\n", argv[optind]); + + if (putenv (argv[optind])) + { + *eq = '\0'; + die (EXIT_CANCELED, errno, _("cannot set %s"), + quote (argv[optind])); + } + optind++; + } + + bool program_specified = optind < argc; + + if (opt_nul_terminate_output && program_specified) + { + error (0, 0, _("cannot specify --null (-0) with command")); + usage (EXIT_CANCELED); + } + + if (newdir && ! program_specified) + { + error (0, 0, _("must specify command with --chdir (-C)")); + usage (EXIT_CANCELED); + } + + if (! program_specified) + { + /* Print the environment and exit. */ + char *const *e = environ; + while (*e) + printf ("%s%c", *e++, opt_nul_terminate_output ? '\0' : '\n'); + return EXIT_SUCCESS; + } + + if (newdir) + { + devmsg ("chdir: %s\n", quoteaf (newdir)); + + if (chdir (newdir) != 0) + die (EXIT_CANCELED, errno, _("cannot change directory to %s"), + quoteaf (newdir)); + } + + if (dev_debug) + { + devmsg ("executing: %s\n", argv[optind]); + for (int i=optind; i<argc; ++i) + devmsg (" arg[%d]= %s\n", i-optind, quote (argv[i])); + } + + execvp (argv[optind], &argv[optind]); + + int exit_status = errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE; + error (0, errno, "%s", quote (argv[optind])); + + if (exit_status == EXIT_ENOENT && strchr (argv[optind], ' ')) + error (0, 0, _("use -[v]S to pass options in shebang lines")); + + return exit_status; +} |