Bruno Haible via GNU coreutils General Discussion <[email protected]>
writes:
> The CI today reports a test failure of tests/tail/pipe-f.
>
> Log is attached.
>
> It looks like a consequence of the
> "head,tail: quote name in file headers appropriately"
> commit from today.
You are right. At first I thought the quoting like this was a bit
strange:
$ ./src/tail - - < /dev/null
==> 'standard input' <==
==> 'standard input' <==
However, I guess I am okay with it given that is what is already done
for errors (*):
$ ./src/tail <&-
tail: cannot fstat 'standard input': Bad file descriptor
tail: -: Bad file descriptor
My 0001 patch attached fixes that test case. I haven't pushed it yet in
case we decide that is not the desired behavior.
I noticed another issue with Pádraig's patch that I mentioned privately,
but I think is worth discussion publicly in case anyone wants to
comment. I believe that it does violate POSIX requirements. The page for
'head' says [1]:
STDOUT
The standard output shall contain designated portions of the input
files.
If multiple file operands are specified, head shall precede the
output for each with the header:
"\n==> %s <==\n", <pathname>
except that the first header written shall not include the initial
<newline>.
My interpretation of this is that "pathname" refers to an unaltered name
from the command line. Conversion between "-" and "standard input" seems
okay since it isn't really a path.
Given that the headers are mostly a visual aid, I am okay with the new
behavior. However, I propose following the existing behavior if
POSIXLY_CORRECT is defined. I have attached patch 0002 which does this.
Note that POSIX only defines the behavior of 'tail' on a single
file [2]. Given that our implementation behaves similar to 'head', I
think that it is reasonable to do the same thing there.
(*) Note that there is an unrelated issue where we attempt to close
standard input when we know it was closed, therefore the duplicate error
messages. I had a look at fixing that in 'cat' which is another simple
case that always calls fstat on standard input. However, I haven't
decided on cases like this:
$ ./src/tail - - - <&- > /dev/null
tail: cannot fstat 'standard input': Bad file descriptor
tail: cannot fstat 'standard input': Bad file descriptor
tail: cannot fstat 'standard input': Bad file descriptor
tail: -: Bad file descriptor
My current opinion is that the behavior in that case should be something
like this:
$ ./src/tail - - - <&- > /dev/null
tail: cannot fstat 'standard input': Bad file descriptor
tail: cannot fstat 'standard input': Bad file descriptor
tail: cannot fstat 'standard input': Bad file descriptor
One might be tempted to skip reading standard input for the second and
third argument. However, it feels wrong to skip a file explicitly given
via the command-line.
Collin
[1] https://pubs.opengroup.org/onlinepubs/9799919799/utilities/head.html
[2] https://pubs.opengroup.org/onlinepubs/9799919799/utilities/tail.html
>From d17f35f9568fa56f6f570db66673961fa23a89c7 Mon Sep 17 00:00:00 2001
Message-ID: <d17f35f9568fa56f6f570db66673961fa23a89c7.1779762689.git.collin.fu...@gmail.com>
From: Collin Funk <[email protected]>
Date: Mon, 25 May 2026 18:40:48 -0700
Subject: [PATCH 1/2] tests: fail: fix a test failure caused by the previous
commit
* tests/tail/pipe-f.sh: Quote 'standard input'.
---
tests/tail/pipe-f.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/tail/pipe-f.sh b/tests/tail/pipe-f.sh
index eeaa92b60..0ae9bfb22 100755
--- a/tests/tail/pipe-f.sh
+++ b/tests/tail/pipe-f.sh
@@ -29,7 +29,7 @@ echo oo > exp || framework_failure_
echo foo | timeout 10 tail -f $mode $fastpoll -c3 > out || fail=1
compare exp out || fail=1
cat <<\EOF > exp || framework_failure_
-==> standard input <==
+==> 'standard input' <==
ar
EOF
echo bar | returns_ 1 \
--
2.54.0
>From ec24edaef892661ac4bd2819477e6e40156815ed Mon Sep 17 00:00:00 2001
Message-ID: <ec24edaef892661ac4bd2819477e6e40156815ed.1779762689.git.collin.fu...@gmail.com>
In-Reply-To: <d17f35f9568fa56f6f570db66673961fa23a89c7.1779762689.git.collin.fu...@gmail.com>
References: <d17f35f9568fa56f6f570db66673961fa23a89c7.1779762689.git.collin.fu...@gmail.com>
From: Collin Funk <[email protected]>
Date: Mon, 25 May 2026 19:02:19 -0700
Subject: [PATCH 2/2] head,tail: don't quote file names in headers if
POSIXLY_CORRECT is used
* src/head.c (quote_headers): New variable.
(write_header): Use it to determine if file names should be quoted.
(main): Check POSIXLY_CORRECT if multiple files are given.
* src/tail.c (quote_headers): New variable.
(write_header): Use it to determine if file names should be quoted.
(main): Check POSIXLY_CORRECT if multiple files are given.
* tests/head/quote-headers.sh: Add a test case using POSIXLY_CORRECT.
* tests/tail/quote-headers.sh: Likewise.
---
src/head.c | 11 +++++++++--
src/tail.c | 11 +++++++++--
tests/head/quote-headers.sh | 13 +++++++++++++
tests/tail/quote-headers.sh | 15 +++++++++++++++
4 files changed, 46 insertions(+), 4 deletions(-)
diff --git a/src/head.c b/src/head.c
index 96c967d8c..a0826879b 100644
--- a/src/head.c
+++ b/src/head.c
@@ -57,6 +57,9 @@ static bool presume_input_pipe;
/* If true, print filename headers. */
static bool print_headers;
+/* If true, quote the file name in headers. */
+static bool quote_headers = true;
+
/* Character to split lines by. */
static char line_end = '\n';
@@ -180,7 +183,8 @@ write_header (char const *filename)
{
static bool first_file = true;
- printf ("%s==> %s <==\n", (first_file ? "" : "\n"), quotef (filename));
+ printf ("%s==> %s <==\n", (first_file ? "" : "\n"),
+ quote_headers ? quotef (filename) : filename);
first_file = false;
}
@@ -1062,7 +1066,10 @@ main (int argc, char **argv)
if (header_mode == always
|| (header_mode == multiple_files && optind < argc - 1))
- print_headers = true;
+ {
+ print_headers = true;
+ quote_headers = ! getenv ("POSIXLY_CORRECT");
+ }
file_list = (optind < argc
? (char const *const *) &argv[optind]
diff --git a/src/tail.c b/src/tail.c
index 2ca27e835..5a6462161 100644
--- a/src/tail.c
+++ b/src/tail.c
@@ -195,6 +195,9 @@ static bool from_start;
/* If true, print filename headers. */
static bool print_headers;
+/* If true, quote the file name in headers. */
+static bool quote_headers = true;
+
/* Character to split lines by. */
static char line_end = '\n';
@@ -461,7 +464,8 @@ write_header (char const *prettyname)
{
static bool first_file = true;
- printf ("%s==> %s <==\n", first_file ? "" : "\n", quotef (prettyname));
+ printf ("%s==> %s <==\n", first_file ? "" : "\n",
+ quote_headers ? quotef (prettyname) : prettyname);
first_file = false;
}
@@ -2442,7 +2446,10 @@ main (int argc, char **argv)
if (header_mode == always
|| (header_mode == multiple_files && n_files > 1))
- print_headers = true;
+ {
+ print_headers = true;
+ quote_headers = ! getenv ("POSIXLY_CORRECT");
+ }
xset_binary_mode (STDOUT_FILENO, O_BINARY);
diff --git a/tests/head/quote-headers.sh b/tests/head/quote-headers.sh
index 336db0e6d..e7487f358 100755
--- a/tests/head/quote-headers.sh
+++ b/tests/head/quote-headers.sh
@@ -35,4 +35,17 @@ EOF
compare exp out || fail=1
+# Check that we comply with POSIX requirements if POSIXLY_CORRECT is defined.
+POSIXLY_CORRECT=1 head -n1 "$NL" normal >out || fail=1
+
+# Avoid mistaking the line for trailing whitespace.
+echo '==> ' >exp || framework_failure_
+cat<<\EOF>>exp || framework_failure_
+ <==
+
+==> normal <==
+EOF
+
+compare exp out || fail=1
+
Exit $fail
diff --git a/tests/tail/quote-headers.sh b/tests/tail/quote-headers.sh
index 2e173a144..348dad1f8 100755
--- a/tests/tail/quote-headers.sh
+++ b/tests/tail/quote-headers.sh
@@ -35,4 +35,19 @@ EOF
compare exp out || fail=1
+# The behavior of 'tail' on multiple files is not specified by POSIX. However,
+# we treat this case similarly to 'head' where file names must be printed
+# without modification.
+POSIXLY_CORRECT=1 tail -n1 "$NL" normal >out || fail=1
+
+# Avoid mistaking the line for trailing whitespace.
+echo '==> ' >exp || framework_failure_
+cat<<\EOF>>exp || framework_failure_
+ <==
+
+==> normal <==
+EOF
+
+compare exp out || fail=1
+
Exit $fail
--
2.54.0