This new module provides a program that executes a program, canonicalizing
newlines in the standard output and/or standard error. It's useful in
test suites that are meant to run also on Windows, because it makes it
easy to deal with the necessary CRLF -> LF canonicalization there.


2025-08-04  Bruno Haible  <[email protected]>

        nlcanon: Add tests.
        * tests/test-nlcanon.sh: New file.
        * modules/nlcanon-tests: New file.
        * tests/init.sh (setup_): Adjust also top_builddir, if set.

        nlcanon: New module.
        * build-aux/nlcanon.sh.in: New file, with a function func_tmpdir taken
        from build-aux/csharpexec.sh.in.
        * modules/nlcanon: New file.

>From ef6e2fcf72e6f90296f92de437754c33832cfdc3 Mon Sep 17 00:00:00 2001
From: Bruno Haible <[email protected]>
Date: Mon, 4 Aug 2025 12:44:14 +0200
Subject: [PATCH 1/2] nlcanon: New module.

* build-aux/nlcanon.sh.in: New file, with a function func_tmpdir taken
from build-aux/csharpexec.sh.in.
* modules/nlcanon: New file.
---
 ChangeLog               |   7 +++
 build-aux/nlcanon.sh.in | 130 ++++++++++++++++++++++++++++++++++++++++
 modules/nlcanon         |  22 +++++++
 3 files changed, 159 insertions(+)
 create mode 100644 build-aux/nlcanon.sh.in
 create mode 100644 modules/nlcanon

diff --git a/ChangeLog b/ChangeLog
index 370aefef3f..ece157234f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,10 @@
+2025-08-04  Bruno Haible  <[email protected]>
+
+	nlcanon: New module.
+	* build-aux/nlcanon.sh.in: New file, with a function func_tmpdir taken
+	from build-aux/csharpexec.sh.in.
+	* modules/nlcanon: New file.
+
 2025-08-01  Collin Funk  <[email protected]>
 
 	doc: Mention the copy_file_range bug.
diff --git a/build-aux/nlcanon.sh.in b/build-aux/nlcanon.sh.in
new file mode 100644
index 0000000000..d5373f0b6c
--- /dev/null
+++ b/build-aux/nlcanon.sh.in
@@ -0,0 +1,130 @@
+#!/bin/sh
+# Execute a program, canonicalizing newlines in the standard output and/or
+# standard error.
+
+# Copyright (C) 2025 Free Software Foundation, Inc.
+# Written by Bruno Haible <[email protected]>, 2025.
+#
+# 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/>.
+
+# Usage: /bin/sh nlcanon.sh PLATFORMS STREAMS PROGRAM [ARGUMENTS]
+# where
+#   PLATFORMS is one of
+#             all             for all platforms
+#             windows-based   for Cygwin and native Windows
+#             windows         for native Windows
+#   STREAMS   is one of
+#             stdout          for standard output
+#             stderr          for standard error
+#             stdout,stderr   for both standard output and standard error
+#   PROGRAM [ARGUMENTS]   is the command to execute.
+
+# func_tmpdir
+# creates a temporary directory.
+# Sets variable
+# - tmp             pathname of freshly created temporary directory
+func_tmpdir ()
+{
+  # Use the environment variable TMPDIR, falling back to /tmp. This allows
+  # users to specify a different temporary directory, for example, if their
+  # /tmp is filled up or too small.
+  : "${TMPDIR=/tmp}"
+  {
+    # Use the mktemp program if available. If not available, hide the error
+    # message.
+    tmp=`(umask 077 && mktemp -d -q "$TMPDIR/gtXXXXXX") 2>/dev/null` &&
+    test -n "$tmp" && test -d "$tmp"
+  } ||
+  {
+    # Use a simple mkdir command. It is guaranteed to fail if the directory
+    # already exists.  $RANDOM is bash specific and expands to empty in shells
+    # other than bash, ksh and zsh.  Its use does not increase security;
+    # rather, it minimizes the probability of failure in a very cluttered /tmp
+    # directory.
+    tmp=$TMPDIR/gt$$-$RANDOM
+    (umask 077 && mkdir "$tmp")
+  } ||
+  {
+    echo "$0: cannot create a temporary directory in $TMPDIR" >&2
+    { (exit 1); exit 1; }
+  }
+}
+
+host_os='@host_os@'
+
+platforms="$1"
+streams="$2"
+shift
+shift
+
+case "$platforms" in
+  all | windows-based | windows) ;;
+  *) echo "nlcanon.sh: Invalid PLATFORMS argument" 1>&2; exit 1 ;;
+esac
+
+case "$streams" in
+  stdout | stderr | stdout,stderr | stderr,stdout ) ;;
+  *) echo "nlcanon.sh: Invalid STREAMS argument" 1>&2; exit 1 ;;
+esac
+
+if case "$platforms" in
+     all)
+       true
+       ;;
+     windows-based)
+       case "$host_os" in
+         cygwin* | mingw* | windows*) true ;;
+         *) false ;;
+       esac
+       ;;
+     windows)
+       case "$host_os" in
+         mingw* | windows*) true ;;
+         *) false ;;
+       esac
+       ;;
+   esac
+then
+
+  # We need a temporary file, to save the exit code.
+  # Since there is no portable atomic 'mktemp' command, and since the only
+  # safe non-atomic way to create a temporary file is in a temporary directory,
+  # we need a temporary directory.
+  func_tmpdir
+  func_cleanup_tmpfiles()
+  {
+    rm -rf "$tmp"
+  }
+  trap func_cleanup_tmpfiles HUP INT QUIT PIPE TERM
+  trap 'exit_status=$?; func_cleanup_tmpfiles; exit $exit_status' EXIT
+  exitcode_file="$tmp/exit"
+
+  # This is not a program. This is art. :D)
+  case "$streams" in
+    stdout)
+      { "$@"; echo "$?" > "$exitcode_file"; } | { sed -e 's/\r$//' 2>/dev/null; }
+      ;;
+    stderr)
+      { { "$@" 2>&1 1>&3; echo "$?" > "$exitcode_file"; } | { sed -e 's/\r$//' 2>/dev/null; }; } 3>&1 1>&2
+      ;;
+    *) # both
+      { { "$@" 2>&1 1>&3; echo "$?" > "$exitcode_file"; } | { sed -e 's/\r$//' 2>/dev/null; }; } 3>&1 1>&2 | { sed -e 's/\r$//' 2>/dev/null; }
+      ;;
+  esac
+  exit `cat "$exitcode_file"`
+
+else
+  # No newline canonicalization is requested.
+  exec "$@"
+fi
diff --git a/modules/nlcanon b/modules/nlcanon
new file mode 100644
index 0000000000..02190818d6
--- /dev/null
+++ b/modules/nlcanon
@@ -0,0 +1,22 @@
+Description:
+Execute a program, canonicalizing newlines in the standard output and/or
+standard error.
+
+Files:
+build-aux/nlcanon.sh.in
+
+Depends-on:
+
+configure.ac:
+AC_REQUIRE([AC_CANONICAL_HOST])
+AC_CONFIG_FILES([nlcanon.sh:build-aux/nlcanon.sh.in])
+
+Makefile.am:
+
+Include:
+
+License:
+GPLed build tool
+
+Maintainer:
+all
-- 
2.50.1

