/* env - run a program in a modified environment Copyright (C) 1986-2020 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 . */ /* Richard Mlynarik and David MacKenzie */ #include #include #include #include #include #include #include #include "system.h" #include "die.h" #include "error.h" #include "operand2sig.h" #include "quote.h" #include "sig2str.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; /* Possible actions on each signal. */ enum SIGNAL_MODE { UNCHANGED = 0, DEFAULT, /* Set to default handler (SIG_DFL). */ DEFAULT_NOERR, /* ditto, but ignore sigaction(2) errors. */ IGNORE, /* Set to ignore (SIG_IGN). */ IGNORE_NOERR /* ditto, but ignore sigaction(2) errors. */ }; static enum SIGNAL_MODE *signals; /* Set of signals to block. */ static sigset_t block_signals; /* Set of signals to unblock. */ static sigset_t unblock_signals; /* Whether signal mask adjustment requested. */ static bool sig_mask_changed; /* Whether to list non default handling. */ static bool report_signal_handling; static char const shortopts[] = "+C:iS:u:v0 \t"; /* For long options that have no equivalent short option, use a non-character as a pseudo short option, starting with CHAR_MAX + 1. */ enum { DEFAULT_SIGNAL_OPTION = CHAR_MAX + 1, IGNORE_SIGNAL_OPTION, BLOCK_SIGNAL_OPTION, LIST_SIGNAL_HANDLING_OPTION, }; 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'}, {"default-signal", optional_argument, NULL, DEFAULT_SIGNAL_OPTION}, {"ignore-signal", optional_argument, NULL, IGNORE_SIGNAL_OPTION}, {"block-signal", optional_argument, NULL, BLOCK_SIGNAL_OPTION}, {"list-signal-handling", no_argument, NULL, LIST_SIGNAL_HANDLING_OPTION}, {"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\ "), stdout); fputs (_("\ --block-signal[=SIG] block delivery of SIG signal(s) to COMMAND\n\ "), stdout); fputs (_("\ --default-signal[=SIG] reset handling of SIG signal(s) to the default\n\ "), stdout); fputs (_("\ --ignore-signal[=SIG] set handling of SIG signals(s) to do nothing\n\ "), stdout); fputs (_("\ --list-signal-handling list non default signal handling to stderr\n\ "), stdout); fputs (_("\ -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); fputs (_("\ \n\ SIG may be a signal name like 'PIPE', or a signal number like '13'.\n\ Without SIG, all known signals are included. Multiple signals can be\n\ comma-separated.\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 */ } static void parse_signal_action_params (const char* optarg, bool set_default) { char signame[SIG2STR_MAX]; char *opt_sig; char *optarg_writable; if (! optarg) { /* without an argument, reset all signals. Some signals cannot be set to ignore or default (e.g., SIGKILL, SIGSTOP on most OSes, and SIGCONT on AIX.) - so ignore errors. */ for (int i = 1 ; i <= SIGNUM_BOUND; i++) if (sig2str (i, signame) == 0) signals[i] = set_default ? DEFAULT_NOERR : IGNORE_NOERR; return; } optarg_writable = xstrdup (optarg); opt_sig = strtok (optarg_writable, ","); while (opt_sig) { int signum = operand2sig (opt_sig, signame); /* operand2sig accepts signal 0 (EXIT) - but we reject it. */ if (signum == 0) error (0, 0, _("%s: invalid signal"), quote (opt_sig)); if (signum <= 0) usage (exit_failure); signals[signum] = set_default ? DEFAULT : IGNORE; opt_sig = strtok (NULL, ","); } free (optarg_writable); } static void reset_signal_handlers (void) { for (int i = 1; i <= SIGNUM_BOUND; i++) { struct sigaction act; if (signals[i] == UNCHANGED) continue; bool ignore_errors = (signals[i] == DEFAULT_NOERR || signals[i] == IGNORE_NOERR); bool set_to_default = (signals[i] == DEFAULT || signals[i] == DEFAULT_NOERR); int sig_err = sigaction (i, NULL, &act); if (sig_err && !ignore_errors) die (EXIT_CANCELED, errno, _("failed to get signal action for signal %d"), i); if (! sig_err) { act.sa_handler = set_to_default ? SIG_DFL : SIG_IGN; if ((sig_err = sigaction (i, &act, NULL)) && !ignore_errors) die (EXIT_CANCELED, errno, _("failed to set signal action for signal %d"), i); } if (dev_debug) { char signame[SIG2STR_MAX]; sig2str (i, signame); devmsg ("Reset signal %s (%d) to %s%s\n", signame, i, set_to_default ? "DEFAULT" : "IGNORE", sig_err ? " (failure ignored)" : ""); } } } static void parse_block_signal_params (const char* optarg, bool block) { char signame[SIG2STR_MAX]; char *opt_sig; char *optarg_writable; if (! optarg) { /* without an argument, reset all signals. */ sigfillset (block ? &block_signals : &unblock_signals); sigemptyset (block ? &unblock_signals : &block_signals); } else if (! sig_mask_changed) { /* Initialize the sets. */ sigemptyset (&block_signals); sigemptyset (&unblock_signals); } sig_mask_changed = true; if (! optarg) return; optarg_writable = xstrdup (optarg); opt_sig = strtok (optarg_writable, ","); while (opt_sig) { int signum = operand2sig (opt_sig, signame); /* operand2sig accepts signal 0 (EXIT) - but we reject it. */ if (signum == 0) error (0, 0, _("%s: invalid signal"), quote (opt_sig)); if (signum <= 0) usage (exit_failure); sigaddset (block ? &block_signals : &unblock_signals, signum); sigdelset (block ? &unblock_signals : &block_signals, signum); opt_sig = strtok (NULL, ","); } free (optarg_writable); } static void set_signal_proc_mask (void) { /* Get the existing signal mask */ sigset_t set; const char *debug_act; sigemptyset (&set); if (sigprocmask (0, NULL, &set)) die (EXIT_CANCELED, errno, _("failed to get signal process mask")); for (int i = 1; i <= SIGNUM_BOUND; i++) { if (sigismember (&block_signals, i)) { sigaddset (&set, i); debug_act = "BLOCK"; } else if (sigismember (&unblock_signals, i)) { sigdelset (&set, i); debug_act = "UNBLOCK"; } else { debug_act = NULL; } if (dev_debug && debug_act) { char signame[SIG2STR_MAX]; sig2str (i, signame); devmsg ("signal %s (%d) mask set to %s\n", signame, i, debug_act); } } if (sigprocmask (SIG_SETMASK, &set, NULL)) die (EXIT_CANCELED, errno, _("failed to set signal process mask")); } static void list_signal_handling (void) { sigset_t set; char signame[SIG2STR_MAX]; sigemptyset (&set); if (sigprocmask (0, NULL, &set)) die (EXIT_CANCELED, errno, _("failed to get signal process mask")); for (int i = 1; i <= SIGNUM_BOUND; i++) { struct sigaction act; if (sigaction (i, NULL, &act)) continue; char const* ignored = act.sa_handler == SIG_IGN ? "IGNORE" : ""; char const* blocked = sigismember (&set, i) ? "BLOCK" : ""; char const* connect = *ignored && *blocked ? "," : ""; if (! *ignored && ! *blocked) continue; sig2str (i, signame); fprintf (stderr, "%-10s (%2d): %s%s%s\n", signame, i, blocked, connect, ignored); } } static void initialize_signals (void) { signals = xmalloc ((sizeof *signals) * (SIGNUM_BOUND + 1)); for (int i = 0 ; i <= SIGNUM_BOUND; i++) signals[i] = UNCHANGED; return; } 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); initialize_signals (); 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 DEFAULT_SIGNAL_OPTION: parse_signal_action_params (optarg, true); parse_block_signal_params (optarg, false); break; case IGNORE_SIGNAL_OPTION: parse_signal_action_params (optarg, false); break; case BLOCK_SIGNAL_OPTION: parse_block_signal_params (optarg, true); break; case LIST_SIGNAL_HANDLING_OPTION: report_signal_handling = 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; } reset_signal_handlers (); if (sig_mask_changed) set_signal_proc_mask (); if (report_signal_handling) list_signal_handling (); 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