The example patch I sent earlier was incorrect, since it quoted the
variable and its value together. This fixes that, and also makes it
trivial to get the same behavior in 'env'.
-- 8< --
* doc/coreutils.texi (printenv invocation): Use the terminalQuoted
macro.
* src/printenv.h: New file.
* src/local.mk (noinst_HEADERS): Add it.
(src_printenv_SOURCES): New variable.
* src/printenv.c: Include argmatch.h and printenv.h.
(main): Quote variable names and values when standard output is
connected to a terminal.
* tests/misc/printenv.sh: Add test cases.
* tests/misc/tty-quoting.sh: Likewise.
* NEWS: Mention the improvement.
---
NEWS | 7 +++---
doc/coreutils.texi | 2 ++
src/local.mk | 3 +++
src/printenv.c | 28 ++++++++++++++++-----
src/printenv.h | 51 +++++++++++++++++++++++++++++++++++++++
tests/misc/printenv.sh | 19 +++++++++++++++
tests/misc/tty-quoting.sh | 10 ++++++--
7 files changed, 109 insertions(+), 11 deletions(-)
create mode 100644 src/printenv.h
diff --git a/NEWS b/NEWS
index a85b0ada9..8eb87cfd3 100644
--- a/NEWS
+++ b/NEWS
@@ -84,9 +84,10 @@ GNU coreutils NEWS -*-
outline -*-
'install -C' will now avoid updating file metadata when the destination
already has the appropriate ownership and permissions.
- 'basename', 'dirname', 'du', 'readlink', and 'realpath' now quote output in
- shell-escape style when standard output is a terminal. The QUOTING_STYLE
- environment variable can be used to adjust or disable the quoting.
+ 'basename', 'dirname', 'du', 'printenv', 'readlink', and 'realpath' now quote
+ output in shell-escape style when standard output is a terminal. The
+ QUOTING_STYLE environment variable can be used to adjust or disable the
+ quoting.
'ls -m' now quotes files names containing commas when appropriate,
so users can better distinguish separating commas.
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index 939b0ccae..777110935 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -15514,6 +15514,8 @@ @node printenv invocation
@end table
+@terminalQuoted
+
@cindex exit status of @command{printenv}
Exit status:
diff --git a/src/local.mk b/src/local.mk
index db86cb5ea..2ca9b1ac7 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 \
@@ -421,6 +422,8 @@ 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_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..64223207f 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 && isatty (STDOUT_FILENO))
+ {
+ 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..8f0e58a7e
--- /dev/null
+++ b/src/printenv.h
@@ -0,0 +1,51 @@
+/* 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);
+ putchar (terminator);
+ }
+ 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/misc/printenv.sh b/tests/misc/printenv.sh
index f146d46b2..1b3b30d82 100755
--- a/tests/misc/printenv.sh
+++ b/tests/misc/printenv.sh
@@ -80,4 +80,23 @@ compare exp out || fail=1
returns_ 1 env a=b=c printenv a=b > out || fail=1
compare /dev/null out || fail=1
+cat <<\EOF >exp-noargs || framework_failure_
+a b=c d
+EOF
+cat <<\EOF >exp-args || framework_failure_
+c d
+EOF
+# QUOTING_STYLE does not affect redirected output.
+for qs in literal shell-always invalid; 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 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 out || fail=1
+ compare /dev/null err || fail=1
+done
+
Exit $fail
diff --git a/tests/misc/tty-quoting.sh b/tests/misc/tty-quoting.sh
index a0dc1d4b8..603a3fa8a 100755
--- a/tests/misc/tty-quoting.sh
+++ b/tests/misc/tty-quoting.sh
@@ -20,6 +20,7 @@
print_ver_ basename dirname du ls readlink realpath printf test
require_strace_ ioctl
+export TEST_ENV1='f oo'
touch 'b ar' || framework_failure_
ln -s 'b ar' 'f oo' || framework_failure_
@@ -44,9 +45,14 @@ run_tty_ env test -t 1 ||
run_tty_ env printf foo >printf.t &&
skip_ 'libc buffering induced a tty probe'
-for cmd in basename du dirname 'ls -w0' readlink 'realpath --relative-to=.'; do
+for cmd in printenv basename du dirname 'ls -w0' readlink \
+ 'realpath --relative-to=.'; do
test "$cmd" = 'du' && field=2 || field=1
- test "$cmd" = 'dirname' && file='f oo/.' || file='f oo'
+ case "$cmd" in
+ dirname) file='f oo/.' ;;
+ printenv) file='TEST_ENV1' ;;
+ *) file='f oo' ;;
+ esac
run_tty_ $cmd "$file" >quoted.t || fail=1
cut -f$field- quoted.t >quoted || framework_failure_
--
2.55.0