Subject: bug#47243: pr lacks -p

From: Collin Funk <[email-masked]>
Date: Tue, 29 Jul 2025 21:51:04 -0700

Paul Eggert <[email-masked]> writes:

>> +After printing each page, print an alert (bell) to standard error and
>> +wait for a newline to be read from @file{/dev/tty} before printing the
>> +next page.
>
> This sentence should start "Before" not "After", with the rest of the
> sentence changed accordingly. There is no bell printed after the last
> page.
>
> Looking at the code, it seems that this mistake has leaked into the
> code as well, in that sometimes 'pr' will print an alert and wait for
> '\n' at end of file, when there is no page to print. If I'm right,
> that needs to be fixed; pr should wait for '\n' only if it will
> actually print something afterward.

Fixing this required a bit more understanding of the code on my part,
but I think the attached patch should address it.

The existing code uses a 'pad_vertically' variable to keep track of
whether the current page has text or a header that needs to be
printed. Therefore, any time we set it to true, we must also pause. But
only once, at the start of the page.

Collin

>From 8afb79bbf294687fd4bdd3c92d1599f5021f703a Mon Sep 17 00:00:00 2001
Message-ID: <8afb[email-masked]>
From: Collin Funk <[email-masked]>
Date: Sun, 27 Jul 2025 15:00:15 -0700
Subject: [PATCH v5] pr: implement '-p' and modify '-f' conforming to POSIX

* src/pr.c (last_paused_page_number, pause_option, pause_on_first_page)
(tty_fd): New variables.
(short_options): Add '-p'.
(long_options): Add '--pause'.
(main): Add the option. Open a file descriptor to /dev/tty. Disable
pausing if POSIXLY_CORRECT is set.
(pause_maybe): New function.
(read_line, print_stored): Call it whenever pad_vertically is set to
true.
(usage): Mention the new option.
* doc/coreutils.texi (pr invocation): Document the new option. Document
the behaviors of POSIXLY_CORRECT on -p and -f.
* NEWS: Mention the new option. Mention the new behavior of 'pr -f' with
the POSIXLY_CORRECT environment variable set.
---
 NEWS               |  7 ++++
 doc/coreutils.texi | 17 ++++++++++
 src/pr.c           | 80 ++++++++++++++++++++++++++++++++++++++++++++--
 3 files changed, 101 insertions(+), 3 deletions(-)

diff --git a/NEWS b/NEWS
index 0b2be7116..3034ce718 100644
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,9 @@ GNU coreutils NEWS                                    -*- outline -*-
   'factor' is now much faster at identifying large prime numbers,
   and significantly faster on composite numbers greater than 2^128.
 
+  'pr -f' with the POSIXLY_CORRECT environment variable set will pause
+  until a newline is read from /dev/tty before printing the first page.
+
 ** Bug fixes
 
   cksum was not compilable by Apple LLVM 10.0.0 x86-64, which
@@ -64,6 +67,10 @@ GNU coreutils NEWS                                    -*- outline -*-
   Iranian locale (fa_IR) and for the Ethiopian locale (am_ET), and also
   does so more consistently for the Thailand locale (th_TH.UTF-8).
 
+  pr now supports the -p option, to pause before printing each page
+  until a newline character is read from /dev/tty, as required by POSIX.
+  The corresponding long option is --pause.
+
 
 * Noteworthy changes in release 9.7 (2025-04-09) [stable]
 
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index 40ecf3126..5df11f910 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -2726,6 +2726,11 @@ @node pr invocation
 Use a form feed instead of newlines to separate output pages.  This does
 not alter the default page length of 66 lines.
 
+@vindex POSIXLY_CORRECT
+This option will also pause until a newline is read from @file{/dev/tty}
+before printing the first page if the @env{POSIXLY_CORRECT} environment
+variable is set and standard output is associated with a terminal.
+
 @item -h @var{header}
 @itemx --header=@var{header}
 @opindex -h