>From c7fd19908502c4c49e1d5beb73242ac96d256dbc Mon Sep 17 00:00:00 2001
From: Bruno Haible <[email protected]>
Date: Mon, 4 Aug 2025 13:23:54 +0200
Subject: [PATCH 2/2] nlcanon: Add tests.

* tests/test-nlcanon.sh: New file.
* modules/nlcanon-tests: New file.
* tests/init.sh (setup_): Adjust also top_builddir, if set.
---
 ChangeLog             |  5 ++++
 modules/nlcanon-tests | 10 +++++++
 tests/init.sh         | 10 ++++++-
 tests/test-nlcanon.sh | 67 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 91 insertions(+), 1 deletion(-)
 create mode 100644 modules/nlcanon-tests
 create mode 100755 tests/test-nlcanon.sh

diff --git a/ChangeLog b/ChangeLog
index ece157234f..867b5886a9 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,10 @@
 2025-08-04  Bruno Haible  <[email protected]>
 
+	nlcanon: Add tests.
+	* tests/test-nlcanon.sh: New file.
+	* modules/nlcanon-tests: New file.
+	* tests/init.sh (setup_): Adjust also top_builddir, if set.
+
 	nlcanon: New module.
 	* build-aux/nlcanon.sh.in: New file, with a function func_tmpdir taken
 	from build-aux/csharpexec.sh.in.
diff --git a/modules/nlcanon-tests b/modules/nlcanon-tests
new file mode 100644
index 0000000000..bfcecda38e
--- /dev/null
+++ b/modules/nlcanon-tests
@@ -0,0 +1,10 @@
+Files:
+tests/test-nlcanon.sh
+
+Depends-on:
+test-framework-sh
+
+Makefile.am:
+TESTS += test-nlcanon.sh
+TESTS_ENVIRONMENT += \
+  top_builddir='@top_builddir@'
diff --git a/tests/init.sh b/tests/init.sh
index d695020545..d3ce11dbee 100644
--- a/tests/init.sh
+++ b/tests/init.sh
@@ -428,13 +428,21 @@ setup_ ()
   test_dir_=`mktempd_ "$initial_cwd_" "$pfx_-$ME_.XXXX"` \
     || fail_ "failed to create temporary directory in $initial_cwd_"
   cd "$test_dir_" || fail_ "failed to cd to temporary directory"
