From 328322ca641a9eb2b500ab8f9e3cb68c41e1aa30 Mon Sep 17 00:00:00 2001 From: Collin Funk Date: Sun, 2 Aug 2026 21:31:07 -0700 Subject: [PATCH] printenv,env: quote problematic variables * doc/coreutils.texi (printenvAlwaysQuoted): New macro. (printenv invocation): Use it. (env invocation): Use it. * src/printenv.h: New file. * src/local.mk (noinst_HEADERS): Add it. (src_env_SOURCES): Likewise. Group with src_printenv_SOURCES. (src_printenv_SOURCES): New variable. * src/printenv.c: Include argmatch.h and printenv.h. (main): Quote printed variable names and values. * src/env.c: Include argmatch.h and printenv.h. (main): Quote printed variable names and values. * tests/misc/printenv.sh: Add test cases. * tests/env/env.sh: Likewise. Adjust invalid UTF-8 test cases. * NEWS: Mention the change in behavior. --- NEWS | 5 +++++ doc/coreutils.texi | 11 ++++++++++ src/env.c | 25 ++++++++++++++++++---- src/local.mk | 5 ++++- src/printenv.c | 28 ++++++++++++++++++------ src/printenv.h | 48 ++++++++++++++++++++++++++++++++++++++++++ tests/env/env.sh | 42 ++++++++++++++++++++++++++++++------ tests/misc/printenv.sh | 34 ++++++++++++++++++++++++++++++ 8 files changed, 181 insertions(+), 17 deletions(-) create mode 100644 src/printenv.h diff --git a/NEWS b/NEWS index a85b0ada9d..5dea7828c7 100644 --- a/NEWS +++ b/NEWS @@ -67,6 +67,11 @@ GNU coreutils NEWS -*- outline -*- ** Changes in behavior + 'env' and 'printenv' now quote printed environment variables honoring the + QUOTING_STYLE environment variable, defaulting to shell-escape style. This + avoids printing arbitrary data to the terminal and allows the output to be + sourced by a POSIX shell. + 'ls' -w,--width no longer includes '\n' in the width of a line. I.e., the width or $COLUMNS is interpreted to be an _inclusive_ maximum. diff --git a/doc/coreutils.texi b/doc/coreutils.texi index 939b0ccae6..769f1930ce 100644 --- a/doc/coreutils.texi +++ b/doc/coreutils.texi @@ -15514,6 +15514,15 @@ The program accepts the following option. Also see @ref{Common options}. @end table +@macro printenvAlwaysQuoted +The printed environment variables and their values are quoted using the +@samp{shell-escape} style. The environment variable +@env{QUOTING_STYLE} specifies the quoting style. Valid quoting styles +are: +@quotingStyles +@end macro +@printenvAlwaysQuoted + @cindex exit status of @command{printenv} Exit status: @@ -17477,6 +17486,8 @@ If no command name is specified following the environment specifications, the resulting environment is printed. This is like specifying the @command{printenv} program. +@printenvAlwaysQuoted + For some examples, suppose the environment passed to @command{env} contains @samp{LOGNAME=rms}, @samp{EDITOR=emacs}, and @samp{PATH=.:/gnubin:/hacks}: diff --git a/src/env.c b/src/env.c index ea7ff70679..3433db83b1 100644 --- a/src/env.c +++ b/src/env.c @@ -23,8 +23,10 @@ #include #include +#include "argmatch.h" /* argmatch($QUOTING_STYLE). */ #include "system.h" #include "operand2sig.h" +#include "printenv.h" #include "quote.h" #include "sig2str.h" @@ -842,6 +844,22 @@ main (int argc, char **argv) ++optind; } + bool quote_output = false; + + /* Get the value from QUOTING_STYLE before unsetting environment + variables. */ + if (!opt_nul_terminate_output) + { + int qs = getenv_quoting_style (); + if (qs < 0) + qs = shell_escape_quoting_style; + if (qs != literal_quoting_style) + { + set_quoting_style (NULL, qs); + quote_output = true; + } + } + if (ignore_environment) { devmsg ("cleaning environ\n"); @@ -887,12 +905,11 @@ main (int argc, char **argv) if (! program_specified) { + char const terminator = opt_nul_terminate_output ? '\0' : '\n'; + /* Print the environment and exit. */ for (char *const *e = environ; *e; ++e) - { - fputs (*e, stdout); - putchar (opt_nul_terminate_output ? '\0' : '\n'); - } + print_envvar (*e, terminator, quote_output); return EXIT_SUCCESS; } diff --git a/src/local.mk b/src/local.mk index db86cb5ea7..cf13f13390 100644 --- a/src/local.mk +++ b/src/local.mk @@ -58,6 +58,7 @@ noinst_HEADERS = \ src/ls.h \ src/octhexdigits.h \ src/operand2sig.h \ + src/printenv.h \ src/prog-fprintf.h \ src/remove.h \ src/set-fields.h \ @@ -398,7 +399,6 @@ src_cp_SOURCES = src/cp.c $(copy_sources) $(selinux_sources) src_date_SOURCES = src/date.c src/show-date.c src_dir_SOURCES = src/ls.c src/ls-dir.c src_du_SOURCES = src/du.c src/show-date.c -src_env_SOURCES = src/env.c src/operand2sig.c src_vdir_SOURCES = src/ls.c src/ls-vdir.c src_id_SOURCES = src/id.c src/group-list.c src_groups_SOURCES = src/groups.c src/group-list.c @@ -421,6 +421,9 @@ src_rmdir_SOURCES = src/rmdir.c src/prog-fprintf.c src_mkfifo_SOURCES = src/mkfifo.c $(selinux_sources) src_mknod_SOURCES = src/mknod.c $(selinux_sources) +src_env_SOURCES = src/env.c src/operand2sig.c src/printenv.h +src_printenv_SOURCES = src/printenv.c src/printenv.h + src_df_SOURCES = src/df.c src/find-mount-point.c src_stat_SOURCES = src/stat.c src/find-mount-point.c diff --git a/src/printenv.c b/src/printenv.c index c2b1c69cda..fbe35d5a67 100644 --- a/src/printenv.c +++ b/src/printenv.c @@ -32,7 +32,9 @@ #include #include +#include "argmatch.h" /* argmatch($QUOTING_STYLE). */ #include "system.h" +#include "printenv.h" /* Exit status for syntax errors, etc. */ enum { PRINTENV_FAILURE = 2 }; @@ -107,14 +109,27 @@ main (int argc, char **argv) } } + bool quote_output = false; + + if (!opt_nul_terminate_output) + { + int qs = getenv_quoting_style (); + if (qs < 0) + qs = shell_escape_quoting_style; + if (qs != literal_quoting_style) + { + set_quoting_style (NULL, qs); + quote_output = true; + } + } + bool ok; + char const terminator = opt_nul_terminate_output ? '\0' : '\n'; + if (optind >= argc) { for (char **env = environ; *env != NULL; ++env) - { - fputs (*env, stdout); - putchar (opt_nul_terminate_output ? '\0' : '\n'); - } + print_envvar (*env, terminator, quote_output); ok = true; } else @@ -137,8 +152,9 @@ main (int argc, char **argv) { if (*ep == '=' && *ap == '\0') { - fputs (ep + 1, stdout); - putchar (opt_nul_terminate_output ? '\0' : '\n'); + char const *val = ep + 1; + fputs (quote_output ? quoteN (val) : val, stdout); + putchar (terminator); matched = true; break; } diff --git a/src/printenv.h b/src/printenv.h new file mode 100644 index 0000000000..ce12ef44a0 --- /dev/null +++ b/src/printenv.h @@ -0,0 +1,48 @@ +/* Common definitions for 'printenv' and 'env' + Copyright (C) 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 . */ + +#ifndef PRINTENV_H +# define PRINTENV_H 1 + +static inline void +print_envvar (char const *entry, char terminator, bool quoted) +{ + if (! quoted) + fputs (entry, stdout); + else + { + idx_t const entry_len = strlen (entry); + char const *equal = memchr (entry, '=', entry_len); + + /* If the parent process manipulates ENVIRON directly, it is possible + that an entry does not contain an equal sign. */ + idx_t const var_len = equal ? equal - entry : entry_len; + fputs (quoteN_mem (entry, var_len), stdout); + + if (equal) + { + putchar ('='); + char const *val = equal + 1; + idx_t const val_len = entry_len - (val - entry); + /* Prefer "VAR=" over "VAR=''". */ + if (0 < val_len) + fputs (quoteN_mem (val, val_len), stdout); + } + } + putchar (terminator); +} + +#endif diff --git a/tests/env/env.sh b/tests/env/env.sh index 425a827243..a4cc005982 100755 --- a/tests/env/env.sh +++ b/tests/env/env.sh @@ -91,12 +91,19 @@ EOF compare exp out || fail=1 # env shouldn't care what encoding name or value is -for nv in 'NON_UTF8_TEST=\240' 'NON_UTF8_TEST\240=1'; do - env $(printf "$nv") env > all || fail=1 - grep '^NON_UTF8_TEST' all | LC_ALL=C sort > out || framework_failure_ - printf "$nv\\n" > exp || framework_failure_ - compare exp out || fail=1 -done +cat <<\EOF >exp || framework_failure_ +NON_UTF8_TEST=''$'\240' +EOF +env $(printf 'NON_UTF8_TEST=\240') env > all || fail=1 +grep '^NON_UTF8_TEST' all | LC_ALL=C sort > out || framework_failure_ +compare exp out || fail=1 + +cat <<\EOF >exp || framework_failure_ +'NON_UTF8_TEST'$'\240'=1 +EOF +env $(printf 'NON_UTF8_TEST\240=1') env > all || fail=1 +grep "^'NON_UTF8_TEST" all | LC_ALL=C sort > out || framework_failure_ +compare exp out || fail=1 # PATH modifications affect exec. mkdir unlikely_name || framework_failure_ @@ -186,4 +193,27 @@ EOF compare err_exp err || fail=1 done +# QUOTING_STYLE affects redirected output. +cat <<\EOF >exp-noargs-literal || framework_failure_ +a b=c d +EOF +cat <<\EOF >exp-noargs-shell || framework_failure_ +'a b'='c d' +EOF +tr "'" '"' exp-noargs-c || framework_failure_ +for qs in literal shell c; do + env -i PATH="$PATH" QUOTING_STYLE=$qs 'a b'='c d' \ + env >out-t 2>err || fail=1 + grep -vE 'QUOTING_STYLE|PATH' out-t > out || framework_failure_ + compare exp-noargs-$qs out || fail=1 + compare /dev/null err || fail=1 +done + +# Check the behavior with an invalid value for QUOTING_STYLE. +printf 'env: ignoring invalid value of environment variable %s\n' \ + "QUOTING_STYLE: 'invalid'" >exp || framework_failure_ +env QUOTING_STYLE=invalid env >out 2>err || fail=1 +grep '^QUOTING_STYLE=invalid$' out || fail=1 +compare exp err || fail=1 + Exit $fail diff --git a/tests/misc/printenv.sh b/tests/misc/printenv.sh index f146d46b22..0cb90681e9 100755 --- a/tests/misc/printenv.sh +++ b/tests/misc/printenv.sh @@ -80,4 +80,38 @@ compare exp out || fail=1 returns_ 1 env a=b=c printenv a=b > out || fail=1 compare /dev/null out || fail=1 +# QUOTING_STYLE affects redirected output. +cat <<\EOF >exp-noargs-literal || framework_failure_ +a b=c d +EOF +cat <<\EOF >exp-args-literal || framework_failure_ +c d +EOF +cat <<\EOF >exp-noargs-shell || framework_failure_ +'a b'='c d' +EOF +cat <<\EOF >exp-args-shell || framework_failure_ +'c d' +EOF +tr "'" '"' exp-noargs-c || framework_failure_ +tr "'" '"' exp-args-c || framework_failure_ +for qs in literal shell c; do + env -i PATH="$PATH" QUOTING_STYLE=$qs 'a b'='c d' \ + printenv >out-t 2>err || fail=1 + grep -vE 'QUOTING_STYLE|PATH' out-t > out || framework_failure_ + compare exp-noargs-$qs out || fail=1 + compare /dev/null err || fail=1 + env -i PATH="$PATH" QUOTING_STYLE=$qs 'a b'='c d' \ + printenv 'a b' >out 2>err || fail=1 + compare exp-args-$qs out || fail=1 + compare /dev/null err || fail=1 +done + +# Check the behavior with an invalid value for QUOTING_STYLE. +printf 'printenv: ignoring invalid value of environment variable %s\n' \ + "QUOTING_STYLE: 'invalid'" >exp || framework_failure_ +env QUOTING_STYLE=invalid printenv >out 2>err || fail=1 +grep '^QUOTING_STYLE=invalid$' out || fail=1 +compare exp err || fail=1 + Exit $fail -- 2.47.3