@@ -2825,6 +2830,18 @@ @node pr invocation
 set with the @option{-W/-w} option.  A limited overflow may occur with
 numbered single column output (compare @option{-n} option).
 
+@item -p
+@itemx --pause
+@opindex -p
+@opindex --pause
+Before printing each page, print an alert (bell) to standard error and
+wait for a newline to be read from @file{/dev/tty} before printing the
+next page.
+
+@vindex POSIXLY_CORRECT
+This option is ignored if the @env{POSIXLY_CORRECT} environment variable
+is set and standard output is not associated with a terminal.
+
 @item -r
 @itemx --no-file-warnings
 @opindex -r
diff --git a/src/pr.c b/src/pr.c
index e7081a059..5d945910c 100644
--- a/src/pr.c
+++ b/src/pr.c
@@ -419,6 +419,7 @@ typedef struct COLUMN COLUMN;
 
 static int char_to_clump (char c);
 static bool read_line (COLUMN *p);
+static void pause_maybe (void);
 static bool print_page (void);
 static bool print_stored (COLUMN *p);
 static bool open_file (char *name, COLUMN *p);
@@ -618,6 +619,9 @@ static int files_ready_to_read = 0;
 /* Current page number.  Displayed in header. */
 static uintmax_t page_number;
 
+/* The last page number that we paused on.  */
+static uintmax_t last_paused_page_number;
+
 /* Current line number.  Displayed when -n flag is specified.
 
    When printing files in parallel (-m flag), line numbering is as follows:
@@ -711,6 +715,15 @@ static char *custom_header;
 /* (-D) Date format for the header.  */
 static char const *date_format;
 
+/* If true, pause before each page until a newline is read from /dev/tty.  */
+static bool pause_option;
+
+/* If true, pause before the first page.  */
+static bool pause_on_first_page;
+
+/* Used to read from /dev/tty if --p/--pause is in use.  */
+static int tty_fd;
+
 /* The local time zone rules, as per the TZ environment variable.  */
 static timezone_t localtz;
 
@@ -738,7 +751,7 @@ enum
 };
 
 static char const short_options[] =
-  "-0123456789D:FJN:S::TW:abcde::fh:i::l:mn::o:rs::tvw:";
+  "-0123456789D:FJN:S::TW:abcde::fh:i::l:mn::po:rs::tvw:";
 
 static struct option const long_options[] =
 {
@@ -758,6 +771,7 @@ static struct option const long_options[] =
   {"number-lines", optional_argument, nullptr, 'n'},
   {"first-line-number", required_argument, nullptr, 'N'},
   {"indent", required_argument, nullptr, 'o'},
+  {"pause", no_argument, nullptr, 'p'},
   {"no-file-warnings", no_argument, nullptr, 'r'},
   {"separator", optional_argument, nullptr, 's'},
   {"sep-string", optional_argument, nullptr, 'S'},
@@ -868,6 +882,8 @@ main (int argc, char **argv)
   idx_t n_digits = 0;
   idx_t n_alloc = 0;
 
+  bool posixly_correct = (getenv ("POSIXLY_CORRECT") != nullptr);
+
   initialize_main (&argc, &argv);
   set_program_name (argv[0]);
   setlocale (LC_ALL, "");
@@ -958,6 +974,8 @@ main (int argc, char **argv)
           untabify_input = true;
           break;
         case 'f':
+          pause_on_first_page = posixly_correct;
+          FALLTHROUGH;
         case 'F':
           use_form_feed = true;
           break;
@@ -999,6 +1017,9 @@ main (int argc, char **argv)
           chars_per_margin = getoptnum (optarg, 0,
                                         _("'-o MARGIN' invalid line offset"));
           break;
+        case 'p':
+          pause_option = true;
+          break;
         case 'r':
           ignore_failed_opens = true;
           break;
@@ -1061,7 +1082,7 @@ main (int argc, char **argv)
     }
 
   if (! date_format)
