From 34350eb7a495cb07ab33a8b23ffe4a896bb09241 Mon Sep 17 00:00:00 2001
From: Nikolaos Papaspyrou <nikolaos@google.com>
Date: Wed, 9 Feb 2022 01:31:41 +0100
Subject: [PATCH] tee: add --remove-cr option

This option is helpful when one uses tee to store the output of a
program to a file and the program uses carriage return characters
to visualize its progress. E.g., to use the option status=progress
for dd and also send the program's output (only without progress
information) to a log file:

    dd if=input of=output status=progress |& tee --remove-cr LOG

In this way, progress information is shown in the standard output
but is not stored to the log file.

* src/tee.c: implement the new option.
* tests/misc/tee.sh: add test the new option.
---
 src/tee.c         | 87 +++++++++++++++++++++++++++++++++++++++++++----
 tests/misc/tee.sh | 19 +++++++++++
 2 files changed, 99 insertions(+), 7 deletions(-)

diff --git a/src/tee.c b/src/tee.c
index 971b768c5..de0de1331 100644
--- a/src/tee.c
+++ b/src/tee.c
@@ -39,12 +39,20 @@
 
 static bool tee_files (int nfiles, char **files);
 
+static size_t wrap_fwrite (bool process_cr, const void *ptr, size_t size,
+                           size_t nmemb, FILE *stream);
+
+static size_t wrap_fwrite_finalize (bool process_cr, FILE *stream);
+
 /* If true, append to output files rather than truncating them. */
 static bool append;
 
 /* If true, ignore interrupts. */
 static bool ignore_interrupts;
 
+/* If true, process and remove CR characters. */
+static bool remove_cr;
+
 enum output_error
   {
     output_error_sigpipe,      /* traditional behavior, sigpipe enabled.  */
@@ -61,6 +69,7 @@ static struct option const long_options[] =
   {"append", no_argument, NULL, 'a'},
   {"ignore-interrupts", no_argument, NULL, 'i'},
   {"output-error", optional_argument, NULL, 'p'},
+  {"remove-cr", no_argument, NULL, 'r'},
   {GETOPT_HELP_OPTION_DECL},
   {GETOPT_VERSION_OPTION_DECL},
   {NULL, 0, NULL, 0}
@@ -89,6 +98,7 @@ usage (int status)
 Copy standard input to each FILE, and also to standard output.\n\
 \n\
   -a, --append              append to the given FILEs, do not overwrite\n\
+  -r, --remove-cr           process and remove CR characters\n\
   -i, --ignore-interrupts   ignore interrupt signals\n\
 "), stdout);
       fputs (_("\
@@ -130,8 +140,9 @@ main (int argc, char **argv)
 
   append = false;
   ignore_interrupts = false;
+  remove_cr = false;
 
-  while ((optc = getopt_long (argc, argv, "aip", long_options, NULL)) != -1)
+  while ((optc = getopt_long (argc, argv, "airp", long_options, NULL)) != -1)
     {
       switch (optc)
         {
@@ -143,6 +154,10 @@ main (int argc, char **argv)
           ignore_interrupts = true;
           break;
 
+        case 'r':
+          remove_cr = true;
+          break;
+
         case 'p':
           if (optarg)
             output_error = XARGMATCH ("--output-error", optarg,
@@ -238,7 +253,8 @@ tee_files (int nfiles, char **files)
          Standard output is the first one.  */
       for (i = 0; i <= nfiles; i++)
         if (descriptors[i]
-            && fwrite (buffer, bytes_read, 1, descriptors[i]) != 1)
+            && wrap_fwrite (remove_cr && i > 0, buffer, bytes_read, 1,
+                            descriptors[i]) != 1)
           {
             int w_errno = errno;
             bool fail = errno != EPIPE || (output_error == output_error_exit
@@ -266,13 +282,70 @@ tee_files (int nfiles, char **files)
 
   /* Close the files, but not standard output.  */
   for (i = 1; i <= nfiles; i++)
-    if (descriptors[i] && fclose (descriptors[i]) != 0)
-      {
-        error (0, errno, "%s", quotef (files[i]));
-        ok = false;
-      }
+    {
+      if (descriptors[i]
+          && wrap_fwrite_finalize (remove_cr, descriptors[i]) != 1)
+        {
+          int w_errno = errno;
+          bool fail = errno != EPIPE || (output_error == output_error_exit
+                                        || output_error == output_error_warn);
+          if (descriptors[i] == stdout)
+            clearerr (stdout); /* Avoid redundant close_stdout diagnostic.  */
+          if (fail)
+            {
+              error (output_error == output_error_exit
+                     || output_error == output_error_exit_nopipe,
+                     w_errno, "%s", quotef (files[i]));
+            }
+          if (fail)
+            ok = false;
+        }
+      if (descriptors[i] && fclose (descriptors[i]) != 0)
+        {
+          error (0, errno, "%s", quotef (files[i]));
+          ok = false;
+        }
+    }
 
   free (descriptors);
 
   return ok;
 }
+
+static char process_buffer[BUFSIZ];
+static int curr = 0;
+
+static size_t
+wrap_fwrite (bool process_cr, const void *ptr, size_t size, size_t nmemb,
+             FILE *stream)
+{
+  if (!process_cr)
+    return fwrite (ptr, size, nmemb, stream);
+  size_t remaining = size * nmemb;
+  char const *p = ptr;
+  size_t ok = 1;
+  while (remaining-- > 0)
+    {
+      char c = *p++;
+      if (c == '\r')
+        {
+          curr = 0;
+          continue;
+        }
+      process_buffer[curr++] = c;
+      if (c != '\n' && curr < BUFSIZ)
+        continue;
+      if (fwrite (process_buffer, curr, 1, stream) != 1)
+        ok = 0;
+      curr = 0;
+    }
+  return ok;
+}
+
+static size_t
+wrap_fwrite_finalize (bool process_cr, FILE *stream)
+{
+  if (process_cr && curr > 0)
+    return fwrite (process_buffer, curr, 1, stream);
+  return 1;
+}
diff --git a/tests/misc/tee.sh b/tests/misc/tee.sh
index 0bd91b6cb..46130a73b 100755
--- a/tests/misc/tee.sh
+++ b/tests/misc/tee.sh
@@ -105,5 +105,24 @@ read_fifo
 yes | timeout 10 tee --output-error=exit-nopipe 2>err >fifo || fail=1
 test $(wc -l < err) = 0 || { cat err; fail=1; }
 
+# Ensure tee correctly removes lines ending with CR
+printf "This line stays\nProgress 1\rProgress 2\rProgress 3\rDone\n" > sample
+printf "This line stays\nDone\n" > expected
+for n in 0 1 2; do
+  files=$(seq $n)
+  rm -f $files
+  tee --remove-cr $files <sample >out || fail=1
+  compare sample out || fail=1
+  for f in $files; do
+    compare expected $f || fail=1
+  done
+  rm -f $files
+  tee -r $files <sample >out || fail=1
+  compare sample out || fail=1
+  for f in $files; do
+    compare expected $f || fail=1
+  done
+done
+
 wait
 Exit $fail
-- 
2.35.0.263.gb82422642f-goog