-  # Set variables srcdir, builddir, for the convenience of the test.
+  # Set variables srcdir, builddir, and optionally top_builddir,
+  # for the convenience of the test.
   case $srcdir in
     /* | ?:*) ;;
     *) srcdir="../$srcdir" ;;
   esac
   builddir=".."
   export srcdir builddir
+  if test -n "$top_builddir"; then
+    case $top_builddir in
+      /* | ?:*) ;;
+      *) top_builddir="../$top_builddir" ;;
+    esac
+    export top_builddir
+  fi
 
   # As autoconf-generated configure scripts do, ensure that IFS
   # is defined initially, so that saving and restoring $IFS works.
diff --git a/tests/test-nlcanon.sh b/tests/test-nlcanon.sh
new file mode 100755
index 0000000000..ddcfc1cd9f
--- /dev/null
+++ b/tests/test-nlcanon.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+
+if test $# != 0; then
+  # Callee.
+  printf 'stdout-contents\r\n'
+  printf 'stderr-contents\r\n' 1>&2
+  exit $1
+else
+  # Unit test.
+  . "${srcdir=.}/init.sh"; path_prepend_ .
+
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout "${srcdir}/test-nlcanon.sh" 42
+  # Test the exit code.
+  test $? = 42 || Exit 11
+  # Test standard output.
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout "${srcdir}/test-nlcanon.sh" 42 | grep stdout
+  test $? = 0 || Exit 12
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout "${srcdir}/test-nlcanon.sh" 42 | grep stderr
+  test $? != 0 || Exit 13
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout "${srcdir}/test-nlcanon.sh" 42 | tr '\r' r | grep contentsr
+  test $? != 0 || Exit 14
+  # Test standard error.
+  { $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout "${srcdir}/test-nlcanon.sh" 42; } 2>&1 >/dev/null | grep stdout
+  test $? != 0 || Exit 15
+  { $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout "${srcdir}/test-nlcanon.sh" 42; } 2>&1 >/dev/null | grep stderr
+  test $? = 0 || Exit 16
+  { $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout "${srcdir}/test-nlcanon.sh" 42; } 2>&1 >/dev/null | tr '\r' r | grep contentsr
+  test $? = 0 || Exit 17
+
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stderr "${srcdir}/test-nlcanon.sh" 42
+  # Test the exit code.
+  test $? = 42 || Exit 21
+  # Test standard output.
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stderr "${srcdir}/test-nlcanon.sh" 42 | grep stdout
+  test $? = 0 || Exit 22
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stderr "${srcdir}/test-nlcanon.sh" 42 | grep stderr
+  test $? != 0 || Exit 23
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stderr "${srcdir}/test-nlcanon.sh" 42 | tr '\r' r | grep contentsr
+  test $? = 0 || Exit 24
+  # Test standard error.
+  { $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stderr "${srcdir}/test-nlcanon.sh" 42; } 2>&1 >/dev/null | grep stdout
+  test $? != 0 || Exit 25
+  { $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stderr "${srcdir}/test-nlcanon.sh" 42; } 2>&1 >/dev/null | grep stderr
+  test $? = 0 || Exit 26
+  { $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stderr "${srcdir}/test-nlcanon.sh" 42; } 2>&1 >/dev/null | tr '\r' r | grep contentsr
+  test $? != 0 || Exit 27
+
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout,stderr "${srcdir}/test-nlcanon.sh" 42
+  # Test the exit code.
+  test $? = 42 || Exit 31
+  # Test standard output.
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout,stderr "${srcdir}/test-nlcanon.sh" 42 | grep stdout
+  test $? = 0 || Exit 32
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout,stderr "${srcdir}/test-nlcanon.sh" 42 | grep stderr
+  test $? != 0 || Exit 33
+  $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout,stderr "${srcdir}/test-nlcanon.sh" 42 | tr '\r' r | grep contentsr
+  test $? != 0 || Exit 34
+  # Test standard error.
+  { $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout,stderr "${srcdir}/test-nlcanon.sh" 42; } 2>&1 >/dev/null | grep stdout
+  test $? != 0 || Exit 35
+  { $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout,stderr "${srcdir}/test-nlcanon.sh" 42; } 2>&1 >/dev/null | grep stderr
+  test $? = 0 || Exit 36
+  { $BOURNE_SHELL ${top_builddir}/nlcanon.sh all stdout,stderr "${srcdir}/test-nlcanon.sh" 42; } 2>&1 >/dev/null | tr '\r' r | grep contentsr
+  test $? != 0 || Exit 37
+
+  Exit 0
+fi
-- 
2.50.1

Reply via email to