-    date_format = (getenv ("POSIXLY_CORRECT") && !hard_locale (LC_TIME)
+    date_format = (posixly_correct && !hard_locale (LC_TIME)
                    ? "%b %e %H:%M %Y"
                    : "%Y-%m-%d %H:%M");
 
@@ -1079,6 +1100,22 @@ main (int argc, char **argv)
     error (EXIT_FAILURE, 0,
            _("cannot specify both printing across and printing in parallel"));
 
+  /* POSIX states that the pausing behavior of -f and -p should occur only if
+     standard output is associated with a terminal.  Only behave this way is
+     POSIXLY_CORRECT is set since the GNU Coding Standards discourages changing
+     program behavior based on output device type.  */
+  if ((pause_option || pause_on_first_page) && posixly_correct
+      && ! isatty (STDOUT_FILENO))
+    pause_option = pause_on_first_page = false;
+
+  if (pause_option || pause_on_first_page)
+    {
+      tty_fd = open ("/dev/tty", O_RDONLY);
+      if (tty_fd < 0)
+        error (EXIT_FAILURE, errno, _("cannot open %s for reading"),
+               quoteaf ("/dev/tty"));
+    }
+
 /* Translate some old short options to new/long options.
    To meet downward compatibility with other UNIX pr utilities
    and some POSIX specifications. */
@@ -1640,6 +1677,36 @@ print_files (int number_of_files, char **av)
     ;
 }
 
+/* Pause until reading a newline from /dev/tty if needed.  This function should
+   not be called until we know that we have text or a header to print for a
+   page.  This means it must be called when pad_vertically is set to true.  */
+
+static void
+pause_maybe (void)
+{
+  if ((pause_option || (pause_on_first_page && page_number == 1))
+      && last_paused_page_number < page_number)
+    {
+      last_paused_page_number = page_number;
+      if (pause_option
+          && (fputc ('\a', stderr) == EOF || fflush (stderr) != 0))
+        write_error ();
+      while (true)
+        {
+          char ch;
+          ssize_t bytes_read = read (tty_fd, &ch, sizeof ch);
+          if (bytes_read < 0)
+            error (EXIT_FAILURE, errno, _("error reading %s"),
+                   quoteaf ("/dev/tty"));
+          /* Just exit if the user presses Ctrl-D.  */
+          if (bytes_read == 0)
+            return;
+          if (ch == '\n')
+            break;
+        }
+    }
+}
+
 /* Initialize header information.
    If DESC is non-negative, it is a file descriptor open to
    FILENAME for reading.  */
@@ -2452,6 +2519,7 @@ read_line (COLUMN *p)
       if (print_a_header && !storing_columns)
         {
           pad_vertically = true;
+          pause_maybe ();
           print_header ();
         }
       else if (keep_FF)
@@ -2476,6 +2544,7 @@ read_line (COLUMN *p)
   if (p->char_func != store_char)
     {
       pad_vertically = true;
+      pause_maybe ();
 
       if (print_a_header && !storing_columns)
         print_header ();
@@ -2586,7 +2655,10 @@ print_stored (COLUMN *p)
   pad_vertically = true;
 
   if (print_a_header)
-    print_header ();
+    {
+      pause_maybe ();
+      print_header ();
+    }
 
   if (p->status == FF_FOUND)
     {
@@ -2824,6 +2896,8 @@ Paginate or columnate FILE(s) for printing.\n\
   -o, --indent=MARGIN\n\
                     offset each line with MARGIN (zero) spaces, do not\n\
                     affect -w or -W, MARGIN will be added to PAGE_WIDTH\n\
+  -p, --pause       pause at the beginning of each page until a newline\n\
+                    is read from /dev/tty.\n\
   -r, --no-file-warnings\n\
                     omit warning when a file cannot be opened\n\
 "), stdout);
-- 
2.50.1