1
0
mirror of https://git.savannah.gnu.org/git/coreutils.git synced 2026-10-24 02:08:57 +03:00
Files
coreutils/src/env.c
Pádraig Brady f799b2f48a env,printenv: support --quoting-style to control quoting
Quoting is currently only enabled by default when writing to terminals,
but it is usually desired to constrain the output format for processing.
E.g. for processing by a shell you can now:

  env --quoting-style=shell-escape | grep ^FOO

We use an option rather than reyling on $QUOTING_STYLE so
that $QUOTING_STYLE is restricted to be user config for terminals
and does not impact redirected processing.
Also it avoids an extra getenv() call if not outputting to tty.

* src/env.c (main): Parse --quoting-style and use that for the output
format for all outputs.
* src/printenv.c (main): Likewise.
* doc/coreutils.texi (env invocation): Describe --quoting-style.
(printenv invocation): Likewise.
* tests/env/env.sh: Reinstate testing of various output modes.
* tests/misc/printenv.sh: Likewise.
* NEWS: Mention the new feature.  Also mention the change from
the previous commit not done there so that builders would have
a clean patch against the previous release.
2026-09-17 08:52:12 +01:00

1266 lines
35 KiB
C

/* env - run a program in a modified environment
Copyright (C) 1986-2026 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 <argz.h>
#include <getopt.h>
#include <c-ctype.h>
#include <signal.h>
#include "argmatch.h" /* argmatch($QUOTING_STYLE). */
#include "hash.h"
#include "hashcode-mem.h"
#include "system.h"
#include "operand2sig.h"
#include "printenv.h"
#include "quote.h"
#include "read-file.h"
#include "sig2str.h"
#include "xbinary-io.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 char const **usvars;
static idx_t usvars_alloc;
static idx_t usvars_used;
/* Storage for entries read by --env0-from. */
static char *env0_from_buffer;
/* An environment vector managed directly to preserve raw entries. */
struct env_vector
{
char **variable;
idx_t used;
idx_t allocated;
};
/* Annotate the output with extra info to aid the user. */
static bool dev_debug;
#define vmsg devmsg
/* Buffer and length of extracted envvars in -S strings. */
static char *varname;
static idx_t vnlen;
/* On mingw, winbase.h defines this to zero. Undefine it so we can use it
in the enum. */
#undef IGNORE
/* 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;
/* The isspace characters in the C locale. */
#define C_ISSPACE_CHARS " \t\n\v\f\r"
static char const shortopts[] = "+a:C:iS:u:v0" C_ISSPACE_CHARS;
/* For long options that have no equivalent short option, use a
non-character as a pseudo short option, starting with CHAR_MAX + 1. */
enum
{
ENV0_FROM_OPTION = CHAR_MAX + 1,
DEFAULT_SIGNAL_OPTION,
IGNORE_SIGNAL_OPTION,
BLOCK_SIGNAL_OPTION,
LIST_SIGNAL_HANDLING_OPTION,
QUOTING_STYLE_OPTION,
};
static struct option const longopts[] =
{
{"argv0", required_argument, NULL, 'a'},
{"ignore-environment", no_argument, NULL, 'i'},
{"env0-from", required_argument, NULL, ENV0_FROM_OPTION},
{"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'},
{"quoting-style", required_argument, NULL, QUOTING_STYLE_OPTION},
{"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 ();
oputs (_("\
-a, --argv0=ARG\n\
pass ARG as the zeroth argument of COMMAND\n\
"));
oputs (_("\
-i, --ignore-environment\n\
start with an empty environment\n\
"));
oputs (_("\
--env0-from=FILE\n\
read NUL-delimited environment entries from FILE\n\
"));
oputs (_("\
-0, --null\n\
end each output line with NUL, not newline\n\
"));
oputs (_("\
-u, --unset=NAME\n\
remove variable from the environment\n\
"));
oputs (_("\
-C, --chdir=DIR\n\
change working directory to DIR\n\
"));
oputs (_("\
--quoting-style=WORD\n\
use quoting style WORD for names and values:\n\
literal, locale, shell, shell-always,\n\
shell-escape, shell-escape-always, c, escape\n\
"));
oputs (_("\
-S, --split-string=S\n\
process and split S into separate arguments;\n\
used to pass multiple arguments on shebang lines\n\
"));
oputs (_("\
--block-signal[=SIG]\n\
block delivery of SIG signal(s) to COMMAND\n\
"));
oputs (_("\
--default-signal[=SIG]\n\
reset handling of SIG signal(s) to the default\n\
"));
oputs (_("\
--ignore-signal[=SIG]\n\
set handling of SIG signal(s) to do nothing\n\
"));
oputs (_("\
--list-signal-handling\n\
list non default signal handling to standard error\n\
"));
oputs (_("\
-v, --debug\n\
print verbose information for each processing step\n\
"));
oputs (HELP_OPTION_DESCRIPTION);
oputs (VERSION_OPTION_DESCRIPTION);
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. An empty SIG argument is a no-op.\n\
"), stdout);
emit_exec_status (PROGRAM_NAME);
emit_ancillary_info (PROGRAM_NAME);
}
exit (status);
}
static void
append_unset_var (char const *var)
{
if (usvars_used == usvars_alloc)
usvars = xpalloc (usvars, &usvars_alloc, 1, -1, sizeof *usvars);
usvars[usvars_used++] = var;
}
static void
unset_envvars (void)
{
for (idx_t i = 0; i < usvars_used; ++i)
{
vmsg (_("unsetting: %s\n"), quote (usvars[i]));
if (unsetenv (usvars[i]))
error (EXIT_CANCELED, errno, _("cannot unset %s"),
quote (usvars[i]));
}
}
/* Return true if ENTRY is an assignment whose name is NAME. */
static bool
entry_has_name (char const *entry, char const *name, idx_t name_length)
{
char const *eq = strchr (entry, '=');
return (eq && eq - entry == name_length
&& memeq (entry, name, name_length));
}
#if ! defined _WIN32 || defined __CYGWIN__
/* Hash an environment vector slot by the name in its assignment. */
static size_t
env_vector_slot_hash (void const *x, size_t table_size)
{
char *const *slot = x;
char const *entry = *slot;
char const *eq = strchr (entry, '=');
return hash_pjw_bare (entry, eq - entry) % table_size;
}
static bool
env_vector_slot_compare (void const *x, void const *y)
{
char const *a = * (char *const *) x;
char const *b = * (char *const *) y;
char const *b_eq = strchr (b, '=');
return entry_has_name (a, b, b_eq - b);
}
#endif
/* Remove the requested variables from ENV,
directly, preserving other raw entries. */
static void
unset_envvars_in_vector (struct env_vector *env)
{
for (idx_t i = 0; i < usvars_used; ++i)
{
char const *name = usvars[i];
vmsg (_("unsetting: %s\n"), quote (name));
if (! *name || strchr (name, '='))
error (EXIT_CANCELED, EINVAL, _("cannot unset %s"), quoteaf (name));
idx_t name_length = strlen (name);
idx_t dest = 0;
for (idx_t source = 0; source < env->used; ++source)
if (! entry_has_name (env->variable[source], name, name_length))
env->variable[dest++] = env->variable[source];
env->used = dest;
env->variable[dest] = NULL;
}
}
/* Add ASSIGNMENT to the environment. EQ points to its '=' byte. */
static void
set_envvar (char *assignment, char *eq)
{
vmsg (_("setting: %s\n"), quote (assignment));
if (putenv (assignment))
{
*eq = '\0';
error (EXIT_CANCELED, errno, _("cannot set %s"), quote (assignment));
}
}
/* Add ASSIGNMENT to ENV directly, preserving other raw entries.
EQ points to its '=' byte. Replace the first existing assignment
with the same name, as putenv does, or append ASSIGNMENT if none. */
static void
set_envvar_in_vector (struct env_vector *env, char *assignment, char *eq)
{
vmsg (_("setting: %s\n"), quote (assignment));
idx_t name_length = eq - assignment;
for (idx_t i = 0; i < env->used; ++i)
if (entry_has_name (env->variable[i], assignment, name_length))
{
env->variable[i] = assignment;
return;
}
if (env->used + 1 == env->allocated)
{
env->variable = xpalloc (env->variable, &env->allocated, 1, -1,
sizeof *env->variable);
environ = env->variable;
}
env->variable[env->used++] = assignment;
env->variable[env->used] = NULL;
}
/* Read the NUL-delimited environment entries in FILE. */
static size_t
read_env_file (char const *file)
{
size_t size;
if (streq (file, "-"))
{
xset_binary_mode (STDIN_FILENO, O_BINARY);
env0_from_buffer = fread_file (stdin, RF_BINARY, &size);
}
else
env0_from_buffer = read_file (file, RF_BINARY, &size);
if (! env0_from_buffer)
error (EXIT_CANCELED, errno, _("cannot read %s"), quoteaf (file));
if (size && env0_from_buffer[size - 1] != '\0')
error (EXIT_CANCELED, 0, _("%s: file must end with a NUL byte"),
quotef (file));
return size;
}
/* Merge the NUL-delimited entries in FILE into the environment.
Return true if ENV now owns the environment vector. */
static bool
set_envvars_from_file (struct env_vector *env, char const *file)
{
size_t size = read_env_file (file);
#if defined _WIN32 && ! defined __CYGWIN__
(void) env;
/* On native Windows, _putenv keeps _environ, _wenviron, and the process
environment block synchronized. Replacing environ directly would
conflict with its ownership of those data structures. */
char *assignment = NULL;
while ((assignment = argz_next (env0_from_buffer, size, assignment)))
{
char *eq = strchr (assignment, '=');
if (! eq)
error (EXIT_CANCELED, 0,
_("invalid variable specification %s in %s"),
quoteaf_n (0, assignment), quoteaf_n (1, file));
set_envvar (assignment, eq);
}
return false;
#else
/* Construct and index the merged vector, rather than making putenv
repeatedly scan the growing environment for every assignment. */
idx_t file_count = argz_count (env0_from_buffer, size);
if (file_count == 0)
return false;
idx_t inherited_count = 0;
while (environ[inherited_count])
++inherited_count;
if (IDX_MAX - inherited_count <= file_count)
xalloc_die ();
idx_t max_count = inherited_count + file_count;
char **merged = xnmalloc (max_count + 1, sizeof *merged);
memcpy (merged, environ, inherited_count * sizeof *merged);
merged[inherited_count] = NULL;
Hash_table *slot_table
= hash_initialize (max_count, NULL, env_vector_slot_hash,
env_vector_slot_compare, NULL);
if (! slot_table)
xalloc_die ();
/* Index only the first occurrence of each inherited name, matching
putenv's replacement of the first such entry. */
for (idx_t i = 0; i < inherited_count; ++i)
{
char *eq = strchr (merged[i], '=');
if (! eq)
continue;
if (! hash_insert (slot_table, &merged[i]))
xalloc_die ();
}
idx_t merged_count = inherited_count;
char *assignment = NULL;
while ((assignment = argz_next (env0_from_buffer, size, assignment)))
{
char *eq = strchr (assignment, '=');
if (! eq)
{
vmsg (_("adding: %s\n"), quote (assignment));
merged[merged_count++] = assignment;
}
else
{
vmsg (_("setting: %s\n"), quote (assignment));
char **slot = hash_lookup (slot_table, &assignment);
if (slot)
*slot = assignment;
else
{
merged[merged_count] = assignment;
if (! hash_insert (slot_table, &merged[merged_count]))
xalloc_die ();
++merged_count;
}
}
}
merged[merged_count] = NULL;
env->variable = merged;
env->used = merged_count;
env->allocated = max_count + 1;
environ = merged;
hash_free (slot_table);
return true;
#endif
}
/* Replace the environment with the NUL-delimited entries in FILE,
preserving the entries byte-for-byte and in their original order. */
static void
set_raw_environment_from_file (struct env_vector *env,
char const *file)
{
size_t size = read_env_file (file);
idx_t entry_count = argz_count (env0_from_buffer, size);
env->allocated = entry_count + 1;
env->variable = xnmalloc (env->allocated, sizeof *env->variable);
argz_extract (env0_from_buffer, size, env->variable);
env->used = entry_count;
environ = env->variable;
}
/* 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. */
ATTRIBUTE_PURE
static char const *
scan_varname (char const *str)
{
if (str[1] == '{' && (c_isalpha (str[2]) || str[2] == '_'))
{
char const *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 (char const *str)
{
char const *p = scan_varname (str);
if (!p)
return NULL;
/* -2 and +2 (below) account for the '${' prefix. */
idx_t i = p - str - 2;
if (i >= vnlen)
{
free (varname);
varname = xpalloc (NULL, &vnlen, i + 1 - vnlen, -1, sizeof *varname);
}
memcpy (varname, str + 2, i);
varname[i] = 0;
return varname;
}
/* Temporary buffer used by --split-string processing. */
struct splitbuf
{
/* Buffer address, arg count, and half the number of elements in the buffer.
ARGC and ARGV are as in 'main', and ARGC + 1 <= HALF_ALLOC so
that the upper half of ARGV can be used for string contents.
This may waste up to half the space but keeps the code simple,
which is better for this rarely-used but security-sensitive code.
ARGV[0] is not initialized; that is the caller's responsibility
after finalization.
During assembly, ARGV[I] (where 0 < I < ARGC) contains the offset
of the Ith string (relative to ARGV + HALF_ALLOC), so that
reallocating ARGV does not change the validity of its contents.
The integer offset is cast to char * during assembly, and is
converted to a true char * pointer on finalization.
During assembly, ARGV[ARGC] contains the offset of the first
unused string byte (relative to ARGV + HALF_ALLOC). */
char **argv;
int argc;
idx_t half_alloc;
/* The number of extra argv slots to keep room for. */
int extra_argc;
/* Whether processing should act as if the most recent character
seen was a separator. */
bool sep;
};
/* Expand SS so that it has at least one more argv slot and at least
one more string byte. */
static void
splitbuf_grow (struct splitbuf *ss)
{
idx_t old_half_alloc = ss->half_alloc;
idx_t string_bytes = (intptr_t) ss->argv[ss->argc];
ss->argv = xpalloc (ss->argv, &ss->half_alloc, 1,
MIN (INT_MAX, IDX_MAX), 2 * sizeof *ss->argv);
memmove (ss->argv + ss->half_alloc, ss->argv + old_half_alloc, string_bytes);
}
/* In SS, append C to the last string. */
static void
splitbuf_append_byte (struct splitbuf *ss, char c)
{
idx_t string_bytes = (intptr_t) ss->argv[ss->argc];
if (ss->half_alloc * sizeof *ss->argv <= string_bytes)
splitbuf_grow (ss);
((char *) (ss->argv + ss->half_alloc))[string_bytes] = c;
ss->argv[ss->argc] = (char *) (intptr_t) {string_bytes + 1};
}
/* If SS's most recent character was a separator, finish off its
previous argument and start a new one. */
static void
check_start_new_arg (struct splitbuf *ss)
{
if (ss->sep)
{
splitbuf_append_byte (ss, '\0');
int argc = ss->argc;
if (ss->half_alloc <= argc + ss->extra_argc + 1)
splitbuf_grow (ss);
ss->argv[argc + 1] = ss->argv[argc];
ss->argc = argc + 1;
ss->sep = false;
}
}
/* All additions to SS have been made. Convert its offsets to pointers,
and return the resulting argument vector. */
static char **
splitbuf_finishup (struct splitbuf *ss)
{
int argc = ss->argc;
char **argv = ss->argv;
char *stringbase = (char *) (ss->argv + ss->half_alloc);
for (int i = 1; i < argc; i++)
argv[i] = stringbase + (intptr_t) argv[i];
return argv;
}
/* Return a newly-allocated argv-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').
Store into *argc the number of arguments found (plus 1 for
the program name).
Example:
int argc;
char **argv = build_argv ("A=B uname -k', 3, &argc);
Results in:
argc = 4
argv[0] = [not initialized]
argv[1] = "A=B"
argv[2] = "uname"
argv[3] = "-k"
argv[4,5,6,7] = [allocated due to extra_argc + 1, but not initialized]
To free allocated memory:
free (argv);
However, 'env' does not free since it's about to exec or exit anyway
and the complexity of keeping track of the storage that may have been
allocated via multiple calls to build_argv is not worth the hassle. */
static char **
build_argv (char const *str, int extra_argc, int *argc)
{
bool dq = false, sq = false;
struct splitbuf ss;
ss.argv = xnmalloc (extra_argc + 2, 2 * sizeof *ss.argv);
ss.argc = 1;
ss.half_alloc = extra_argc + 2;
ss.extra_argc = extra_argc;
ss.sep = true;
ss.argv[ss.argc] = NULL;
/* 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 (&ss);
++str;
continue;
case '"':
if (sq)
break;
dq = !dq;
check_start_new_arg (&ss);
++str;
continue;
case ' ': case '\t': case '\n': case '\v': case '\f': case '\r':
/* Start a new argument if outside quotes. */
if (sq || dq)
break;
ss.sep = true;
str += strspn (str, C_ISSPACE_CHARS);
continue;
case '#':
if (!ss.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;
switch (newc)
{
case '"': case '#': case '$': case '\'': case '\\':
/* Pass escaped character as-is. */
break;
case '_':
if (!dq)
{
++str; /* '\_' outside double-quotes is arg separator. */
ss.sep = true;
continue;
}
newc = ' '; /* '\_' inside double-quotes is space. */
break;
case 'c':
if (dq)
error (EXIT_CANCELED, 0,
_("'\\c' must not appear in double-quoted -S string"));
goto eos; /* '\c' terminates the string. */
case 'f': newc = '\f'; break;
case 'n': newc = '\n'; break;
case 'r': newc = '\r'; break;
case 't': newc = '\t'; break;
case 'v': newc = '\v'; break;
case '\0':
error (EXIT_CANCELED, 0,
_("invalid backslash at end of string in -S"));
default:
error (EXIT_CANCELED, 0,
_("invalid sequence '\\%c' in -S"), newc);
}
break;
case '$':
/* ${VARNAME} are not expanded inside single-quotes. */
if (sq)
break;
/* Store the ${VARNAME} value. */
{
char *n = extract_varname (str);
if (!n)
error (EXIT_CANCELED, 0,
_("only ${VARNAME} expansion is supported, error at: %s"),
str);
char *v = getenv (n);
if (v)
{
check_start_new_arg (&ss);
vmsg (_("expanding ${%s} into %s\n"), n, quote (v));
for (; *v; v++)
splitbuf_append_byte (&ss, *v);
}
else
vmsg (_("replacing ${%s} with empty string\n"), n);
str = strchr (str, '}') + 1;
continue;
}
}
check_start_new_arg (&ss);
splitbuf_append_byte (&ss, newc);
++str;
}
if (dq || sq)
error (EXIT_CANCELED, 0, _("no terminating quote in -S string"));
eos:
splitbuf_append_byte (&ss, '\0');
*argc = ss.argc;
return splitbuf_finishup (&ss);
}
/* 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"
argv[4] = NULL
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"
argv[6] = NULL
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 (char const *str, int *orig_optind,
int *orig_argc, char ***orig_argv)
{
int extra_argc = *orig_argc - *orig_optind, newargc;
char **newargv = build_argv (str, extra_argc, &newargc);
/* Restore argv[0] - the 'env' executable name. */
*newargv = (*orig_argv)[0];
/* Print parsed arguments. */
if (dev_debug && 1 < newargc)
{
vmsg (_("split -S: %s\n"), quote (str));
vmsg (_(" into: %s\n"), quote (newargv[1]));
for (int i = 2; i < newargc; i++)
vmsg (_(" & %s\n"), quote (newargv[i]));
}
/* Add remaining arguments and terminating null from the original
command line. */
memcpy (newargv + newargc, *orig_argv + *orig_optind,
(extra_argc + 1) * sizeof *newargv);
/* Set new values for original getopt variables. */
*orig_argc = newargc + extra_argc;
*orig_argv = newargv;
*orig_optind = 0; /* Tell getopt to restart from first argument. */
}
static void
parse_signal_action_params (char const *arg, bool set_default)
{
if (! arg)
{
/* 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++)
signals[i] = set_default ? DEFAULT_NOERR : IGNORE_NOERR;
return;
}
char *optarg_writable = xstrdup (arg);
char *opt_sig = strtok (optarg_writable, ",");
while (opt_sig)
{
int signum = operand2sig (opt_sig);
/* 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)
error (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;
sig_err = sigaction (i, &act, NULL);
if (sig_err && !ignore_errors)
error (EXIT_CANCELED, errno,
_("failed to set signal action for signal %d"), i);
}
if (dev_debug)
{
char signame[SIG2STR_MAX];
if (sig2str (i, signame) != 0)
snprintf (signame, sizeof signame, "SIG%d", i);
vmsg (_("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 (char const *arg, bool block)
{
if (! arg)
{
/* 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 (! arg)
return;
char *optarg_writable = xstrdup (arg);
char *opt_sig = strtok (optarg_writable, ",");
while (opt_sig)
{
int signum = operand2sig (opt_sig);
/* 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);
if (sigaddset (block ? &block_signals : &unblock_signals, signum) == -1)
{
if (block)
error (EXIT_CANCELED, errno,
_("failed to block signal %d"), signum);
/* else diagnosed in parse_signal_action_params(). */
}
else
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;
sigemptyset (&set);
if (sigprocmask (0, NULL, &set))
error (EXIT_CANCELED, errno, _("failed to get signal process mask"));
for (int i = 1; i <= SIGNUM_BOUND; i++)
{
char const *debug_act = NULL;
if (sigismember (&block_signals, i))
{
sigaddset (&set, i);
debug_act = "BLOCK";
}
else if (sigismember (&unblock_signals, i))
{
sigdelset (&set, i);
debug_act = "UNBLOCK";
}
if (dev_debug && debug_act)
{
char signame[SIG2STR_MAX];
if (sig2str (i, signame) != 0)
snprintf (signame, sizeof signame, "SIG%d", i);
vmsg (_("signal %s (%d) mask set to %s\n"),
signame, i, debug_act);
}
}
if (sigprocmask (SIG_SETMASK, &set, NULL))
error (EXIT_CANCELED, errno, _("failed to set signal process mask"));
}
static void
list_signal_handling (void)
{
sigset_t set;
sigemptyset (&set);
if (sigprocmask (0, NULL, &set))
error (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;
char signame[SIG2STR_MAX];
if (sig2str (i, signame) != 0)
snprintf (signame, sizeof signame, "SIG%d", i);
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;
}
int
main (int argc, char **argv)
{
bool ignore_environment = false;
bool opt_nul_terminate_output = false;
int quoting_style_opt = -1;
char const *newdir = NULL;
char const *env0_from_file = NULL;
char *argv0 = NULL;
struct env_vector env_vector;
#ifdef lint
memset (&env_vector, 0, sizeof env_vector);
char **initial_environ = environ;
#endif
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 ();
int optc;
while ((optc = getopt_long (argc, argv, shortopts, longopts, NULL)) != -1)
{
switch (optc)
{
case 'a':
argv0 = optarg;
break;
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;
quoting_style_opt = literal_quoting_style;
break;
case ENV0_FROM_OPTION:
env0_from_file = optarg;
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 QUOTING_STYLE_OPTION:
quoting_style_opt = XARGMATCH ("--quoting-style", optarg,
quoting_style_args,
quoting_style_vals);
break;
case 'C':
newdir = optarg;
break;
case 'S':
parse_split_string (optarg, &optind, &argc, &argv);
break;
case ' ': case '\t': case '\n': case '\v': case '\f': case '\r':
/* 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". */
{
char c = optc;
error (0, 0, _("invalid option -- %s"), quote_mem (&c, 1));
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;
}
bool quote_output = false;
int qs = quoting_style_opt;
if (qs < 0 && isatty (STDOUT_FILENO))
{
/* Get the value from $QUOTING_STYLE before unsetting env vars. */
qs = getenv_quoting_style ();
if (qs < 0)
qs = shell_escape_quoting_style;
}
if (0 <= qs && qs != literal_quoting_style)
{
set_quoting_style (NULL, qs);
quote_output = true;
}
bool env_vector_active = false;
if (ignore_environment && env0_from_file)
{
vmsg (_("clearing environment\n"));
set_raw_environment_from_file (&env_vector, env0_from_file);
env_vector_active = true;
}
else if (ignore_environment)
{
vmsg (_("clearing environment\n"));
#if defined _WIN32 && ! defined __CYGWIN__
static char *dummy_environ[] = { NULL };
environ = dummy_environ;
#else
env_vector.variable = xnmalloc (1, sizeof *env_vector.variable);
env_vector.variable[0] = NULL;
env_vector.used = 0;
env_vector.allocated = 1;
environ = env_vector.variable;
env_vector_active = true;
#endif
}
else if (env0_from_file)
env_vector_active = set_envvars_from_file (&env_vector, env0_from_file);
if (! ignore_environment || env0_from_file)
{
if (env_vector_active)
unset_envvars_in_vector (&env_vector);
else
unset_envvars ();
}
char *eq;
while (optind < argc && (eq = strchr (argv[optind], '=')))
{
if (env_vector_active)
set_envvar_in_vector (&env_vector, argv[optind], eq);
else
set_envvar (argv[optind], eq);
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 (argv0 && ! program_specified)
{
error (0, 0, _("must specify command with --argv0 (-a)"));
usage (EXIT_CANCELED);
}
if (! program_specified)
{
char const terminator = opt_nul_terminate_output ? '\0' : '\n';
if (opt_nul_terminate_output)
xset_binary_mode (STDOUT_FILENO, O_BINARY);
/* Print the environment and exit. */
for (char *const *e = environ; *e; ++e)
print_envvar (*e, terminator, quote_output);
#ifdef lint
if (env_vector_active)
{
environ = initial_environ;
free (env_vector.variable);
free (env0_from_buffer);
}
#endif
return EXIT_SUCCESS;
}
reset_signal_handlers ();
if (sig_mask_changed)
set_signal_proc_mask ();
if (report_signal_handling)
list_signal_handling ();
if (newdir)
{
vmsg (_("change dir: %s\n"), quote (newdir));
if (chdir (newdir) != 0)
error (EXIT_CANCELED, errno, _("cannot change directory to %s"),
quoteaf (newdir));
}
char *program = argv[optind];
if (argv0)
{
vmsg ("argv0: %s\n", quote (argv0));
argv[optind] = argv0;
}
if (dev_debug)
{
vmsg (_("executing: %s\n"), quote (program));
for (int i=optind; i<argc; ++i)
vmsg (" arg[%d]= %s\n", i-optind, quote (argv[i]));
}
execvp (program, &argv[optind]);
int exit_status = errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE;
error (0, errno, "%s", quote (program));
if (exit_status == EXIT_ENOENT && strpbrk (program, C_ISSPACE_CHARS))
error (0, 0, _("use -[v]S to pass options in shebang lines"));
main_exit (exit_status);
}