I wasn't entirely sure if the output in 'env --debug' should be
quoted, so I left it out of this patch. Certainly it would look a bit
strange with the messages for 'env --split-string'.

-- 8< --

* 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 improvement.
---
 NEWS                   |  4 ++++
 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, 180 insertions(+), 17 deletions(-)
 create mode 100644 src/printenv.h

diff --git a/NEWS b/NEWS
index a85b0ada9..ac6184481 100644
--- a/NEWS
+++ b/NEWS
@@ -81,6 +81,10 @@ GNU coreutils NEWS                                    -*- 
outline -*-
   'df', 'du', 'ls', 'od', 'pr', and 'sort' now escape invalid arguments in 
error
   messages for options expecting an integer.
 
+  'env' and 'printenv' now quote printed environment variables in shell-escape
+  style.  This avoids printing arbitrary data to the terminal and allows the
+  output to be sourced by a POSIX shell.
+
   'install -C' will now avoid updating file metadata when the destination
   already has the appropriate ownership and permissions.
 
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index 939b0ccae..769f1930c 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -15514,6 +15514,15 @@ @node printenv invocation
 
 @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 @@ @node env invocation
 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 ea7ff7067..3433db83b 100644
--- a/src/env.c
+++ b/src/env.c
@@ -23,8 +23,10 @@
 #include <c-ctype.h>
 #include <signal.h>
 
+#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 db86cb5ea..cf13f1339 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 c2b1c69cd..fbe35d5a6 100644
--- a/src/printenv.c
+++ b/src/printenv.c
@@ -32,7 +32,9 @@
 #include <sys/types.h>
 #include <getopt.h>
 
+#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 000000000..ce12ef44a
--- /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 <https://www.gnu.org/licenses/>.  */
+
+#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 425a82724..a4cc00598 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-shell >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 f146d46b2..0cb90681e 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-shell >exp-noargs-c || framework_failure_
+tr "'" '"' <exp-args-shell >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.55.0


Reply via email to