You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@daffodil.apache.org by GitBox <gi...@apache.org> on 2022/11/04 21:30:24 UTC

[GitHub] [daffodil] tuxji commented on a diff in pull request #865: Refactor integration tests for clarity and speed

tuxji commented on code in PR #865:
URL: https://github.com/apache/daffodil/pull/865#discussion_r1014370923


##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to
+   * true. Otherwise a new process is spawned.
+   *
+   * @param args arguments to pass to the CLI. This should not include the
+   *   daffodil binary
+   * @param classpaths sequence of paths to add to the classpath. If non-empty,
+   *   runs the CLI in a new process instead of a thread and will likely decrease
+   *   performance
+   * @param fork if true, forces the the CLI in a new process
+   * @param timeout how long to wait, in seconds, for the CLI to exit after the
+   *   testFunc has returned. Also how long to wait for individual expect
+   *   operations in the CLITester
+   * @param debug if true, prints arguments and classpath information to
+   *   stdout. Also echos all CLITester input and output to stdout.
+   * @param testFunc function to call to send input to the CLI and validate
+   *   output from CLI stdout/stderr.
+   * @param expectedExitCode the expected exit code of the CLI. In the actual
+   *   exit code does not match

Review Comment:
   Second sentence doesn't make sense grammatically, please delete or revise.  Also please decide if param descriptions should end with a period or end without a period and end all of them the same way.



##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to
+   * true. Otherwise a new process is spawned.
+   *
+   * @param args arguments to pass to the CLI. This should not include the
+   *   daffodil binary
+   * @param classpaths sequence of paths to add to the classpath. If non-empty,
+   *   runs the CLI in a new process instead of a thread and will likely decrease
+   *   performance
+   * @param fork if true, forces the the CLI in a new process
+   * @param timeout how long to wait, in seconds, for the CLI to exit after the
+   *   testFunc has returned. Also how long to wait for individual expect
+   *   operations in the CLITester
+   * @param debug if true, prints arguments and classpath information to
+   *   stdout. Also echos all CLITester input and output to stdout.
+   * @param testFunc function to call to send input to the CLI and validate
+   *   output from CLI stdout/stderr.
+   * @param expectedExitCode the expected exit code of the CLI. In the actual
+   *   exit code does not match
+   *
+   * @throws AssertionError if the actual exit code does not match the expected exit code
+   * @throws ExpectIOException if the an CLITester expect validation operation fails
+   */
+  def runCLI
+    (args: Array[String], classpaths: Seq[Path] = Seq(), fork: Boolean = false, timeout: Int = 10, debug: Boolean = false)
+    (testFunc: (CLITester) => Unit)
+    (expectedExitCode: ExitCode.Value): Unit = {
+
+    val (toIn, fromOut, fromErr, threadOrProc: Either[CLIThread, Process]) =
+      if (classpaths.nonEmpty || fork) {
+        // spawn a new process to run Daffodil, needed if a custom classpath is
+        // defined or if the caller explicitly wants to fork
+        val processBuilder = new ProcessBuilder()
+
+        if (classpaths.nonEmpty) {
+          val classpath = classpaths.mkString(File.pathSeparator)
+          if (debug) System.out.println(s"DAFFODIL_CLASSPATH=$classpath")
+          processBuilder.environment().put("DAFFODIL_CLASSPATH", classpath)
+        }
+
+        val cmd = daffodilBinPath.toString +: args
+        if (debug) System.out.println(cmd.mkString(" "))
+        processBuilder.command(cmd.toList.asJava)
+
+        val process = processBuilder.start()
+
+        val toIn = process.getOutputStream()
+        val fromOut = process.getInputStream()
+        val fromErr = process.getErrorStream()
+        (toIn, fromOut, fromErr, Right(process))
+      } else {
+        // create a new thread for the CLI test to run, using piped
+        // input/output streams to connected the thread and the CLItester
+        val in = new PipedInputStream()
+        val toIn = new PipedOutputStream(in)
+
+        val out = new PipedOutputStream()
+        val fromOut = new PipedInputStream(out)
+
+        val err = new PipedOutputStream()
+        val fromErr = new PipedInputStream(err)
+
+        if (debug) System.out.println("daffodil " + args.mkString(" "))
+
+        val thread = new CLIThread(args, in, out, err)
+        thread.start()
+        (toIn, fromOut, fromErr, Left(thread))
+      }
+
+    val eb = new ExpectBuilder()
+    eb.withOutput(toIn)
+    eb.withInputs(fromOut, fromErr)
+    eb.withInputFilters(replaceInString("\r\n", "\n"))
+    eb.withTimeout(timeout, TimeUnit.SECONDS)
+    eb.withExceptionOnFailure()
+    if (debug) {
+      eb.withEchoOutput(System.out)
+      eb.withEchoInput(System.out)
+    }
+    val expect = eb.build()
+    val tester = new CLITester(expect, toIn)
+
+    try {
+      testFunc(tester)
+    } finally {
+      threadOrProc match {
+        case Left(thread) => thread.join(timeout * 1000)
+        case Right(process) => process.waitFor(timeout, TimeUnit.SECONDS)
+      }
+      expect.close()
+      toIn.close()
+      fromOut.close()
+      fromErr.close()
     }
-  }
 
-  def makeMultipleCmds(cmds: Array[String]): String = {
-    if (isWindows) {
-      cmds.mkString(" & ")
-    } else {
-      cmds.mkString("; ")
+    val actualExitCode = threadOrProc match {
+      case Left(thread) => thread.exitCode
+      case Right(process) => ExitCode(process.exitValue)
     }
+    assertEquals("Incorrect exit code,", expectedExitCode, actualExitCode)
   }
 
-  def md5sum(blob_path: String): String = {
-    if (isWindows) {
-      String.format("certutil -hashfile %s MD5", blob_path)
-    } else {
-      String.format("md5sum %s", blob_path)
+  /**
+   * A class to run the CLI in a thread instead of a new process, given the
+   * arguments to use (excluded the daffodil binary) and streams to use for

Review Comment:
   excluded -> excluding



##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to
+   * true. Otherwise a new process is spawned.
+   *
+   * @param args arguments to pass to the CLI. This should not include the
+   *   daffodil binary
+   * @param classpaths sequence of paths to add to the classpath. If non-empty,
+   *   runs the CLI in a new process instead of a thread and will likely decrease
+   *   performance
+   * @param fork if true, forces the the CLI in a new process
+   * @param timeout how long to wait, in seconds, for the CLI to exit after the
+   *   testFunc has returned. Also how long to wait for individual expect
+   *   operations in the CLITester
+   * @param debug if true, prints arguments and classpath information to
+   *   stdout. Also echos all CLITester input and output to stdout.
+   * @param testFunc function to call to send input to the CLI and validate
+   *   output from CLI stdout/stderr.
+   * @param expectedExitCode the expected exit code of the CLI. In the actual
+   *   exit code does not match
+   *
+   * @throws AssertionError if the actual exit code does not match the expected exit code
+   * @throws ExpectIOException if the an CLITester expect validation operation fails
+   */
+  def runCLI
+    (args: Array[String], classpaths: Seq[Path] = Seq(), fork: Boolean = false, timeout: Int = 10, debug: Boolean = false)
+    (testFunc: (CLITester) => Unit)
+    (expectedExitCode: ExitCode.Value): Unit = {
+
+    val (toIn, fromOut, fromErr, threadOrProc: Either[CLIThread, Process]) =
+      if (classpaths.nonEmpty || fork) {
+        // spawn a new process to run Daffodil, needed if a custom classpath is
+        // defined or if the caller explicitly wants to fork
+        val processBuilder = new ProcessBuilder()
+
+        if (classpaths.nonEmpty) {
+          val classpath = classpaths.mkString(File.pathSeparator)
+          if (debug) System.out.println(s"DAFFODIL_CLASSPATH=$classpath")
+          processBuilder.environment().put("DAFFODIL_CLASSPATH", classpath)
+        }
+
+        val cmd = daffodilBinPath.toString +: args
+        if (debug) System.out.println(cmd.mkString(" "))
+        processBuilder.command(cmd.toList.asJava)
+
+        val process = processBuilder.start()
+
+        val toIn = process.getOutputStream()
+        val fromOut = process.getInputStream()
+        val fromErr = process.getErrorStream()
+        (toIn, fromOut, fromErr, Right(process))
+      } else {
+        // create a new thread for the CLI test to run, using piped
+        // input/output streams to connected the thread and the CLItester
+        val in = new PipedInputStream()
+        val toIn = new PipedOutputStream(in)
+
+        val out = new PipedOutputStream()
+        val fromOut = new PipedInputStream(out)
+
+        val err = new PipedOutputStream()
+        val fromErr = new PipedInputStream(err)
+
+        if (debug) System.out.println("daffodil " + args.mkString(" "))
+
+        val thread = new CLIThread(args, in, out, err)
+        thread.start()
+        (toIn, fromOut, fromErr, Left(thread))
+      }
+
+    val eb = new ExpectBuilder()
+    eb.withOutput(toIn)
+    eb.withInputs(fromOut, fromErr)
+    eb.withInputFilters(replaceInString("\r\n", "\n"))
+    eb.withTimeout(timeout, TimeUnit.SECONDS)
+    eb.withExceptionOnFailure()
+    if (debug) {
+      eb.withEchoOutput(System.out)
+      eb.withEchoInput(System.out)
+    }
+    val expect = eb.build()
+    val tester = new CLITester(expect, toIn)
+
+    try {
+      testFunc(tester)
+    } finally {
+      threadOrProc match {
+        case Left(thread) => thread.join(timeout * 1000)
+        case Right(process) => process.waitFor(timeout, TimeUnit.SECONDS)
+      }
+      expect.close()
+      toIn.close()
+      fromOut.close()
+      fromErr.close()
     }
-  }
 
-  def makeMultipleCmds(cmds: Array[String]): String = {
-    if (isWindows) {
-      cmds.mkString(" & ")
-    } else {
-      cmds.mkString("; ")
+    val actualExitCode = threadOrProc match {
+      case Left(thread) => thread.exitCode
+      case Right(process) => ExitCode(process.exitValue)
     }
+    assertEquals("Incorrect exit code,", expectedExitCode, actualExitCode)
   }
 
-  def md5sum(blob_path: String): String = {
-    if (isWindows) {
-      String.format("certutil -hashfile %s MD5", blob_path)
-    } else {
-      String.format("md5sum %s", blob_path)
+  /**
+   * A class to run the CLI in a thread instead of a new process, given the
+   * arguments to use (excluded the daffodil binary) and streams to use for
+   * stdin/out/err.
+   */
+  private class CLIThread(args: Array[String], in: InputStream, out: OutputStream, err: OutputStream) extends Thread {
+    var exitCode = ExitCode.Failure
+
+    override def run(): Unit = {
+      val psOut = new PrintStream(out)
+      val psErr = new PrintStream(err)
+
+      // configure the CLI and log4j to use our custom streams, nothing should
+      // not actually use stdin/stdout/stderr
+      Main.setInputOutput(in, psOut, psErr)
+      configureLog4j(psErr)
+
+      exitCode = Main.run(args)
     }
-  }
 
-  def rmdir(path: String): String = {
-    if (Util.isWindows)
-      String.format("rmdir /Q /S %s", path)
-    else
-      String.format("rm -rf %s", path)
+    /**
+     * By default log4j outputs to stderr. This changes that so it writes to a
+     * provided PrintStream which is connected to the CLITester, allowing tests
+     * to expect contain written by log4j. This also defines the same pattern
+     * used by the CLI--that must be defined here because log4j configuration
+     * is normally given using a config file specified in the CLI wrapper
+     * script, which this does not use since it calls Main.run directly
+     */
+    private def configureLog4j(ps: PrintStream): Unit = {
+      val config = new AbstractConfiguration(null, ConfigurationSource.NULL_SOURCE) {
+        override def doConfigure(): Unit = {
+          val appenderName = "DaffodilCli"
+
+          val layout = PatternLayout.newBuilder()
+            .withPattern("[%p{lowerCase=true}] %m%n")
+            .withConfiguration(this)
+            .build()
+
+          val appenderBuilder: OutputStreamAppender.Builder[_] = OutputStreamAppender.newBuilder()
+          appenderBuilder.setName(appenderName)
+          appenderBuilder.setLayout(layout)
+          appenderBuilder.setTarget(ps)
+          appenderBuilder.setConfiguration(this)
+          val appender = appenderBuilder.build()
+
+          val rootLogger = getRootLogger()
+          rootLogger.setLevel(Level.WARN);
+          rootLogger.addAppender(appender, null, null)
+        }
+      }
+      Configurator.reconfigure(config)
+    }
   }
 
-  def cat(str: String): String = {
-    if (isWindows) {
-      "type " + str
-    } else {
-      "cat " + str
+  /**
+   * Wrapper around Expect to make integration tests less verbose. It also
+   * supports closing the mimicked stdin input stream (via the closeInput()
+   * function or the inputDone parameter to the send*() functions), which is
+   * sometimes needed since Daffodil may need to receive an EOF before it can
+   * finish parsing.
+   */
+  private class CLITester(expect: Expect, toIn: OutputStream) {
+
+    /**
+     * Close stdin, triggering an EOF.
+     */
+    def closeInput(): Unit = { toIn.close() }
+
+    /**
+     * Write a string to stdin. This does not incluede trailing newline. If
+     * inputDone is true, close stdin afterwards.
+     */
+    def send(string: String, inputDone: Boolean = false): Unit = {
+      expect.send(string)
+      if (inputDone) closeInput()
     }
-  }
 
-  def newTempFile(filePrefix: String, fileSuffix: String, optFileContents: Option[String] = None): File = {
-    val inputFile = File.createTempFile(filePrefix, fileSuffix)
-    inputFile.deleteOnExit
-    if (optFileContents.nonEmpty) {
-      val contents = optFileContents.get
-      val pw = new PrintWriter(inputFile)
-      pw.write(contents)
-      pw.close
+    /**
+     * Write a string to stdin with a trailing newline. If inputDone is true,
+     * close stdin afterwards.
+     */
+    def sendLine(string: String, inputDone: Boolean = false): Unit = {
+      expect.sendLine(string)
+      if (inputDone) closeInput()
     }
-    inputFile
-  }
 
-  def expectExitCode(expectedExitCode: ExitCode.Value, shell: Expect): Unit = {
-    val expectedCode = expectedExitCode.id
+    /**
+     * Write an entire byte array to stdin. If inputDone is true, close stdin
+     * afterwards.
+     */
+    def sendBytes(bytes: Array[Byte], inputDone: Boolean = false): Unit = {
+      expect.sendBytes(bytes)
+      if (inputDone) closeInput()
+    }
 
-    val keyWord = "EXITCODE:"
+    /**
+     * Write a file to stdin. If inputDone is true, close stdin afterwards.
+     */
+    def sendFile(path: Path, inputDone: Boolean = false): Unit = {
+      val chunkSize = 8192
+      val buffer = new Array[Byte](chunkSize)
+      val stream = Files.newInputStream(path)
+      var read = 0
+      while ({read = stream.read(buffer); read} > 0) {
+        if (read == chunkSize) {
+          expect.sendBytes(buffer)
+        } else {
+          // The expect.sendBytes function does not have parameters to send a
+          // subset of an array, it just sends the whole array. So we need to
+          // trim it down to the actual read size and send that
+          val smaller = new Array[Byte](read)
+          buffer.copyToArray(smaller, 0, read)
+          expect.sendBytes(smaller)
+        }
+      }
+      if (inputDone) closeInput()
+    }
 
-    //Escaped characters ^| for windows and \\! for linux makes the echo outputs different text than the command,
-    //That way the expect function can tell the difference.
-    val exitCodeCmd = "echo " + keyWord + (if (Util.isWindows) "^|%errorlevel%" else "\\!$?")
-    val exitCodeExpectation = keyWord + (if (Util.isWindows) "|" else "!")
+    def expect(matcher: Matcher[_]): Result = expect.expect(matcher)
+    def expect(string: String): Result = expect.expect(contains(string))
 
-    shell.sendLine(exitCodeCmd)
-    shell.expect(contains(exitCodeExpectation))
+    def expectErr(matcher: Matcher[_]): Result = expect.expectIn(1, matcher)
+    def expectErr(string: String): Result = expect.expectIn(1, contains(string))
+  }
 
-    val sExitCode = shell.expect(contains("\n")).getBefore().trim()
-    val actualInt = Integer.parseInt(sExitCode)
+  /**
+   * Escapes a string that is expected to be a json string
+   */
+  def jsonEscape(string: String): String = {
+    val chars = JsonStringEncoder.getInstance().quoteAsString(string)
+    new String(chars)
+  }
 
-    if (actualInt != expectedCode) {
-      val expectedExitCodeName = expectedExitCode.toString
-      val actualExitCodeName = ExitCode.values.find { _.id == actualInt }.map { _.toString }.getOrElse("Unknown")
-      val failMessage = "Exit code %s expected (%s), but got %s (%s) instead.".format(
-        expectedCode, expectedExitCodeName, actualInt, actualExitCodeName)
-      fail(failMessage)
+  /**
+   * This "args" string interpoloator makes it easy to create an Array[String]
+   * used for CLI arguments. Only spaces in the "format" string are split on.
+   * Spaces in an expressions in the format string are not split. For example
+   *
+   *   args"parse -s $schema $input".split(" ")
+   *
+   * Becomes someething like this:
+   *
+   *   Array("parse", "-s", "path/to/schema.dfdl.xsd", "path/to/input.bin")
+   *
+   * An alternative approach one might choose by using existing interpolators
+   * is something like this:
+   *
+   *   s"parse -s $schema $input".split(" ")
+   *
+   * This issue with this approach is that if the $schema or $input variables
+   * evaluate to something with string (which is not uncommon on some windows
+   * systems), then we end up splitting those files paths into separate
+   * arguments. This args interpolator ensures we don't split spaces that come
+   * from expressions.
+   *
+   * Note that quotes cannot be used to prevent splitting. For example, this
+   *
+   *   args"quotes do 'not prevent' splitting"
+   *
+   * Results in the following:
+   *
+   *   Array("quotes", "do", "'not", "prevent'", "splitting")
+   *
+   * To prevent splitting on a particular space, then expressions can be used,
+   * for example:
+   *
+   *   args"this ${"is split"} correctly"
+   *
+   * Which results in the following:
+   *
+   *   Array("this", "is split", "correctly")
+   *
+   * Note that this also handles concatenating expression correctly, for
+   * example:
+   *
+   *   args"some --arg=$root/$value"
+   *
+   * Results in
+   *
+   *   Array("some", "--arg=the/result/of/root/and/value")
+   *
+   */
+  implicit class ArgsHelper(val sc: StringContext) extends AnyVal {
+    def args(exprs: Any*): Array[String] = {
+      val strings = sc.parts.iterator
+      val expressions = exprs.iterator
+      val buf = mutable.ArrayBuffer[String]()
+
+      // regex to split on spaces, but using positive lookahead and lookbehind
+      // so the spaces aren't discard and end up in the array themselves. For
+      // example, using this regular expression to split this string:
+      //
+      //   "parse --schema foo.xsd input"
+      //
+      // Results in the following:
+      //
+      //   Array("parse", " ", "--schema", "foo.xsd", " ", "input")
+      //
+      // This is necessary so that when after we build the buf array, the

Review Comment:
   Please choose only one of "when" or "after"



##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to
+   * true. Otherwise a new process is spawned.
+   *
+   * @param args arguments to pass to the CLI. This should not include the
+   *   daffodil binary
+   * @param classpaths sequence of paths to add to the classpath. If non-empty,
+   *   runs the CLI in a new process instead of a thread and will likely decrease
+   *   performance
+   * @param fork if true, forces the the CLI in a new process

Review Comment:
   the the CLI in -> the CLI to run in



##########
daffodil-cli/src/it/scala/org/apache/daffodil/parsing/TestCLIParsing.scala:
##########
@@ -265,1124 +153,535 @@ class TestCLIparsing {
   //  verifies the expected output. If this test fails, it likely means we've
   //  broken our attempts to create consistent prefix mappings.
   @Test def test_1585_CLI_Parsing_MultifileSchema_methodImportSameDir(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/multi_base_14.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format(Util.echoN("test") + "| %s parse -s %s", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output9))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/multi_base_14.dfdl.xsd")
+
+    runCLI(args"parse -s $schema") { cli =>
+      cli.send("test", inputDone = true)
+      cli.expect("""<base14:rabbitHole xmlns:a14="http://a14.com" xmlns:b14="http://b14.com" xmlns:base14="http://baseSchema.com">""")
+      cli.expect("<a14:nestSequence>")
+      cli.expect("<b14:nest>test</b14:nest>")
+    } (ExitCode.Success)
   }
 
   @Test def test_1586_CLI_Parsing_MultifileSchema_methodIncludeSameDir(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/multi_base_15.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format(Util.echoN("test") + "| %s parse -s %s", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-
-      shell.expect(contains(output10))
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/multi_base_15.dfdl.xsd")
 
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema") { cli =>
+      cli.send("test", inputDone = true)
+      cli.expect("<rabbitHole>")
+      cli.expect("<nest>test</nest>")
+    } (ExitCode.Success)
   }
 
   @Test def test_1587_CLI_Parsing_MultifileSchema_methodImportSameDir2(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/multi_base_16.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format(Util.echoN("test") + "| %s parse -s %s", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/multi_base_16.dfdl.xsd")
 
-      shell.expect(contains(output10))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema") { cli =>
+      cli.send("test", inputDone = true)
+      cli.expect("<rabbitHole>")
+      cli.expect("<nest>test</nest>")
+    } (ExitCode.Success)
   }
 
   @Test def test_1317_IBMCompatibility_ABC_test_ibm_abc_cli(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/ABC_IBM.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/ABC_IBM.dfdl.xsd")
 
-    try {
-      val cmd = String.format("echo abcabcabc| %s parse -s %s -r ABC", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-
-      shell.expect(contains(output8))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema -r ABC") { cli =>
+      cli.sendLine("abcabcabc", inputDone = true)
+      cli.expect("<Container>")
+      cli.expect("<c>c</c>")
+    } (ExitCode.Success)
   }
 
   @Test def test_977_CLI_Parsing_SimpleParse_stdOut(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-    try {
-      val cmd = String.format("echo 0,1,2| %s parse -s %s -r matrix", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-
-      shell.expect(contains(output1))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema -r matrix") { cli =>
+      cli.sendLine("0,1,2", inputDone = true)
+      cli.expect("<tns:cell>2</tns:cell>")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_978_CLI_Parsing_SimpleParse_outFile(): Unit = {
-    val tmp_filename: String = (System.currentTimeMillis / 1000).toString()
-    val file = new File(tmp_filename)
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 0,1,2| %s parse -s %s -r matrix -o %s", Util.binPath, testSchemaFile, tmp_filename)
-      shell.sendLine(cmd)
-
-      val catCmd = if (Util.isWindows) "type" else "cat"
-      val openCmd = String.format("%s %s", catCmd, tmp_filename)
-
-      shell.sendLine(openCmd)
-      shell.expect(contains("<tns:cell>2</tns:cell>"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-      assertTrue("Failed to remove temporary file: %s".format(file), file.delete)
+    withTempFile { output =>
+      val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+
+      runCLI(args"parse -s $schema -r matrix -o $output") { cli =>
+        cli.sendLine("0,1,2", inputDone = true)
+      } (ExitCode.LeftOverData)
+
+      val res = FileUtils.readFileToString(output.toFile, UTF_8)
+      assertTrue(res.contains("<tns:cell>2</tns:cell>"))
     }
   }
 
   @Test def test_979_CLI_Parsing_SimpleParse_inFile(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output1))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
+
+    runCLI(args"parse -s $schema -r matrix $input") { cli =>
+      cli.expect("<tns:cell>2</tns:cell>")
+    } (ExitCode.Success)
   }
 
   @Test def test_980_CLI_Parsing_SimpleParse_stOutDash(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s parse -s %s -r matrix -o - %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output1))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
+
+    runCLI(args"parse -s $schema -r matrix -o - $input") { cli =>
+      cli.expect("<tns:cell>2</tns:cell>")
+    } (ExitCode.Success)
   }
 
   @Test def test_981_CLI_Parsing_SimpleParse_stdInDash(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-    try {
-      val cmd = String.format("echo 0,1,2,3| %s parse -s %s -r matrix -", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output2))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema -r matrix -") { cli =>
+      cli.sendLine("0,1,2,3", inputDone = true)
+      cli.expect("<tns:cell>3</tns:cell>")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_983_CLI_Parsing_SimpleParse_verboseMode(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-    try {
-      shell.sendLine(String.format("echo 0,1| %s -v parse -s %s -r matrix -", Util.binPath, testSchemaFile))
-      shell.expectIn(1, contains("[info]"))
+    runCLI(args"-v parse -s $schema -r matrix -") { cli =>
+      cli.sendLine("0,1", inputDone = true)
+      cli.expectErr("[info]")
+    } (ExitCode.LeftOverData)
 
-      shell.sendLine(String.format("echo 0,1| %s -vv parse -s %s -r matrix -", Util.binPath, testSchemaFile))
-      shell.expectIn(1, contains("[debug]"))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"-vv parse -s $schema -r matrix -") { cli =>
+      cli.sendLine("0,1", inputDone = true)
+      cli.expectErr("[debug]")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_984_CLI_Parsing_negativeTest(): Unit = {
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 0,1,2,3| %s parse", Util.binPath)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("There should be exactly one of the following options: schema, parser"))
-
-      Util.expectExitCode(ExitCode.Usage, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse") { cli =>
+      cli.sendLine("0,1,2,3", inputDone = true)
+      cli.expectErr("There should be exactly one of the following options: schema, parser")
+    } (ExitCode.Usage)
   }
 
   @Test def test_985_CLI_Parsing_SimpleParse_defaultRoot(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 0,1,2,3| %s parse -s %s", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output2))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema") { cli =>
+      cli.sendLine("0,1,2,3", inputDone = true)
+      cli.expect("<tns:cell>3</tns:cell>")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_988_CLI_Parsing_SimpleParse_specifiedRoot(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
-
-    try {
-      //val expected = """<tns:hcp2 xmlns:tns="http://www.example.org/example1/">12</tns:hcp2>"""
-      val cmd = String.format("echo 12| %s parse -s %s -r hcp2", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("<tns:hcp2"))
-      shell.expect(contains("12"))
-      shell.expect(contains("</tns:hcp2>"))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+
+    runCLI(args"parse -s $schema -r hcp2") { cli =>
+      cli.sendLine("12", inputDone = true)
+      cli.expect("<tns:hcp2")
+      cli.expect("12")
+      cli.expect("</tns:hcp2>")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_996_CLI_Parsing_negativeTest04(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 12| %s parse -s %s -r unknown", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("No root element found for unknown in any available namespace"))
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-      Util.expectExitCode(ExitCode.UnableToCreateProcessor, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema -r unknown") { cli =>
+      cli.sendLine("12", inputDone = true)
+      cli.expectErr("No root element found for unknown in any available namespace")
+    } (ExitCode.UnableToCreateProcessor)
   }
 
   @Test def test_997_CLI_Parsing_multSchemas(): Unit = {
-    val schemaFile1 = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val schemaFile2 = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section07/defineFormat/defineFormat.dfdl.xsd")
-    val (testSchemaFile1, testSchemaFile2) = if (Util.isWindows) (Util.cmdConvert(schemaFile1), Util.cmdConvert(schemaFile2)) else (schemaFile1, schemaFile2)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 12| %s parse -s %s -s %s  -r hcp2", Util.binPath, testSchemaFile1, testSchemaFile2)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("Bad arguments for option 'schema'"))
-
-      Util.expectExitCode(ExitCode.Usage, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema1 = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val schema2 = path("daffodil-test/src/test/resources/org/apache/daffodil/section07/defineFormat/defineFormat.dfdl.xsd")
+
+    runCLI(args"parse -s $schema1 -s $schema2 -r hcp2") { cli =>
+      cli.sendLine("12", inputDone = true)
+      cli.expectErr("Bad arguments for option 'schema'")
+    } (ExitCode.Usage)
   }
 
   @Test def test_3661_CLI_Parsing_badSchemaPath(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/doesnotexist.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 12| %s parse -s %s -r root", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("Bad arguments for option 'schema'"))
-      shell.expectIn(1, contains("Could not find file or resource"))
-
-      Util.expectExitCode(ExitCode.Usage, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/doesnotexist.dfdl.xsd")
+
+    runCLI(args"parse -s $schema -r root") { cli =>
+      cli.sendLine("12", inputDone = true)
+      cli.expectErr("Bad arguments for option 'schema'")
+      cli.expectErr("Could not find file or resource")
+    } (ExitCode.Usage)
   }
 
   @Test def test_1002_CLI_Parsing_negativeTest03(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 0,1,2| %s parse -P parserThatDoesNotExist", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      if (Util.isWindows) {
-        shell.expectIn(1, contains("parserThatDoesNotExist (The system cannot find the file specified)"))
-      } else {
-        shell.expectIn(1, contains("parserThatDoesNotExist (No such file or directory)"))
-      }
 
-      Util.expectExitCode(ExitCode.FileNotFound, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -P parserThatDoesNotExist") { cli =>
+      cli.sendLine("0,1,2", inputDone = true)
+      cli.expectErr("parserThatDoesNotExist")
+    } (ExitCode.FileNotFound)
   }
 
   @Test def test_1003_CLI_Parsing_SimpleParse_emptyNamespace(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section07/defineFormat/defineFormat.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input7.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s parse -s %s -r {}address %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output4))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section07/defineFormat/defineFormat.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input7.txt")
+
+    runCLI(args"parse -s $schema -r {}address $input") { cli =>
+      cli.expect("<address>")
+    } (ExitCode.Success)
   }
 
   @Test def test_1004_CLI_Parsing_SimpleParse_namespaceUsed(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input8.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s parse -s %s -r {target}matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output6))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input8.txt")
+
+    runCLI(args"parse -s $schema -r {target}matrix $input") { cli =>
+      cli.expect("""<tns:matrix xmlns:tns="target">""")
+      cli.expect("<cell>14</cell>")
+    } (ExitCode.Success)
   }
 
   @Test def test_2615_CLI_Parsing_SimpleParse_namespaceUsedLongOpt(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input8.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s parse -s %s --root {target}matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output6))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input8.txt")
+
+    runCLI(args"parse -s $schema --root {target}matrix $input") { cli =>
+      cli.expect("""<tns:matrix xmlns:tns="target">""")
+      cli.expect("<cell>14</cell>")
+    } (ExitCode.Success)
   }
 
   @Test def test_1005_CLI_Parsing_SimpleParse_rootPath(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.startNoConvert("")
-
-    try {
-      //val expected = """<tns:hcp2 xmlns:tns="http://www.example.org/example1/">12</tns:hcp2>"""
-      val cmd = String.format(Util.echoN("12") + "| %s parse -s %s -r hcp2 -p /", Util.binPath, testSchemaFile)
-
-      shell.sendLine(cmd)
-      shell.expect(contains("<tns:hcp2 xmlns:tns=\"http://www.example.org/example1/\">"))
-      shell.expect(contains("12"))
-      shell.expect(contains("</tns:hcp2>"))
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema -r hcp2 -p /") { cli =>
+      cli.send("12", inputDone = true)
+      cli.expect("<tns:hcp2 xmlns:tns=\"http://www.example.org/example1/\">")
+      cli.expect("12")
+      cli.expect("</tns:hcp2>")
+    } (ExitCode.Success)
   }
 
   @Test def test_1015_CLI_Parsing_SimpleParse_defaultRootMultSchema(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section07/defineFormat/defineFormat.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input7.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s parse -s %s %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output4))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section07/defineFormat/defineFormat.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input7.txt")
+
+    runCLI(args"parse -s $schema $input") { cli =>
+      cli.expect("<address>")
+    } (ExitCode.Success)
   }
 
   @Test def test_XXX_CLI_Parsing_SimpleSchema_basicTest_validationOn(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 0,1,2| %s parse -s %s -r matrix --validate on", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output1))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema -r matrix --validate on") { cli =>
+      cli.sendLine("0,1,2", inputDone = true)
+      cli.expect("<tns:cell>2</tns:cell>")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_XXX_CLI_Parsing_SimpleSchema_basicTest_validation_missing_mode(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 0,1,2| %s parse -s %s -r matrix --validate", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("Bad arguments"))
-      shell.expectIn(1, contains("validate"))
-      shell.expectIn(1, contains("exactly one argument"))
-
-      Util.expectExitCode(ExitCode.Usage, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+
+    runCLI(args"parse -s $schema -r matrix --validate") { cli =>
+      cli.sendLine("0,1,2", inputDone = true)
+      cli.expectErr("Bad arguments")
+      cli.expectErr("validate")
+      cli.expectErr("exactly one argument")
+    } (ExitCode.Usage)
   }
 
   @Test def test_XXX_CLI_Parsing_SimpleSchema_basicTest_validationLimited(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 0,1,2| %s parse -s %s -r matrix --validate limited", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output1))
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema -r matrix --validate limited") { cli =>
+      cli.sendLine("0,1,2", inputDone = true)
+      cli.expect("<tns:cell>2</tns:cell>")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_XXX_CLI_Parsing_SimpleSchema_basicTest_validationOff(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 0,1,2| %s parse -s %s -r matrix --validate off", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains(output1))
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema -r matrix --validate off") { cli =>
+      cli.sendLine("0,1,2", inputDone = true)
+      cli.expect("<tns:cell>2</tns:cell>")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_XXX_CLI_Parsing_SimpleSchema_basicTest_validationFooBar(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 0,1,2| %s parse --validate FooBar -s %s -r matrix", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("FooBar"))
-
-      Util.expectExitCode(ExitCode.Usage, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
-  }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-  /*
-  //On hold until I implement a way to set the classpath before executing
-  @Test def test_1313_CLI_Parsing_assertionFailure() {
-    val cmd = "echo unacceptable| " + Util.binPath + " parse -s daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/multi_B_08.dfdl.xsd -s daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/multi_C_08.dfdl.xsd --root bElem2\n"
-    val shell = Util.start(cmd)
-    shell.expect(contains("Parse Error: Assertion failed. Assertion failed for dfdl:checkConstraints(.)"))
-
-    shell.send("exit\n")
-    shell.expect(eof)
-    shell.close()
+    runCLI(args"parse --validate FooBar -s $schema -r matrix") { cli =>
+      cli.sendLine("0,1,2", inputDone = true)
+      cli.expectErr("FooBar")
+    } (ExitCode.Usage)
   }
-*/
 
   @Test def test_1319_CLI_Parsing_invalidElementSDE(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/ABC_IBM_invalid.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo ababababbaacccccb| %s parse -s %s -r ABC", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("'fixed' is not a valid"))
-
-      Util.expectExitCode(ExitCode.UnableToCreateProcessor, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/ABC_IBM_invalid.dfdl.xsd")
+
+    runCLI(args"parse -s $schema -r ABC") { cli =>
+      cli.sendLine("ababababbaacccccb", inputDone = true)
+      cli.expectErr("'fixed' is not a valid")
+    } (ExitCode.UnableToCreateProcessor)
   }
 
   @Test def test_1346_CLI_Parsing_SimpleParse_defaultRootMultSchemaMultiple(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section07/defineFormat/defineFormat.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input7.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val cmd = String.format("%s parse -s %s %s", Util.binPath, testSchemaFile, testInputFile)
-
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section07/defineFormat/defineFormat.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input7.txt")
     for (x <- 1 to 10) {
-      val shell = Util.start("")
-
-      try {
-        println("Run " + x + " of 10")
-        shell.sendLine(cmd)
-        shell.expect(contains(output4))
-
-        Util.expectExitCode(ExitCode.Success, shell)
-        shell.sendLine("exit")
-        shell.expect(eof)
-      } finally {
-        shell.close()
-      }
+      runCLI(args"parse -s $schema $input") { cli =>
+        cli.expect("<address>")
+      } (ExitCode.Success)
     }
   }
 
   @Test def test_1386_CLI_Parsing_negativeTest05(): Unit = {
-    val cmd = String.format("echo 12| %s", Util.binPath)
-    val shell = Util.start("")
-
-    try {
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("Subcommand required"))
-
-      Util.expectExitCode(ExitCode.Usage, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"") { cli =>
+      cli.sendLine("12", inputDone = true)
+      cli.expectErr("Subcommand required")
+    } (ExitCode.Usage)
   }
 
   @Test def test_1971_CLI_Parsing_traceMode01(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/multi_base_15.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo test| %s -t parse -s %s", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("parser: <Element name='rabbitHole'>"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/namespaces/multi_base_15.dfdl.xsd")
+
+    runCLI(args"-t parse -s $schema") { cli =>
+      cli.sendLine("test", inputDone = true)
+      cli.expect("parser: <Element name='rabbitHole'>")
+    } (ExitCode.Success)
   }
 
   @Test def test_1973_CLI_Parsing_traceMode03(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 0,1,2,3,,,,| %s -t parse -s %s", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("Left over data. Consumed 56 bit(s) with at least"))
-      shell.expectIn(1, contains("Left over data (Hex) starting at byte 8 is: ("))
-      shell.expectIn(1, contains("Left over data (UTF-8) starting at byte 8 is: ("))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-      //assert(shell.getExitValue() == 1)
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+
+    runCLI(args"-t parse -s $schema") { cli =>
+      cli.sendLine("0,1,2,3,,,,", inputDone = true)
+      cli.expectErr("Left over data. Consumed 56 bit(s) with at least")
+      cli.expectErr("Left over data (Hex) starting at byte 8 is: (")
+      cli.expectErr("Left over data (UTF-8) starting at byte 8 is: (")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_1941_CLI_Parsing_SimpleParse_leftOverData(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("echo 1,2,3,4,,,| %s parse -s %s -r matrix", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("Left over data. Consumed 56 bit(s) with at least"))
-      shell.expectIn(1, contains("Left over data (Hex) starting at byte 8 is: ("))
-      shell.expectIn(1, contains("Left over data (UTF-8) starting at byte 8 is: ("))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+
+    runCLI(args"parse -s $schema -r matrix") { cli =>
+      cli.sendLine("1,2,3,4,,,", inputDone = true)
+      cli.expectErr("Left over data. Consumed 56 bit(s) with at least")
+      cli.expectErr("Left over data (Hex) starting at byte 8 is: (")
+      cli.expectErr("Left over data (UTF-8) starting at byte 8 is: (")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_CLI_Parsing_BitParse_LSBPartialByte_leftOverData(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/bits_parsing.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format(Util.echoN("stri") + "| %s parse -s %s -r lsbPartialByte", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("Left over data. Consumed 10 bit(s) with at least 16 bit(s) remaining."
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/bits_parsing.dfdl.xsd")
+
+    runCLI(args"parse -s $schema -r lsbPartialByte") { cli =>
+      cli.send("stri", inputDone = true)
+      cli.expectErr("Left over data. Consumed 10 bit(s) with at least 16 bit(s) remaining."
         + "\nLeft over data starts with partial byte. Left over data (Binary) at byte 2 is: (0b011101xx)"
         + "\nLeft over data (Hex) starting at byte 3 is: (0x7269...)"
-        + "\nLeft over data (UTF-8) starting at byte 3 is: (ri...)"))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+        + "\nLeft over data (UTF-8) starting at byte 3 is: (ri...)")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_CLI_Parsing_BitParse_MSBPartialByte_leftOverData(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/bits_parsing.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format(Util.echoN("stri") + "| %s parse -s %s -r msbPartialByte", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("Left over data. Consumed 10 bit(s) with at least 16 bit(s) remaining."
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/bits_parsing.dfdl.xsd")
+
+    runCLI(args"parse -s $schema -r msbPartialByte") { cli =>
+      cli.send("stri", inputDone = true)
+      cli.expectErr("Left over data. Consumed 10 bit(s) with at least 16 bit(s) remaining."
         + "\nLeft over data starts with partial byte. Left over data (Binary) at byte 2 is: (0bxx110100)"
         + "\nLeft over data (Hex) starting at byte 3 is: (0x7269...)"
-        + "\nLeft over data (UTF-8) starting at byte 3 is: (ri...)"))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+        + "\nLeft over data (UTF-8) starting at byte 3 is: (ri...)")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_CLI_Parsing_BitParse_MSBFullByte_leftOverData(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/bits_parsing.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format(Util.echoN("stri") + "| %s parse -s %s -r msbFullByte", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("Left over data. Consumed 16 bit(s) with at least 16 bit(s) remaining."
-        + "\nLeft over data (Hex) starting at byte 3 is: (0x7269...)"
-        + "\nLeft over data (UTF-8) starting at byte 3 is: (ri...)"))
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/bits_parsing.dfdl.xsd")
 
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema -r msbFullByte") { cli =>
+      cli.send("stri", inputDone = true)
+      cli.expectErr("Left over data. Consumed 16 bit(s) with at least 16 bit(s) remaining."
+        + "\nLeft over data (Hex) starting at byte 3 is: (0x7269...)"
+        + "\nLeft over data (UTF-8) starting at byte 3 is: (ri...)")
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_DFDL_714(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/global_element.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/test_DFDL-714.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s parse -s %s %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("<tns:elem xmlns:tns=\"http://baseSchema.com\">"))
-      shell.expect(contains("<content"))
-      shell.expect(contains("Hello World"))
-      shell.expect(contains("</tns:elem>"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/global_element.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/test_DFDL-714.txt")
+
+    runCLI(args"parse -s $schema $input") { cli =>
+      cli.expect("<tns:elem xmlns:tns=\"http://baseSchema.com\">")
+      cli.expect("<content")
+      cli.expect("Hello World")
+      cli.expect("</tns:elem>")
+    } (ExitCode.Success)
   }
 
   @Test def test_DFDL_1203_schema_from_jar(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/global_element.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/test_DFDL-714.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("", envp = Map("DAFFODIL_CLASSPATH" -> Util.daffodilPath("daffodil-cli/target/scala-2.10/*")))
-
-    try {
-      val cmd = String.format("%s parse -s %s %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("<tns:elem xmlns:tns=\"http://baseSchema.com\">"))
-      shell.expect(contains("<content"))
-      shell.expect(contains("Hello World"))
-      shell.expect(contains("</tns:elem>"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/global_element.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/test_DFDL-714.txt")
+
+    runCLI(args"parse -s $schema $input") { cli =>
+      cli.expect("<tns:elem xmlns:tns=\"http://baseSchema.com\">")
+      cli.expect("<content")
+      cli.expect("Hello World")
+      cli.expect("</tns:elem>")
+    } (ExitCode.Success)
   }
 
   @Test def test_3606_CLI_Parsing_SimpleParse_largeInfoset(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val shell = Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-    try {
+    runCLI(args"parse -s $schema -r matrix") { cli =>
       val longInput = "0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64,0,1,24,5,64"
-      val cmd = String.format("echo %s| %s parse -s %s -r matrix", longInput, Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-
-      val result = shell.expect(contains("<tns:row")).getBefore()
-      println(result)
+      cli.sendLine(longInput, inputDone = true)
+      val result = cli.expect("<tns:row").getBefore()
       if (result.contains("""<tns:matrix xmlns:tns="http://www.example.org/example1/"><tns:matrix xmlns:tns="http://www.example.org/example1/">""")) {
         throw new Exception("Error - Root has been duplicated")
       }
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.LeftOverData)
   }
 
   @Test def test_CLI_Parsing_built_in_formats(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema_04.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input6.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("")
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema_04.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input6.txt")
 
-    try {
-      val cmd = String.format("%s parse -s %s -r e %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-
-      shell.expectIn(1, contains("Schema Definition Warning"))
-      shell.expectIn(1, contains("edu/illinois/ncsa/daffodil/xsd/built-in-formats.xsd"))
-      shell.expectIn(1, contains("org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("quit")
-    } finally {
-      shell.close()
-    }
+    runCLI(args"parse -s $schema -r e $input") { cli =>
+      cli.expectErr("Schema Definition Warning")
+      cli.expectErr("edu/illinois/ncsa/daffodil/xsd/built-in-formats.xsd")
+      cli.expectErr("org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd")
+    } (ExitCode.Success)
   }
 
-  // These DAFFODIL_JAVA_OPTS values change the Java defaults classes like
-  // SAXParserFactory and SchemaFactory to be Java's internal classes instead
-  // of those provided by dependencies (e.g. Xerces) included with Daffodil.
-  // Some places require dependency version of these classes. This test ensures
-  // that we override defaults when necesssary
   @Test def test_CLI_Parsing_JavaDefaults(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-    val java_opts = Map("DAFFODIL_JAVA_OPTS" ->
-      ("-Djavax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl " +
-        "-Djavax.xml.xml.validation.SchemaFactory=com/sun/org/apache/xerces/internal/jaxp/validation/XMLSchemaFactory"))
-
-    val shell = Util.start("", envp = java_opts)
-
-    try {
-      val cmd = String.format("echo 0,1,2| %s parse -s %s -r matrix", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-
-      shell.expect(contains(output1))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+
+    withSysProp("javax.xml.parsers.SAXParserFactory" -> "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl") {
+      withSysProp("javax.xml.xml.validation.SchemaFactory" -> "com/sun/org/apache/xerces/internal/jaxp/validation/XMLSchemaFactory") {
+        runCLI(args"parse -s $schema -r matrix") { cli =>

Review Comment:
   Nice!



##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to
+   * true. Otherwise a new process is spawned.
+   *
+   * @param args arguments to pass to the CLI. This should not include the
+   *   daffodil binary
+   * @param classpaths sequence of paths to add to the classpath. If non-empty,
+   *   runs the CLI in a new process instead of a thread and will likely decrease
+   *   performance
+   * @param fork if true, forces the the CLI in a new process
+   * @param timeout how long to wait, in seconds, for the CLI to exit after the
+   *   testFunc has returned. Also how long to wait for individual expect
+   *   operations in the CLITester
+   * @param debug if true, prints arguments and classpath information to
+   *   stdout. Also echos all CLITester input and output to stdout.
+   * @param testFunc function to call to send input to the CLI and validate
+   *   output from CLI stdout/stderr.
+   * @param expectedExitCode the expected exit code of the CLI. In the actual
+   *   exit code does not match
+   *
+   * @throws AssertionError if the actual exit code does not match the expected exit code
+   * @throws ExpectIOException if the an CLITester expect validation operation fails

Review Comment:
   Please choose only one article (the / a).



##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to
+   * true. Otherwise a new process is spawned.
+   *
+   * @param args arguments to pass to the CLI. This should not include the
+   *   daffodil binary
+   * @param classpaths sequence of paths to add to the classpath. If non-empty,
+   *   runs the CLI in a new process instead of a thread and will likely decrease
+   *   performance
+   * @param fork if true, forces the the CLI in a new process
+   * @param timeout how long to wait, in seconds, for the CLI to exit after the
+   *   testFunc has returned. Also how long to wait for individual expect
+   *   operations in the CLITester
+   * @param debug if true, prints arguments and classpath information to
+   *   stdout. Also echos all CLITester input and output to stdout.
+   * @param testFunc function to call to send input to the CLI and validate
+   *   output from CLI stdout/stderr.
+   * @param expectedExitCode the expected exit code of the CLI. In the actual
+   *   exit code does not match
+   *
+   * @throws AssertionError if the actual exit code does not match the expected exit code
+   * @throws ExpectIOException if the an CLITester expect validation operation fails
+   */
+  def runCLI
+    (args: Array[String], classpaths: Seq[Path] = Seq(), fork: Boolean = false, timeout: Int = 10, debug: Boolean = false)
+    (testFunc: (CLITester) => Unit)
+    (expectedExitCode: ExitCode.Value): Unit = {
+
+    val (toIn, fromOut, fromErr, threadOrProc: Either[CLIThread, Process]) =
+      if (classpaths.nonEmpty || fork) {
+        // spawn a new process to run Daffodil, needed if a custom classpath is
+        // defined or if the caller explicitly wants to fork
+        val processBuilder = new ProcessBuilder()
+
+        if (classpaths.nonEmpty) {
+          val classpath = classpaths.mkString(File.pathSeparator)
+          if (debug) System.out.println(s"DAFFODIL_CLASSPATH=$classpath")
+          processBuilder.environment().put("DAFFODIL_CLASSPATH", classpath)
+        }
+
+        val cmd = daffodilBinPath.toString +: args
+        if (debug) System.out.println(cmd.mkString(" "))
+        processBuilder.command(cmd.toList.asJava)
+
+        val process = processBuilder.start()
+
+        val toIn = process.getOutputStream()
+        val fromOut = process.getInputStream()
+        val fromErr = process.getErrorStream()
+        (toIn, fromOut, fromErr, Right(process))
+      } else {
+        // create a new thread for the CLI test to run, using piped
+        // input/output streams to connected the thread and the CLItester

Review Comment:
   connected -> connect



##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to

Review Comment:
   he -> the (fork parameter....)



##########
daffodil-cli/src/it/scala/org/apache/daffodil/executing/TestCLIexecuting.scala:
##########
@@ -1,245 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.daffodil.executing
-
-import org.junit.Assert._
-import org.junit.Test
-import org.apache.daffodil.CLI.Util
-import net.sf.expectit.matcher.Matchers.contains
-import net.sf.expectit.matcher.Matchers.matches
-import net.sf.expectit.ExpectIOException
-import org.apache.daffodil.Main.ExitCode
-
-class TestCLIexecuting {
-
-  val output3 = Util.getExpectedString("output3.txt")
-  val output13 = Util.getExpectedString("output13.txt", true)
-  val output14 = Util.getExpectedString("output14.txt", true)
-  val output15 = Util.getExpectedString("output15.txt", true)
-  val output16 = Util.getExpectedString("output16.txt", true)

Review Comment:
   This PR remove these output files above, discards one or more tests, and moves some tests to a new file called TestCLItdml.scala, inlining the expected strings instead of getting the expected strings from the output files.  My only question is, why is the new name TestCLItdml.scala better than the old name TestCLIexecuting.scala?



##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to
+   * true. Otherwise a new process is spawned.
+   *
+   * @param args arguments to pass to the CLI. This should not include the
+   *   daffodil binary
+   * @param classpaths sequence of paths to add to the classpath. If non-empty,
+   *   runs the CLI in a new process instead of a thread and will likely decrease
+   *   performance
+   * @param fork if true, forces the the CLI in a new process
+   * @param timeout how long to wait, in seconds, for the CLI to exit after the
+   *   testFunc has returned. Also how long to wait for individual expect
+   *   operations in the CLITester
+   * @param debug if true, prints arguments and classpath information to
+   *   stdout. Also echos all CLITester input and output to stdout.
+   * @param testFunc function to call to send input to the CLI and validate
+   *   output from CLI stdout/stderr.
+   * @param expectedExitCode the expected exit code of the CLI. In the actual
+   *   exit code does not match
+   *
+   * @throws AssertionError if the actual exit code does not match the expected exit code
+   * @throws ExpectIOException if the an CLITester expect validation operation fails
+   */
+  def runCLI
+    (args: Array[String], classpaths: Seq[Path] = Seq(), fork: Boolean = false, timeout: Int = 10, debug: Boolean = false)
+    (testFunc: (CLITester) => Unit)
+    (expectedExitCode: ExitCode.Value): Unit = {
+
+    val (toIn, fromOut, fromErr, threadOrProc: Either[CLIThread, Process]) =
+      if (classpaths.nonEmpty || fork) {
+        // spawn a new process to run Daffodil, needed if a custom classpath is
+        // defined or if the caller explicitly wants to fork
+        val processBuilder = new ProcessBuilder()
+
+        if (classpaths.nonEmpty) {
+          val classpath = classpaths.mkString(File.pathSeparator)
+          if (debug) System.out.println(s"DAFFODIL_CLASSPATH=$classpath")
+          processBuilder.environment().put("DAFFODIL_CLASSPATH", classpath)
+        }
+
+        val cmd = daffodilBinPath.toString +: args
+        if (debug) System.out.println(cmd.mkString(" "))
+        processBuilder.command(cmd.toList.asJava)
+
+        val process = processBuilder.start()
+
+        val toIn = process.getOutputStream()
+        val fromOut = process.getInputStream()
+        val fromErr = process.getErrorStream()
+        (toIn, fromOut, fromErr, Right(process))
+      } else {
+        // create a new thread for the CLI test to run, using piped
+        // input/output streams to connected the thread and the CLItester
+        val in = new PipedInputStream()
+        val toIn = new PipedOutputStream(in)
+
+        val out = new PipedOutputStream()
+        val fromOut = new PipedInputStream(out)
+
+        val err = new PipedOutputStream()
+        val fromErr = new PipedInputStream(err)
+
+        if (debug) System.out.println("daffodil " + args.mkString(" "))
+
+        val thread = new CLIThread(args, in, out, err)
+        thread.start()
+        (toIn, fromOut, fromErr, Left(thread))
+      }
+
+    val eb = new ExpectBuilder()
+    eb.withOutput(toIn)
+    eb.withInputs(fromOut, fromErr)
+    eb.withInputFilters(replaceInString("\r\n", "\n"))
+    eb.withTimeout(timeout, TimeUnit.SECONDS)
+    eb.withExceptionOnFailure()
+    if (debug) {
+      eb.withEchoOutput(System.out)
+      eb.withEchoInput(System.out)
+    }
+    val expect = eb.build()
+    val tester = new CLITester(expect, toIn)
+
+    try {
+      testFunc(tester)
+    } finally {
+      threadOrProc match {
+        case Left(thread) => thread.join(timeout * 1000)
+        case Right(process) => process.waitFor(timeout, TimeUnit.SECONDS)
+      }
+      expect.close()
+      toIn.close()
+      fromOut.close()
+      fromErr.close()
     }
-  }
 
-  def makeMultipleCmds(cmds: Array[String]): String = {
-    if (isWindows) {
-      cmds.mkString(" & ")
-    } else {
-      cmds.mkString("; ")
+    val actualExitCode = threadOrProc match {
+      case Left(thread) => thread.exitCode
+      case Right(process) => ExitCode(process.exitValue)
     }
+    assertEquals("Incorrect exit code,", expectedExitCode, actualExitCode)
   }
 
-  def md5sum(blob_path: String): String = {
-    if (isWindows) {
-      String.format("certutil -hashfile %s MD5", blob_path)
-    } else {
-      String.format("md5sum %s", blob_path)
+  /**
+   * A class to run the CLI in a thread instead of a new process, given the
+   * arguments to use (excluded the daffodil binary) and streams to use for
+   * stdin/out/err.
+   */
+  private class CLIThread(args: Array[String], in: InputStream, out: OutputStream, err: OutputStream) extends Thread {
+    var exitCode = ExitCode.Failure
+
+    override def run(): Unit = {
+      val psOut = new PrintStream(out)
+      val psErr = new PrintStream(err)
+
+      // configure the CLI and log4j to use our custom streams, nothing should
+      // not actually use stdin/stdout/stderr

Review Comment:
   not actually -> actually
   
   However, there are some println calls in Daffodil code not touched by this PR that still will go to the original stdout/stderr.  The majority of printlns are in test code and can be ignored, but what about the rest of the printlns?



##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to
+   * true. Otherwise a new process is spawned.
+   *
+   * @param args arguments to pass to the CLI. This should not include the
+   *   daffodil binary
+   * @param classpaths sequence of paths to add to the classpath. If non-empty,
+   *   runs the CLI in a new process instead of a thread and will likely decrease
+   *   performance
+   * @param fork if true, forces the the CLI in a new process
+   * @param timeout how long to wait, in seconds, for the CLI to exit after the
+   *   testFunc has returned. Also how long to wait for individual expect
+   *   operations in the CLITester
+   * @param debug if true, prints arguments and classpath information to
+   *   stdout. Also echos all CLITester input and output to stdout.
+   * @param testFunc function to call to send input to the CLI and validate
+   *   output from CLI stdout/stderr.
+   * @param expectedExitCode the expected exit code of the CLI. In the actual
+   *   exit code does not match
+   *
+   * @throws AssertionError if the actual exit code does not match the expected exit code
+   * @throws ExpectIOException if the an CLITester expect validation operation fails
+   */
+  def runCLI
+    (args: Array[String], classpaths: Seq[Path] = Seq(), fork: Boolean = false, timeout: Int = 10, debug: Boolean = false)
+    (testFunc: (CLITester) => Unit)
+    (expectedExitCode: ExitCode.Value): Unit = {
+
+    val (toIn, fromOut, fromErr, threadOrProc: Either[CLIThread, Process]) =
+      if (classpaths.nonEmpty || fork) {
+        // spawn a new process to run Daffodil, needed if a custom classpath is
+        // defined or if the caller explicitly wants to fork
+        val processBuilder = new ProcessBuilder()
+
+        if (classpaths.nonEmpty) {
+          val classpath = classpaths.mkString(File.pathSeparator)
+          if (debug) System.out.println(s"DAFFODIL_CLASSPATH=$classpath")
+          processBuilder.environment().put("DAFFODIL_CLASSPATH", classpath)
+        }
+
+        val cmd = daffodilBinPath.toString +: args
+        if (debug) System.out.println(cmd.mkString(" "))
+        processBuilder.command(cmd.toList.asJava)
+
+        val process = processBuilder.start()
+
+        val toIn = process.getOutputStream()
+        val fromOut = process.getInputStream()
+        val fromErr = process.getErrorStream()
+        (toIn, fromOut, fromErr, Right(process))
+      } else {
+        // create a new thread for the CLI test to run, using piped
+        // input/output streams to connected the thread and the CLItester
+        val in = new PipedInputStream()
+        val toIn = new PipedOutputStream(in)
+
+        val out = new PipedOutputStream()
+        val fromOut = new PipedInputStream(out)
+
+        val err = new PipedOutputStream()
+        val fromErr = new PipedInputStream(err)
+
+        if (debug) System.out.println("daffodil " + args.mkString(" "))
+
+        val thread = new CLIThread(args, in, out, err)
+        thread.start()
+        (toIn, fromOut, fromErr, Left(thread))
+      }
+
+    val eb = new ExpectBuilder()
+    eb.withOutput(toIn)
+    eb.withInputs(fromOut, fromErr)
+    eb.withInputFilters(replaceInString("\r\n", "\n"))
+    eb.withTimeout(timeout, TimeUnit.SECONDS)
+    eb.withExceptionOnFailure()
+    if (debug) {
+      eb.withEchoOutput(System.out)
+      eb.withEchoInput(System.out)
+    }
+    val expect = eb.build()
+    val tester = new CLITester(expect, toIn)
+
+    try {
+      testFunc(tester)
+    } finally {
+      threadOrProc match {
+        case Left(thread) => thread.join(timeout * 1000)
+        case Right(process) => process.waitFor(timeout, TimeUnit.SECONDS)
+      }
+      expect.close()
+      toIn.close()
+      fromOut.close()
+      fromErr.close()
     }
-  }
 
-  def makeMultipleCmds(cmds: Array[String]): String = {
-    if (isWindows) {
-      cmds.mkString(" & ")
-    } else {
-      cmds.mkString("; ")
+    val actualExitCode = threadOrProc match {
+      case Left(thread) => thread.exitCode
+      case Right(process) => ExitCode(process.exitValue)
     }
+    assertEquals("Incorrect exit code,", expectedExitCode, actualExitCode)
   }
 
-  def md5sum(blob_path: String): String = {
-    if (isWindows) {
-      String.format("certutil -hashfile %s MD5", blob_path)
-    } else {
-      String.format("md5sum %s", blob_path)
+  /**
+   * A class to run the CLI in a thread instead of a new process, given the
+   * arguments to use (excluded the daffodil binary) and streams to use for
+   * stdin/out/err.
+   */
+  private class CLIThread(args: Array[String], in: InputStream, out: OutputStream, err: OutputStream) extends Thread {
+    var exitCode = ExitCode.Failure
+
+    override def run(): Unit = {
+      val psOut = new PrintStream(out)
+      val psErr = new PrintStream(err)
+
+      // configure the CLI and log4j to use our custom streams, nothing should
+      // not actually use stdin/stdout/stderr
+      Main.setInputOutput(in, psOut, psErr)
+      configureLog4j(psErr)
+
+      exitCode = Main.run(args)
     }
-  }
 
-  def rmdir(path: String): String = {
-    if (Util.isWindows)
-      String.format("rmdir /Q /S %s", path)
-    else
-      String.format("rm -rf %s", path)
+    /**
+     * By default log4j outputs to stderr. This changes that so it writes to a
+     * provided PrintStream which is connected to the CLITester, allowing tests
+     * to expect contain written by log4j. This also defines the same pattern
+     * used by the CLI--that must be defined here because log4j configuration
+     * is normally given using a config file specified in the CLI wrapper
+     * script, which this does not use since it calls Main.run directly

Review Comment:
   The path to the config file is daffodil-cli/src/conf/log4j2.xml.  I think the real reason why you don't wrap it in a ConfigurationSource and pass it to ConfigurationFactory.getConfiguration is because it's easier to duplicate the pattern than to mutate a built Configuration to target the given PrintStream instead of STDERR, yes?  Maybe reword the sentence as:
   
   --that must be duplicated here because the normal log4j config file `daffodil-cli/src/conf/log4j2.xml` targets stderr while we need to target the provided PrintStream passed here.



##########
daffodil-cli/src/it/scala/org/apache/daffodil/saving/TestCLISaveParser.scala:
##########
@@ -17,253 +17,143 @@
 
 package org.apache.daffodil.saving
 
-import org.junit.Assert._
 import org.junit.Test
-import org.junit.Before
-import org.junit.After
-import org.apache.daffodil.CLI.Util
-import java.io.File
-import net.sf.expectit.matcher.Matchers.contains
-import net.sf.expectit.matcher.Matchers.eof
+
+import org.apache.daffodil.CLI.Util._
 import org.apache.daffodil.Main.ExitCode
 
 class TestCLISaveParser {
 
-  val output1 = Util.getExpectedString("output1.txt")
-  val output4 = Util.getExpectedString("output4.txt")
-  val output6 = Util.getExpectedString("output6.txt")
-  val output12 = Util.getExpectedString("output12.txt")
-  val savedParserFile = new File("savedParser.xsd.bin")

Review Comment:
   This PR inlines these files' expected strings into the tests below and uses withTempFile { parser instead of savedParserFile.  Nice!



##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to
+   * true. Otherwise a new process is spawned.
+   *
+   * @param args arguments to pass to the CLI. This should not include the
+   *   daffodil binary
+   * @param classpaths sequence of paths to add to the classpath. If non-empty,
+   *   runs the CLI in a new process instead of a thread and will likely decrease
+   *   performance
+   * @param fork if true, forces the the CLI in a new process
+   * @param timeout how long to wait, in seconds, for the CLI to exit after the
+   *   testFunc has returned. Also how long to wait for individual expect
+   *   operations in the CLITester
+   * @param debug if true, prints arguments and classpath information to
+   *   stdout. Also echos all CLITester input and output to stdout.
+   * @param testFunc function to call to send input to the CLI and validate
+   *   output from CLI stdout/stderr.
+   * @param expectedExitCode the expected exit code of the CLI. In the actual
+   *   exit code does not match
+   *
+   * @throws AssertionError if the actual exit code does not match the expected exit code
+   * @throws ExpectIOException if the an CLITester expect validation operation fails
+   */
+  def runCLI
+    (args: Array[String], classpaths: Seq[Path] = Seq(), fork: Boolean = false, timeout: Int = 10, debug: Boolean = false)
+    (testFunc: (CLITester) => Unit)
+    (expectedExitCode: ExitCode.Value): Unit = {
+
+    val (toIn, fromOut, fromErr, threadOrProc: Either[CLIThread, Process]) =
+      if (classpaths.nonEmpty || fork) {
+        // spawn a new process to run Daffodil, needed if a custom classpath is
+        // defined or if the caller explicitly wants to fork
+        val processBuilder = new ProcessBuilder()
+
+        if (classpaths.nonEmpty) {
+          val classpath = classpaths.mkString(File.pathSeparator)
+          if (debug) System.out.println(s"DAFFODIL_CLASSPATH=$classpath")
+          processBuilder.environment().put("DAFFODIL_CLASSPATH", classpath)
+        }
+
+        val cmd = daffodilBinPath.toString +: args
+        if (debug) System.out.println(cmd.mkString(" "))
+        processBuilder.command(cmd.toList.asJava)
+
+        val process = processBuilder.start()
+
+        val toIn = process.getOutputStream()
+        val fromOut = process.getInputStream()
+        val fromErr = process.getErrorStream()
+        (toIn, fromOut, fromErr, Right(process))
+      } else {
+        // create a new thread for the CLI test to run, using piped
+        // input/output streams to connected the thread and the CLItester
+        val in = new PipedInputStream()
+        val toIn = new PipedOutputStream(in)
+
+        val out = new PipedOutputStream()
+        val fromOut = new PipedInputStream(out)
+
+        val err = new PipedOutputStream()
+        val fromErr = new PipedInputStream(err)
+
+        if (debug) System.out.println("daffodil " + args.mkString(" "))
+
+        val thread = new CLIThread(args, in, out, err)
+        thread.start()
+        (toIn, fromOut, fromErr, Left(thread))
+      }
+
+    val eb = new ExpectBuilder()
+    eb.withOutput(toIn)
+    eb.withInputs(fromOut, fromErr)
+    eb.withInputFilters(replaceInString("\r\n", "\n"))
+    eb.withTimeout(timeout, TimeUnit.SECONDS)
+    eb.withExceptionOnFailure()
+    if (debug) {
+      eb.withEchoOutput(System.out)
+      eb.withEchoInput(System.out)
+    }
+    val expect = eb.build()
+    val tester = new CLITester(expect, toIn)
+
+    try {
+      testFunc(tester)
+    } finally {
+      threadOrProc match {
+        case Left(thread) => thread.join(timeout * 1000)
+        case Right(process) => process.waitFor(timeout, TimeUnit.SECONDS)
+      }
+      expect.close()
+      toIn.close()
+      fromOut.close()
+      fromErr.close()
     }
-  }
 
-  def makeMultipleCmds(cmds: Array[String]): String = {
-    if (isWindows) {
-      cmds.mkString(" & ")
-    } else {
-      cmds.mkString("; ")
+    val actualExitCode = threadOrProc match {
+      case Left(thread) => thread.exitCode
+      case Right(process) => ExitCode(process.exitValue)
     }
+    assertEquals("Incorrect exit code,", expectedExitCode, actualExitCode)
   }
 
-  def md5sum(blob_path: String): String = {
-    if (isWindows) {
-      String.format("certutil -hashfile %s MD5", blob_path)
-    } else {
-      String.format("md5sum %s", blob_path)
+  /**
+   * A class to run the CLI in a thread instead of a new process, given the
+   * arguments to use (excluded the daffodil binary) and streams to use for
+   * stdin/out/err.
+   */
+  private class CLIThread(args: Array[String], in: InputStream, out: OutputStream, err: OutputStream) extends Thread {
+    var exitCode = ExitCode.Failure
+
+    override def run(): Unit = {
+      val psOut = new PrintStream(out)
+      val psErr = new PrintStream(err)
+
+      // configure the CLI and log4j to use our custom streams, nothing should
+      // not actually use stdin/stdout/stderr
+      Main.setInputOutput(in, psOut, psErr)
+      configureLog4j(psErr)
+
+      exitCode = Main.run(args)
     }
-  }
 
-  def rmdir(path: String): String = {
-    if (Util.isWindows)
-      String.format("rmdir /Q /S %s", path)
-    else
-      String.format("rm -rf %s", path)
+    /**
+     * By default log4j outputs to stderr. This changes that so it writes to a
+     * provided PrintStream which is connected to the CLITester, allowing tests
+     * to expect contain written by log4j. This also defines the same pattern
+     * used by the CLI--that must be defined here because log4j configuration
+     * is normally given using a config file specified in the CLI wrapper
+     * script, which this does not use since it calls Main.run directly
+     */
+    private def configureLog4j(ps: PrintStream): Unit = {
+      val config = new AbstractConfiguration(null, ConfigurationSource.NULL_SOURCE) {
+        override def doConfigure(): Unit = {
+          val appenderName = "DaffodilCli"
+
+          val layout = PatternLayout.newBuilder()
+            .withPattern("[%p{lowerCase=true}] %m%n")
+            .withConfiguration(this)
+            .build()
+
+          val appenderBuilder: OutputStreamAppender.Builder[_] = OutputStreamAppender.newBuilder()
+          appenderBuilder.setName(appenderName)
+          appenderBuilder.setLayout(layout)
+          appenderBuilder.setTarget(ps)
+          appenderBuilder.setConfiguration(this)
+          val appender = appenderBuilder.build()
+
+          val rootLogger = getRootLogger()
+          rootLogger.setLevel(Level.WARN);
+          rootLogger.addAppender(appender, null, null)
+        }
+      }
+      Configurator.reconfigure(config)
+    }
   }
 
-  def cat(str: String): String = {
-    if (isWindows) {
-      "type " + str
-    } else {
-      "cat " + str
+  /**
+   * Wrapper around Expect to make integration tests less verbose. It also
+   * supports closing the mimicked stdin input stream (via the closeInput()
+   * function or the inputDone parameter to the send*() functions), which is
+   * sometimes needed since Daffodil may need to receive an EOF before it can
+   * finish parsing.
+   */
+  private class CLITester(expect: Expect, toIn: OutputStream) {
+
+    /**
+     * Close stdin, triggering an EOF.
+     */
+    def closeInput(): Unit = { toIn.close() }
+
+    /**
+     * Write a string to stdin. This does not incluede trailing newline. If
+     * inputDone is true, close stdin afterwards.
+     */
+    def send(string: String, inputDone: Boolean = false): Unit = {
+      expect.send(string)
+      if (inputDone) closeInput()
     }
-  }
 
-  def newTempFile(filePrefix: String, fileSuffix: String, optFileContents: Option[String] = None): File = {
-    val inputFile = File.createTempFile(filePrefix, fileSuffix)
-    inputFile.deleteOnExit
-    if (optFileContents.nonEmpty) {
-      val contents = optFileContents.get
-      val pw = new PrintWriter(inputFile)
-      pw.write(contents)
-      pw.close
+    /**
+     * Write a string to stdin with a trailing newline. If inputDone is true,
+     * close stdin afterwards.
+     */
+    def sendLine(string: String, inputDone: Boolean = false): Unit = {
+      expect.sendLine(string)
+      if (inputDone) closeInput()
     }
-    inputFile
-  }
 
-  def expectExitCode(expectedExitCode: ExitCode.Value, shell: Expect): Unit = {
-    val expectedCode = expectedExitCode.id
+    /**
+     * Write an entire byte array to stdin. If inputDone is true, close stdin
+     * afterwards.
+     */
+    def sendBytes(bytes: Array[Byte], inputDone: Boolean = false): Unit = {
+      expect.sendBytes(bytes)
+      if (inputDone) closeInput()
+    }
 
-    val keyWord = "EXITCODE:"
+    /**
+     * Write a file to stdin. If inputDone is true, close stdin afterwards.
+     */
+    def sendFile(path: Path, inputDone: Boolean = false): Unit = {
+      val chunkSize = 8192
+      val buffer = new Array[Byte](chunkSize)
+      val stream = Files.newInputStream(path)
+      var read = 0
+      while ({read = stream.read(buffer); read} > 0) {
+        if (read == chunkSize) {
+          expect.sendBytes(buffer)
+        } else {
+          // The expect.sendBytes function does not have parameters to send a
+          // subset of an array, it just sends the whole array. So we need to
+          // trim it down to the actual read size and send that
+          val smaller = new Array[Byte](read)
+          buffer.copyToArray(smaller, 0, read)
+          expect.sendBytes(smaller)
+        }
+      }
+      if (inputDone) closeInput()
+    }
 
-    //Escaped characters ^| for windows and \\! for linux makes the echo outputs different text than the command,
-    //That way the expect function can tell the difference.
-    val exitCodeCmd = "echo " + keyWord + (if (Util.isWindows) "^|%errorlevel%" else "\\!$?")
-    val exitCodeExpectation = keyWord + (if (Util.isWindows) "|" else "!")
+    def expect(matcher: Matcher[_]): Result = expect.expect(matcher)
+    def expect(string: String): Result = expect.expect(contains(string))
 
-    shell.sendLine(exitCodeCmd)
-    shell.expect(contains(exitCodeExpectation))
+    def expectErr(matcher: Matcher[_]): Result = expect.expectIn(1, matcher)
+    def expectErr(string: String): Result = expect.expectIn(1, contains(string))
+  }
 
-    val sExitCode = shell.expect(contains("\n")).getBefore().trim()
-    val actualInt = Integer.parseInt(sExitCode)
+  /**
+   * Escapes a string that is expected to be a json string
+   */
+  def jsonEscape(string: String): String = {
+    val chars = JsonStringEncoder.getInstance().quoteAsString(string)
+    new String(chars)
+  }
 
-    if (actualInt != expectedCode) {
-      val expectedExitCodeName = expectedExitCode.toString
-      val actualExitCodeName = ExitCode.values.find { _.id == actualInt }.map { _.toString }.getOrElse("Unknown")
-      val failMessage = "Exit code %s expected (%s), but got %s (%s) instead.".format(
-        expectedCode, expectedExitCodeName, actualInt, actualExitCodeName)
-      fail(failMessage)
+  /**
+   * This "args" string interpoloator makes it easy to create an Array[String]
+   * used for CLI arguments. Only spaces in the "format" string are split on.
+   * Spaces in an expressions in the format string are not split. For example
+   *
+   *   args"parse -s $schema $input".split(" ")
+   *
+   * Becomes someething like this:
+   *
+   *   Array("parse", "-s", "path/to/schema.dfdl.xsd", "path/to/input.bin")
+   *
+   * An alternative approach one might choose by using existing interpolators
+   * is something like this:
+   *
+   *   s"parse -s $schema $input".split(" ")
+   *
+   * This issue with this approach is that if the $schema or $input variables
+   * evaluate to something with string (which is not uncommon on some windows
+   * systems), then we end up splitting those files paths into separate
+   * arguments. This args interpolator ensures we don't split spaces that come
+   * from expressions.
+   *
+   * Note that quotes cannot be used to prevent splitting. For example, this
+   *
+   *   args"quotes do 'not prevent' splitting"
+   *
+   * Results in the following:
+   *
+   *   Array("quotes", "do", "'not", "prevent'", "splitting")
+   *
+   * To prevent splitting on a particular space, then expressions can be used,
+   * for example:
+   *
+   *   args"this ${"is split"} correctly"
+   *
+   * Which results in the following:
+   *
+   *   Array("this", "is split", "correctly")
+   *
+   * Note that this also handles concatenating expression correctly, for
+   * example:
+   *
+   *   args"some --arg=$root/$value"
+   *
+   * Results in
+   *
+   *   Array("some", "--arg=the/result/of/root/and/value")
+   *
+   */
+  implicit class ArgsHelper(val sc: StringContext) extends AnyVal {
+    def args(exprs: Any*): Array[String] = {
+      val strings = sc.parts.iterator
+      val expressions = exprs.iterator
+      val buf = mutable.ArrayBuffer[String]()
+
+      // regex to split on spaces, but using positive lookahead and lookbehind
+      // so the spaces aren't discard and end up in the array themselves. For
+      // example, using this regular expression to split this string:
+      //
+      //   "parse --schema foo.xsd input"
+      //
+      // Results in the following:
+      //
+      //   Array("parse", " ", "--schema", "foo.xsd", " ", "input")
+      //
+      // This is necessary so that when after we build the buf array, the
+      // spaces provide extra information about when to concatenate expression
+      // with he string contexts.

Review Comment:
   expression -> expressions
   he -> the



##########
daffodil-cli/src/it/scala/org/apache/daffodil/listing/TestCLIListing.scala:
##########
@@ -1,96 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.daffodil.listing
-
-import org.junit.Test
-import org.apache.daffodil.CLI.Util
-import net.sf.expectit.matcher.Matchers.contains
-import net.sf.expectit.matcher.Matchers.eof
-import org.apache.daffodil.Main.ExitCode
-
-class TestCLIlisting {
-
-  @Test def test_992_CLI_Executing_Listing_singleTestList(): Unit = {

Review Comment:
   This PR removes TestCLIlisting.scala and moves its two tests to TestCLItdml.scala too.



##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to
+   * true. Otherwise a new process is spawned.
+   *
+   * @param args arguments to pass to the CLI. This should not include the
+   *   daffodil binary
+   * @param classpaths sequence of paths to add to the classpath. If non-empty,
+   *   runs the CLI in a new process instead of a thread and will likely decrease
+   *   performance
+   * @param fork if true, forces the the CLI in a new process
+   * @param timeout how long to wait, in seconds, for the CLI to exit after the
+   *   testFunc has returned. Also how long to wait for individual expect
+   *   operations in the CLITester
+   * @param debug if true, prints arguments and classpath information to
+   *   stdout. Also echos all CLITester input and output to stdout.
+   * @param testFunc function to call to send input to the CLI and validate
+   *   output from CLI stdout/stderr.
+   * @param expectedExitCode the expected exit code of the CLI. In the actual
+   *   exit code does not match
+   *
+   * @throws AssertionError if the actual exit code does not match the expected exit code
+   * @throws ExpectIOException if the an CLITester expect validation operation fails
+   */
+  def runCLI
+    (args: Array[String], classpaths: Seq[Path] = Seq(), fork: Boolean = false, timeout: Int = 10, debug: Boolean = false)
+    (testFunc: (CLITester) => Unit)
+    (expectedExitCode: ExitCode.Value): Unit = {
+
+    val (toIn, fromOut, fromErr, threadOrProc: Either[CLIThread, Process]) =
+      if (classpaths.nonEmpty || fork) {
+        // spawn a new process to run Daffodil, needed if a custom classpath is
+        // defined or if the caller explicitly wants to fork
+        val processBuilder = new ProcessBuilder()
+
+        if (classpaths.nonEmpty) {
+          val classpath = classpaths.mkString(File.pathSeparator)
+          if (debug) System.out.println(s"DAFFODIL_CLASSPATH=$classpath")
+          processBuilder.environment().put("DAFFODIL_CLASSPATH", classpath)
+        }
+
+        val cmd = daffodilBinPath.toString +: args
+        if (debug) System.out.println(cmd.mkString(" "))
+        processBuilder.command(cmd.toList.asJava)
+
+        val process = processBuilder.start()
+
+        val toIn = process.getOutputStream()
+        val fromOut = process.getInputStream()
+        val fromErr = process.getErrorStream()
+        (toIn, fromOut, fromErr, Right(process))
+      } else {
+        // create a new thread for the CLI test to run, using piped
+        // input/output streams to connected the thread and the CLItester
+        val in = new PipedInputStream()
+        val toIn = new PipedOutputStream(in)
+
+        val out = new PipedOutputStream()
+        val fromOut = new PipedInputStream(out)
+
+        val err = new PipedOutputStream()
+        val fromErr = new PipedInputStream(err)
+
+        if (debug) System.out.println("daffodil " + args.mkString(" "))
+
+        val thread = new CLIThread(args, in, out, err)
+        thread.start()
+        (toIn, fromOut, fromErr, Left(thread))
+      }
+
+    val eb = new ExpectBuilder()
+    eb.withOutput(toIn)
+    eb.withInputs(fromOut, fromErr)
+    eb.withInputFilters(replaceInString("\r\n", "\n"))
+    eb.withTimeout(timeout, TimeUnit.SECONDS)
+    eb.withExceptionOnFailure()
+    if (debug) {
+      eb.withEchoOutput(System.out)
+      eb.withEchoInput(System.out)
+    }
+    val expect = eb.build()
+    val tester = new CLITester(expect, toIn)
+
+    try {
+      testFunc(tester)
+    } finally {
+      threadOrProc match {
+        case Left(thread) => thread.join(timeout * 1000)
+        case Right(process) => process.waitFor(timeout, TimeUnit.SECONDS)
+      }
+      expect.close()
+      toIn.close()
+      fromOut.close()
+      fromErr.close()
     }
-  }
 
-  def makeMultipleCmds(cmds: Array[String]): String = {
-    if (isWindows) {
-      cmds.mkString(" & ")
-    } else {
-      cmds.mkString("; ")
+    val actualExitCode = threadOrProc match {
+      case Left(thread) => thread.exitCode
+      case Right(process) => ExitCode(process.exitValue)
     }
+    assertEquals("Incorrect exit code,", expectedExitCode, actualExitCode)
   }
 
-  def md5sum(blob_path: String): String = {
-    if (isWindows) {
-      String.format("certutil -hashfile %s MD5", blob_path)
-    } else {
-      String.format("md5sum %s", blob_path)
+  /**
+   * A class to run the CLI in a thread instead of a new process, given the
+   * arguments to use (excluded the daffodil binary) and streams to use for
+   * stdin/out/err.
+   */
+  private class CLIThread(args: Array[String], in: InputStream, out: OutputStream, err: OutputStream) extends Thread {
+    var exitCode = ExitCode.Failure
+
+    override def run(): Unit = {
+      val psOut = new PrintStream(out)
+      val psErr = new PrintStream(err)
+
+      // configure the CLI and log4j to use our custom streams, nothing should
+      // not actually use stdin/stdout/stderr
+      Main.setInputOutput(in, psOut, psErr)
+      configureLog4j(psErr)
+
+      exitCode = Main.run(args)
     }
-  }
 
-  def rmdir(path: String): String = {
-    if (Util.isWindows)
-      String.format("rmdir /Q /S %s", path)
-    else
-      String.format("rm -rf %s", path)
+    /**
+     * By default log4j outputs to stderr. This changes that so it writes to a
+     * provided PrintStream which is connected to the CLITester, allowing tests
+     * to expect contain written by log4j. This also defines the same pattern
+     * used by the CLI--that must be defined here because log4j configuration
+     * is normally given using a config file specified in the CLI wrapper
+     * script, which this does not use since it calls Main.run directly
+     */
+    private def configureLog4j(ps: PrintStream): Unit = {
+      val config = new AbstractConfiguration(null, ConfigurationSource.NULL_SOURCE) {
+        override def doConfigure(): Unit = {
+          val appenderName = "DaffodilCli"
+
+          val layout = PatternLayout.newBuilder()
+            .withPattern("[%p{lowerCase=true}] %m%n")
+            .withConfiguration(this)
+            .build()
+
+          val appenderBuilder: OutputStreamAppender.Builder[_] = OutputStreamAppender.newBuilder()
+          appenderBuilder.setName(appenderName)
+          appenderBuilder.setLayout(layout)
+          appenderBuilder.setTarget(ps)
+          appenderBuilder.setConfiguration(this)
+          val appender = appenderBuilder.build()
+
+          val rootLogger = getRootLogger()
+          rootLogger.setLevel(Level.WARN);
+          rootLogger.addAppender(appender, null, null)
+        }
+      }
+      Configurator.reconfigure(config)
+    }
   }
 
-  def cat(str: String): String = {
-    if (isWindows) {
-      "type " + str
-    } else {
-      "cat " + str
+  /**
+   * Wrapper around Expect to make integration tests less verbose. It also
+   * supports closing the mimicked stdin input stream (via the closeInput()
+   * function or the inputDone parameter to the send*() functions), which is
+   * sometimes needed since Daffodil may need to receive an EOF before it can
+   * finish parsing.
+   */
+  private class CLITester(expect: Expect, toIn: OutputStream) {
+
+    /**
+     * Close stdin, triggering an EOF.
+     */
+    def closeInput(): Unit = { toIn.close() }
+
+    /**
+     * Write a string to stdin. This does not incluede trailing newline. If
+     * inputDone is true, close stdin afterwards.
+     */
+    def send(string: String, inputDone: Boolean = false): Unit = {
+      expect.send(string)
+      if (inputDone) closeInput()
     }
-  }
 
-  def newTempFile(filePrefix: String, fileSuffix: String, optFileContents: Option[String] = None): File = {
-    val inputFile = File.createTempFile(filePrefix, fileSuffix)
-    inputFile.deleteOnExit
-    if (optFileContents.nonEmpty) {
-      val contents = optFileContents.get
-      val pw = new PrintWriter(inputFile)
-      pw.write(contents)
-      pw.close
+    /**
+     * Write a string to stdin with a trailing newline. If inputDone is true,
+     * close stdin afterwards.
+     */
+    def sendLine(string: String, inputDone: Boolean = false): Unit = {
+      expect.sendLine(string)
+      if (inputDone) closeInput()
     }
-    inputFile
-  }
 
-  def expectExitCode(expectedExitCode: ExitCode.Value, shell: Expect): Unit = {
-    val expectedCode = expectedExitCode.id
+    /**
+     * Write an entire byte array to stdin. If inputDone is true, close stdin
+     * afterwards.
+     */
+    def sendBytes(bytes: Array[Byte], inputDone: Boolean = false): Unit = {
+      expect.sendBytes(bytes)
+      if (inputDone) closeInput()
+    }
 
-    val keyWord = "EXITCODE:"
+    /**
+     * Write a file to stdin. If inputDone is true, close stdin afterwards.
+     */
+    def sendFile(path: Path, inputDone: Boolean = false): Unit = {
+      val chunkSize = 8192
+      val buffer = new Array[Byte](chunkSize)
+      val stream = Files.newInputStream(path)
+      var read = 0
+      while ({read = stream.read(buffer); read} > 0) {
+        if (read == chunkSize) {
+          expect.sendBytes(buffer)
+        } else {
+          // The expect.sendBytes function does not have parameters to send a
+          // subset of an array, it just sends the whole array. So we need to
+          // trim it down to the actual read size and send that
+          val smaller = new Array[Byte](read)
+          buffer.copyToArray(smaller, 0, read)
+          expect.sendBytes(smaller)
+        }
+      }
+      if (inputDone) closeInput()
+    }
 
-    //Escaped characters ^| for windows and \\! for linux makes the echo outputs different text than the command,
-    //That way the expect function can tell the difference.
-    val exitCodeCmd = "echo " + keyWord + (if (Util.isWindows) "^|%errorlevel%" else "\\!$?")
-    val exitCodeExpectation = keyWord + (if (Util.isWindows) "|" else "!")
+    def expect(matcher: Matcher[_]): Result = expect.expect(matcher)
+    def expect(string: String): Result = expect.expect(contains(string))
 
-    shell.sendLine(exitCodeCmd)
-    shell.expect(contains(exitCodeExpectation))
+    def expectErr(matcher: Matcher[_]): Result = expect.expectIn(1, matcher)
+    def expectErr(string: String): Result = expect.expectIn(1, contains(string))
+  }
 
-    val sExitCode = shell.expect(contains("\n")).getBefore().trim()
-    val actualInt = Integer.parseInt(sExitCode)
+  /**
+   * Escapes a string that is expected to be a json string
+   */
+  def jsonEscape(string: String): String = {
+    val chars = JsonStringEncoder.getInstance().quoteAsString(string)
+    new String(chars)
+  }
 
-    if (actualInt != expectedCode) {
-      val expectedExitCodeName = expectedExitCode.toString
-      val actualExitCodeName = ExitCode.values.find { _.id == actualInt }.map { _.toString }.getOrElse("Unknown")
-      val failMessage = "Exit code %s expected (%s), but got %s (%s) instead.".format(
-        expectedCode, expectedExitCodeName, actualInt, actualExitCodeName)
-      fail(failMessage)
+  /**
+   * This "args" string interpoloator makes it easy to create an Array[String]
+   * used for CLI arguments. Only spaces in the "format" string are split on.
+   * Spaces in an expressions in the format string are not split. For example
+   *
+   *   args"parse -s $schema $input".split(" ")
+   *
+   * Becomes someething like this:
+   *
+   *   Array("parse", "-s", "path/to/schema.dfdl.xsd", "path/to/input.bin")
+   *
+   * An alternative approach one might choose by using existing interpolators
+   * is something like this:
+   *
+   *   s"parse -s $schema $input".split(" ")
+   *
+   * This issue with this approach is that if the $schema or $input variables
+   * evaluate to something with string (which is not uncommon on some windows
+   * systems), then we end up splitting those files paths into separate
+   * arguments. This args interpolator ensures we don't split spaces that come
+   * from expressions.
+   *
+   * Note that quotes cannot be used to prevent splitting. For example, this
+   *
+   *   args"quotes do 'not prevent' splitting"
+   *
+   * Results in the following:
+   *
+   *   Array("quotes", "do", "'not", "prevent'", "splitting")
+   *
+   * To prevent splitting on a particular space, then expressions can be used,
+   * for example:
+   *
+   *   args"this ${"is split"} correctly"
+   *
+   * Which results in the following:
+   *
+   *   Array("this", "is split", "correctly")
+   *
+   * Note that this also handles concatenating expression correctly, for
+   * example:
+   *
+   *   args"some --arg=$root/$value"
+   *
+   * Results in
+   *
+   *   Array("some", "--arg=the/result/of/root/and/value")
+   *
+   */
+  implicit class ArgsHelper(val sc: StringContext) extends AnyVal {
+    def args(exprs: Any*): Array[String] = {
+      val strings = sc.parts.iterator
+      val expressions = exprs.iterator
+      val buf = mutable.ArrayBuffer[String]()
+
+      // regex to split on spaces, but using positive lookahead and lookbehind
+      // so the spaces aren't discard and end up in the array themselves. For

Review Comment:
   -> so the spaces end up in the array themselves instead of being discarded. 



##########
project/Rat.scala:
##########
@@ -39,22 +39,6 @@ object Rat {
     file("daffodil-runtime2/src/test/c/examples"),
 
     // test files that cannot include the Apache license without breaking tests
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/982"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1326"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1328"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1329"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1330"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1331"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1333"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1334"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1337"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1338"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1339"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1340"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1382"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1591"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1602"),
-    file("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/debugger/1863"),

Review Comment:
   Does the PR delete these files because their contents had already been inlined as input strings into the corresponding CLI tests in an earlier PR?  Nothing seems to read or use these files even before this PR was created, so it's time to remove them.



##########
daffodil-cli/src/it/scala/org/apache/daffodil/saving/TestCLISaveParser.scala:
##########
@@ -272,173 +162,108 @@ class TestCLISaveParser {
    * compiling. They are a runtime-thing only.
    */
   @Test def test_3508_CLI_Saving_SaveParser_extVars(): Unit = {
-
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section07/external_variables/external_variables.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) (Util.cmdConvert(schemaFile)) else (schemaFile)
-
-    val savedParser = "test_3508.xsd.bin"
-    val parserFile = new File(savedParser)
-
-    val shell = Util.start("")
-
-    try {
-      var cmd = String.format("%s -v save-parser -s %s -r row2  %s", Util.binPath, testSchemaFile, savedParser)
-      shell.sendLine(cmd)
-      shell.expectIn(1, (contains("[info] Time (saving)")))
-      assertTrue("save-parser failed", parserFile.exists())
-
-      shell.sendLine();
-      cmd = String.format("""echo 0| %s parse --parser %s -D"{http://example.com}var1=25" "{http://example.com}var3=7" """, Util.binPath, savedParser)
-      shell.sendLine(cmd)
-      shell.expect(contains("<tns:row2 xmlns:tns=\"http://example.com\">"))
-      shell.expect(contains("<cell>25</cell>"))
-      shell.expect(contains("<cell>7</cell>"))
-
-      Util.expectExitCode(ExitCode.LeftOverData, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-      if (parserFile.exists()) parserFile.delete()
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section07/external_variables/external_variables.dfdl.xsd")
+
+    withTempFile { parser =>
+      runCLI(args"save-parser -s $schema -r row2 $parser") { cli =>
+      } (ExitCode.Success)
+
+      runCLI(args"parse --parser $parser -D{http://example.com}var1=25 {http://example.com}var3=7") { cli =>
+        cli.sendLine("0", inputDone = true)
+        cli.expect("<tns:row2 xmlns:tns=\"http://example.com\">")
+        cli.expect("<cell>25</cell>")
+        cli.expect("<cell>7</cell>")
+      } (ExitCode.LeftOverData)
     }
   }
 
-  // See DFDL-1147
-  /*@Test def test_3063_CLI_Saving_SaveParser_validate() {
-
-    val cmd = Util.binPath + " save-parser --validate on -s daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd -r validation_check savedParser.xml\n"
-    shell.send(cmd)
-
-    var cmd2 = "echo -ne 'test'| " + Util.binPath + " parse --parser savedParser.xml \n"
-    shell.send(cmd2)
-    shell.expect(contains("[warn] Validation Error: validation_check: cvc-pattern-valid"))
-    shell.expect(contains("[warn] Validation Error: validation_check failed"))
-
-    cmd = Util.binPath + " save-parser --validate -s daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd -r validation_check savedParser.xml\n"
-    shell.send(cmd)
+  @Test def test_3063_CLI_Saving_SaveParser_validate(): Unit = {
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd")
 
-    cmd2 = "echo -ne 'test'| " + Util.binPath + " parse --parser savedParser.xml \n"
-    shell.send(cmd2)
-    shell.expect(contains("[warn] Validation Error: validation_check: cvc-pattern-valid"))
-    shell.expect(contains("[warn] Validation Error: validation_check failed"))
+    withTempFile { parser =>
+      runCLI(args"save-parser --validate on -s $schema -r validation_check $parser") { cli =>
+        cli.expectErr("Unknown option 'validate'")
+      } (ExitCode.Usage)
 
-    cmd = Util.binPath + " save-parser --validate limited -s daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd -r validation_check savedParser.xml\n"
-    shell.send(cmd)
+      runCLI(args"save-parser -s $schema -r validation_check $parser") { cli =>
+      } (ExitCode.Success)
 
-    cmd2 = "echo -ne 'test'| " + Util.binPath + " parse --parser savedParser.xml \n"
-    shell.send(cmd2)
-    shell.expect(contains("[warn] Validation Error: validation_check failed"))
+      runCLI(args"parse --validate limited -P $parser") { cli =>
+        cli.send("test", inputDone = true)
+        cli.expectErr("[error] Validation Error")
+        cli.expectErr("ex:validation_check failed")
+        cli.expectErr("[0-8]+")
+      } (ExitCode.ParseError)
 
-    cmd = Util.binPath + " save-parser --validate off -s daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd -r validation_check savedParser.xml\n"
-    shell.send(cmd)
+      runCLI(args"parse --validate on -P $parser") { cli =>
+        cli.send("test", inputDone = true)
+        cli.expectErr("validation mode must be 'limited' or 'off' when using a saved parser.")
+      } (ExitCode.Usage)
+    }
+  }
 
-    cmd2 = "echo -ne 'test'| " + Util.binPath + " parse --parser savedParser.xml \n"
-    shell.send(cmd2)
+  // DAFFODIL-1141
+  /*@Test*/ def test_3036_CLI_Saving_SaveParser_debug(): Unit = {
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-    shell.send("exit\n")
-    shell.expect(eof())
-    shell.close()
-  }*/
+    withTempFile { parser =>
+      runCLI(args"-d save-parser -s $schema -r matrix $parser") { cli =>
+        cli.expectErr("Some error about -d not being valid with save-parser")
+      } (ExitCode.Usage)
+    }
+  }
 
-  // See DFDL-1141
-  /*@Test def test_3036_CLI_Saving_SaveParser_debug() {
+  // DAFFODIL-1141
+  /*@Test*/ def test_3037_CLI_Saving_SaveParser_trace(): Unit = {

Review Comment:
   This PR uncomments the test's body and keeps only the `@Test` commented out.  Nice, this will keep the test compiling even after changes to functions.



##########
daffodil-cli/src/it/scala/org/apache/daffodil/parsing/TestCLIParsing.scala:
##########
@@ -17,246 +17,134 @@
 
 package org.apache.daffodil.parsing
 
+import java.nio.charset.StandardCharsets.UTF_8
+
+import org.apache.commons.io.FileUtils
+
 import org.junit.Assert._
 import org.junit.Test
 
-import java.io.File
-import org.apache.daffodil.CLI.Util
-import net.sf.expectit.matcher.Matchers.contains
-import net.sf.expectit.matcher.Matchers.eof
+import org.apache.daffodil.CLI.Util._
 import org.apache.daffodil.Main.ExitCode
 
 class TestCLIparsing {
 
-  val output1 = Util.getExpectedString("output1.txt")
-  val output1_nopretty = Util.getExpectedString("output1_nopretty.txt")
-  val output2 = Util.getExpectedString("output2.txt")
-  val output4 = Util.getExpectedString("output4.txt")
-  val output6 = Util.getExpectedString("output6.txt")
-  val output8 = Util.getExpectedString("output8.txt")
-  val output9 = Util.getExpectedString("output9.txt")
-  val output10 = Util.getExpectedString("output10.txt")
-  val output12 = Util.getExpectedString("output12.txt")

Review Comment:
   This PR removes these output files and inlines the expected strings into the tests below instead of getting the expected strings from these files.  I agree that this makes the tests easier to understand.



##########
daffodil-cli/src/it/scala/org/apache/daffodil/debugger/TestCLIDebugger.scala:
##########
@@ -19,1477 +19,1108 @@ package org.apache.daffodil.debugger
 
 import org.junit.Test
 
-import net.sf.expectit.matcher.Matchers.allOf
-import net.sf.expectit.matcher.Matchers.contains
+import java.nio.file.Files
+import java.nio.charset.StandardCharsets.UTF_8
+
 import net.sf.expectit.matcher.Matchers.regexp
-import net.sf.expectit.matcher.Matchers.times
-import org.apache.daffodil.Main.ExitCode
 
-import org.apache.daffodil.CLI.Util
+import org.apache.daffodil.CLI.Util._
+import org.apache.daffodil.Main.ExitCode
 
 class TestCLIdebugger {
 
-  val DAFFODIL_JAVA_OPTS = Map("DAFFODIL_JAVA_OPTS" -> "-Xms256m -Xmx2048m -Dfile.encoding=UTF-8")
-
   @Test def test_3385_CLI_Debugger_invalidExpressions(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
 
-      shell.expect(contains("(debug)"))
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-      shell.sendLine("eval (/invalid)")
-      shell.expect(contains("error: expression evaluation failed: Schema Definition Error:"))
-      shell.expect(contains("(debug)"))
+      cli.sendLine("eval (/invalid)")
+      cli.expect("error: expression evaluation failed: Schema Definition Error:")
+      cli.expect("(debug)")
 
-      shell.sendLine("eval (func())")
-      shell.expect(contains("error: expression evaluation failed: Schema Definition Error: Unsupported function:"))
-      shell.expect(contains("(debug)"))
+      cli.sendLine("eval (func())")
+      cli.expect("error: expression evaluation failed: Schema Definition Error: Unsupported function:")
+      cli.expect("(debug)")
 
-      shell.sendLine("eval (/invalid!)")
-      shell.expect(contains("error: expression evaluation failed: Schema Definition Error:"))
-      shell.expect(contains("(debug)"))
+      cli.sendLine("eval (/invalid!)")
+      cli.expect("error: expression evaluation failed: Schema Definition Error:")
+      cli.expect("(debug)")
 
-      shell.sendLine("eval (!)")
-      shell.expect(contains("error: expression evaluation failed: Schema Definition Error:"))
-      shell.expect(contains("(debug)"))
+      cli.sendLine("eval (!)")
+      cli.expect("error: expression evaluation failed: Schema Definition Error:")
+      cli.expect("(debug)")
 
-      shell.sendLine("eval (././.\\/)")
-      shell.expect(contains("error: expression evaluation failed: Schema Definition Error:"))
-      shell.expect(contains("(debug)"))
+      cli.sendLine("eval (././.\\/)")
+      cli.expect("error: expression evaluation failed: Schema Definition Error:")
+      cli.expect("(debug)")
 
-      shell.sendLine("quit")
-      Util.expectExitCode(ExitCode.Failure, shell)
-    } finally {
-      shell.close()
-    }
+      cli.sendLine("quit")
+    } (ExitCode.Failure)
   }
 
   @Test def test_1591_CLI_Debugger_invalidCommandError(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
-      shell.sendLine("garbage")
-      shell.expect(contains("error: undefined command: garbage"))
-      shell.sendLine("quit")
-      Util.expectExitCode(ExitCode.Failure, shell)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
+
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
+      cli.sendLine("garbage")
+      cli.expect("error: undefined command: garbage")
+      cli.sendLine("quit")
+    } (ExitCode.Failure)
   }
 
   @Test def test_1335_CLI_Debugger_dataAndWrapLength(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("debug"))
-
-      shell.sendLine("info data")
-      shell.expect(contains("0~,~1~,~2~,~3~,~4~,~5~,~6~"))
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
 
-      //      shell.sendLine("set dataLength 5")
-      //      shell.sendLine("info data")
-      //      shell.expect(contains("0,1,2"))
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("debug")
 
-      shell.sendLine("set dataLength -938")
-      shell.sendLine("info data")
-      shell.expect(contains("0~,~1~,~2~,~3~,~4~,~5~,~6~"))
+      cli.sendLine("info data")
+      cli.expect("0~,~1~,~2~,~3~,~4~,~5~,~6~")
 
-      //      shell.sendLine("set wrapLength 2")
-      //      shell.sendLine("info data")
-      //      shell.expect(contains("0,\n    1,\n    2,\n    3,\n    4,\n    5,\n    6\n"))
+      cli.sendLine("set dataLength -938")
+      cli.sendLine("info data")
+      cli.expect("0~,~1~,~2~,~3~,~4~,~5~,~6~")
 
-      shell.sendLine("continue")
+      cli.sendLine("continue")
 
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_982_CLI_Debugger_simpleDebugger(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
-      shell.sendLine("continue")
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
+      cli.sendLine("continue")
 
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_1326_CLI_Debugger_displaysTesting(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
+      cli.sendLine("display eval (.)")
+      cli.sendLine("step")
+      cli.expect("matrix")
 
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      cli.sendLine("info displays")
+      cli.expect("1: eval (.)")
 
-      shell.sendLine("display eval (.)")
-      shell.sendLine("step")
-      shell.expect(contains("matrix"))
+      cli.sendLine("disable display 1")
+      cli.sendLine("info displays")
+      cli.expect("1*: eval (.)")
+      cli.sendLine("step")
+      cli.sendLine("enable display 1")
 
-      shell.sendLine("info displays")
-      shell.expect(contains("1: eval (.)"))
+      cli.sendLine("step")
+      cli.expect("</tns:cell>")
 
-      shell.sendLine("disable display 1")
-      shell.sendLine("info displays")
-      shell.expect(contains("1*: eval (.)"))
-      shell.sendLine("step")
-      shell.sendLine("enable display 1")
+      cli.sendLine("delete display 1")
+      cli.sendLine("step")
 
-      shell.sendLine("step")
-      shell.expect(contains("</tns:cell>"))
+      cli.sendLine("enable display 1")
+      cli.expect("error: 1 is not a valid display id")
 
-      shell.sendLine("delete display 1")
-      shell.sendLine("step")
+      cli.sendLine("continue")
+      cli.expect("matrix")
 
-      shell.sendLine("enable display 1")
-      shell.expect(contains("error: 1 is not a valid display id"))
-
-      shell.sendLine("continue")
-      shell.expect(contains("matrix"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_1339_CLI_Debugger_removeHidden(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input6.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d parse -s %s -r e %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-
-      shell.expect(contains("(debug)"))
-      shell.sendLine("set removeHidden false")
-      shell.sendLine("display info infoset")
-      shell.sendLine("step")
-      shell.sendLine("step")
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input6.txt")
+
+    runCLI(args"-d parse -s $schema -r e $input") { cli =>
+      cli.expect("(debug)")
+      cli.sendLine("set removeHidden false")
+      cli.sendLine("display info infoset")
+      cli.sendLine("step")
+      cli.sendLine("step")
       // intentionally look for a newline to make sure normally hidden elements
       // are output with a trailing newline when the debugger displays them
-      shell.expect(contains("<sneaky></sneaky>\n"))
-      shell.sendLine("break g")
-      shell.sendLine("continue")
-      shell.expect(contains("<sneaky>5</sneaky>\n"))
-      shell.sendLine("quit")
-      Util.expectExitCode(ExitCode.Failure, shell)
-    } finally {
-      shell.close()
-    }
+      cli.expect("<sneaky></sneaky>\n")
+      cli.sendLine("break g")
+      cli.sendLine("continue")
+      cli.expect("<sneaky>5</sneaky>\n")
+      cli.sendLine("quit")
+    } (ExitCode.Failure)
   }
 
   @Test def test_3268_CLI_Debugger_removeHidden2(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input6.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d parse -s %s -r e %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-
-      shell.expect(contains("(debug)"))
-      shell.sendLine("set removeHidden false")
-      shell.sendLine("display info infoset")
-      shell.sendLine("break g")
-      shell.sendLine("continue")
-      shell.expect(contains("<sneaky>5</sneaky>"))
-      shell.sendLine("continue")
-      val result = shell.expect(contains("</ex:e>")).getBefore();
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input6.txt")
+
+    runCLI(args"-d parse -s $schema -r e $input") { cli =>
+      cli.expect("(debug)")
+      cli.sendLine("set removeHidden false")
+      cli.sendLine("display info infoset")
+      cli.sendLine("break g")
+      cli.sendLine("continue")
+      cli.expect("<sneaky>5</sneaky>")
+      cli.sendLine("continue")
+      val result = cli.expect("</ex:e>").getBefore();
       assert(!result.contains("sneaky"))
 
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_1331_CLI_Debugger_breakpointTesting4(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input3.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input3.txt")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      cli.sendLine("break cell")
+      cli.sendLine("break cell")
 
-      shell.sendLine("break cell")
-      shell.sendLine("break cell")
+      cli.sendLine("condition 1 dfdl:occursIndex() mod 2 eq 1")
+      cli.sendLine("condition 2 dfdl:occursIndex() mod 2 eq 0")
 
-      shell.sendLine("condition 1 dfdl:occursIndex() mod 2 eq 1")
-      shell.sendLine("condition 2 dfdl:occursIndex() mod 2 eq 0")
+      cli.sendLine("info breakpoints")
+      cli.expect("2: cell   { dfdl:occursIndex() mod 2 eq 0 }")
 
-      shell.sendLine("info breakpoints")
-      shell.expect(contains("2: cell   { dfdl:occursIndex() mod 2 eq 0 }"))
+      cli.sendLine("display info occursIndex")
 
-      shell.sendLine("display info occursIndex")
+      cli.sendLine("continue")
+      cli.expect("occursIndex: 1")
 
-      shell.sendLine("continue")
-      shell.expect(contains("occursIndex: 1"))
+      cli.sendLine("continue")
+      cli.expect("occursIndex: 2")
 
-      shell.sendLine("continue")
-      shell.expect(contains("occursIndex: 2"))
+      cli.sendLine("continue")
+      cli.expect("occursIndex: 3")
 
-      shell.sendLine("continue")
-      shell.expect(contains("occursIndex: 3"))
+      cli.sendLine("disable breakpoint 2")
 
-      shell.sendLine("disable breakpoint 2")
+      cli.sendLine("continue")
+      cli.expect("occursIndex: 5")
 
-      shell.sendLine("continue")
-      shell.expect(contains("occursIndex: 5"))
+      cli.sendLine("continue")
+      cli.expect("occursIndex: 7")
 
-      shell.sendLine("continue")
-      shell.expect(contains("occursIndex: 7"))
+      cli.sendLine("enable breakpoint 2")
 
-      shell.sendLine("enable breakpoint 2")
+      cli.sendLine("continue")
+      cli.expect("occursIndex: 8")
 
-      shell.sendLine("continue")
-      shell.expect(contains("occursIndex: 8"))
+      cli.sendLine("disable breakpoint 1")
+      cli.sendLine("disable breakpoint 2")
 
-      shell.sendLine("disable breakpoint 1")
-      shell.sendLine("disable breakpoint 2")
+      cli.sendLine("continue")
+      cli.expect("<tns:cell>3</tns:cell>")
 
-      shell.sendLine("continue")
-      shell.expect(contains("<tns:cell>3</tns:cell>"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_1463_CLI_Debugger_breakOnValueOfElement(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input3.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
-
-      shell.sendLine("set breakOnlyOnCreation false")
-      shell.expect(contains("(debug)"))
-
-      shell.sendLine("display info infoset")
-      shell.expect(contains("(debug)"))
-
-      shell.sendLine("break cell")
-      shell.expect(contains("1: cell"))
-      shell.sendLine("condition 1 xsd:string(.) eq '3'")
-      shell.expect(contains("1: cell   { xsd:string(.) eq '3' }"))
-
-      shell.sendLine("info breakpoints")
-      shell.expect(allOf(contains("breakpoints:"), contains("1: cell   { xsd:string(.) eq '3' }")))
-
-      shell.sendLine("continue")
-      shell.expect(contains("<tns:cell>3</tns:cell>"))
-      shell.expect(contains("</tns:row>"))
-      shell.expect(contains("</tns:matrix>"))
-      shell.sendLine("continue")
-      shell.expect(contains("<tns:cell>3</tns:cell>"))
-      shell.expect(contains("</tns:row>"))
-      shell.expect(contains("</tns:matrix>"))
-      shell.sendLine("continue")
-      shell.expect(contains("<tns:cell>3</tns:cell>"))
-      shell.expect(contains("</tns:row>"))
-      shell.expect(contains("</tns:matrix>"))
-
-      shell.sendLine("continue")
-      shell.expect(times(1, contains("<tns:cell>3</tns:cell>")))
-      shell.expect(contains("<tns:cell>3</tns:cell>"))
-      shell.expect(contains("</tns:row>"))
-      shell.expect(contains("</tns:matrix>"))
-      shell.sendLine("continue")
-      shell.expect(times(1, contains("<tns:cell>3</tns:cell>")))
-      shell.expect(contains("<tns:cell>3</tns:cell>"))
-      shell.expect(contains("</tns:row>"))
-      shell.expect(contains("</tns:matrix>"))
-
-      shell.sendLine("quit")
-      Util.expectExitCode(ExitCode.Failure, shell)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input3.txt")
+
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
+
+      cli.sendLine("set breakOnlyOnCreation false")
+      cli.expect("(debug)")
+
+      cli.sendLine("display info infoset")
+      cli.expect("(debug)")
+
+      cli.sendLine("break cell")
+      cli.expect("1: cell")
+      cli.sendLine("condition 1 xsd:string(.) eq '3'")
+      cli.expect("1: cell   { xsd:string(.) eq '3' }")
+
+      cli.sendLine("info breakpoints")
+      cli.expect("breakpoints:")
+      cli.expect("1: cell   { xsd:string(.) eq '3' }")
+
+      cli.sendLine("continue")
+      cli.expect("<tns:cell>3</tns:cell>")
+      cli.expect("</tns:row>")
+      cli.expect("</tns:matrix>")
+      cli.sendLine("continue")
+      cli.expect("<tns:cell>3</tns:cell>")
+      cli.expect("</tns:row>")
+      cli.expect("</tns:matrix>")
+      cli.sendLine("continue")
+      cli.expect("<tns:cell>3</tns:cell>")
+      cli.expect("</tns:row>")
+      cli.expect("</tns:matrix>")
+
+      cli.sendLine("continue")
+      cli.expect("<tns:cell>3</tns:cell>")
+      cli.expect("<tns:cell>3</tns:cell>")
+      cli.expect("</tns:row>")
+      cli.expect("</tns:matrix>")
+      cli.sendLine("continue")
+      cli.expect("<tns:cell>3</tns:cell>")
+      cli.expect("<tns:cell>3</tns:cell>")
+      cli.expect("</tns:row>")
+      cli.expect("</tns:matrix>")
+
+      cli.sendLine("quit")
+    } (ExitCode.Failure)
   }
 
   @Test def test_1338_CLI_Debugger_pointsOfUncertaintyInfo(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input5.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input5.txt")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    runCLI(args"-d parse -s $schema -r Item2 $input") { cli =>
+      cli.expect("(debug)")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r Item2 %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      cli.sendLine("display info pointsOfUncertainty")
 
-      shell.sendLine("display info pointsOfUncertainty")
+      cli.sendLine("step")
+      cli.expect("pointsOfUncertainty:")
+      cli.expect("(none)")
 
-      shell.sendLine("step")
-      shell.expect(contains("pointsOfUncertainty:"))
-      shell.expect(contains("(none)"))
+      cli.sendLine("step")
+      cli.expect("pointsOfUncertainty:")
+      cli.expect("bitPos: 0, context: choice[1]")
 
-      shell.sendLine("step")
-      shell.expect(contains("pointsOfUncertainty:"))
-      shell.expect(contains("bitPos: 0, context: choice[1]"))
+      cli.sendLine("step")
+      cli.expect("pointsOfUncertainty:")
+      cli.expect("(none)")
 
-      shell.sendLine("step")
-      shell.expect(contains("pointsOfUncertainty:"))
-      shell.expect(contains("(none)"))
-
-      shell.sendLine("quit")
-      Util.expectExitCode(ExitCode.Failure, shell)
-    } finally {
-      shell.close()
-    }
+      cli.sendLine("quit")
+    } (ExitCode.Failure)
   }
 
   @Test def test_1328_CLI_Debugger_breakpointTesting(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-      shell.sendLine("display info infoset")
-      shell.sendLine("break cell")
+      cli.sendLine("display info infoset")
+      cli.sendLine("break cell")
 
-      shell.sendLine("continue")
-      shell.expect(contains("</tns:cell>"))
+      cli.sendLine("continue")
+      cli.expect("</tns:cell>")
 
-      shell.sendLine("step")
-      shell.sendLine("step")
-      shell.sendLine("step")
-      shell.sendLine("step")
-      shell.expect(contains("<tns:cell>0</tns:cell>"))
+      cli.sendLine("step")
+      cli.sendLine("step")
+      cli.sendLine("step")
+      cli.sendLine("step")
+      cli.expect("<tns:cell>0</tns:cell>")
 
-      shell.sendLine("continue")
-      shell.expect(contains("</tns:cell>"))
+      cli.sendLine("continue")
+      cli.expect("</tns:cell>")
 
-      shell.sendLine("step")
-      shell.sendLine("step")
-      shell.sendLine("step")
-      shell.sendLine("step")
-      shell.expect(contains("<tns:cell>1</tns:cell>"))
+      cli.sendLine("step")
+      cli.sendLine("step")
+      cli.sendLine("step")
+      cli.sendLine("step")
+      cli.expect("<tns:cell>1</tns:cell>")
 
-      shell.sendLine("delete breakpoint 1")
-      shell.sendLine("continue")
+      cli.sendLine("delete breakpoint 1")
+      cli.sendLine("continue")
 
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_1329_CLI_Debugger_breakpointTesting2(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-      shell.sendLine("display info infoset")
-      shell.sendLine("break cell")
-      shell.sendLine("condition 1 dfdl:occursIndex() eq 3")
+      cli.sendLine("display info infoset")
+      cli.sendLine("break cell")
+      cli.sendLine("condition 1 dfdl:occursIndex() eq 3")
 
-      shell.sendLine("info breakpoints")
-      shell.expect(contains("1: cell   { dfdl:occursIndex() eq 3 }"))
+      cli.sendLine("info breakpoints")
+      cli.expect("1: cell   { dfdl:occursIndex() eq 3 }")
 
-      shell.sendLine("continue")
-      shell.expect(contains("</tns:cell>"))
+      cli.sendLine("continue")
+      cli.expect("</tns:cell>")
 
-      shell.sendLine("step")
-      shell.sendLine("step")
-      shell.sendLine("step")
-      shell.sendLine("step")
-      shell.expect(contains("<tns:cell>2</tns:cell>")) // lacks tns: prefix because debugger explicitly strips them.
+      cli.sendLine("step")
+      cli.sendLine("step")
+      cli.sendLine("step")
+      cli.sendLine("step")
+      cli.expect("<tns:cell>2</tns:cell>")
 
-      shell.sendLine("continue")
-      shell.expect(contains("<tns:cell>6</tns:cell>")) // has tns prefix because this is the final infoset, not the debugger printing this.
+      cli.sendLine("continue")
+      cli.expect("<tns:cell>6</tns:cell>")
 
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_CLI_Debugger_SDE_message(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      cli.sendLine("display info infoset")
+      cli.sendLine("break cell")
+      cli.sendLine("condition 1 fn:count(../cell) eq 3") // ../cell is wrong. Needs to be ../tns:cell
 
-      shell.sendLine("display info infoset")
-      shell.sendLine("break cell")
-      shell.sendLine("condition 1 fn:count(../cell) eq 3") // ../cell is wrong. Needs to be ../tns:cell
+      cli.sendLine("continue")
+      cli.expect("Schema Definition Error")
+      cli.expect("{}cell")
+      cli.expect("tns:cell")
 
-      shell.sendLine("continue")
-      shell.expect(allOf(contains("Schema Definition Error"), contains("{}cell"), contains("tns:cell")))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_1330_CLI_Debugger_breakpointTesting3(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      cli.sendLine("display info occursIndex")
+      cli.expect("(debug)")
+      cli.sendLine("break cell")
+      cli.expect("(debug)")
+      cli.sendLine("info breakpoints")
+      cli.expect("1: cell")
 
-      shell.sendLine("display info occursIndex")
-      shell.expect(contains("(debug)"))
-      shell.sendLine("break cell")
-      shell.expect(contains("(debug)"))
-      shell.sendLine("info breakpoints")
-      shell.expect(contains("1: cell"))
+      cli.sendLine("continue")
+      cli.expect("occursIndex: 1")
 
-      shell.sendLine("continue")
-      shell.expect(contains("occursIndex: 1"))
+      cli.sendLine("continue")
+      cli.expect("occursIndex: 2")
 
-      shell.sendLine("continue")
-      shell.expect(contains("occursIndex: 2"))
+      cli.sendLine("disable breakpoint 1")
+      cli.sendLine("info breakpoints")
+      cli.expect("1*: cell")
 
-      shell.sendLine("disable breakpoint 1")
-      shell.sendLine("info breakpoints")
-      shell.expect(contains("1*: cell"))
+      cli.sendLine("info data")
+      cli.expect("0~,~1~,~2~,~3~,~4~,~5~,~6~")
 
-      shell.sendLine("info data")
-      // shell.expect(contains("(2 to 2)"))
-      shell.expect(contains("0~,~1~,~2~,~3~,~4~,~5~,~6~"))
+      cli.sendLine("continue")
+      cli.expect("<tns:cell>6</tns:cell>")
 
-      shell.sendLine("continue")
-      shell.expect(contains("<tns:cell>6</tns:cell>"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_1333_CLI_Debugger_settingInfosetLines(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input3.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
-
-      shell.sendLine("display info infoset")
-      shell.sendLine("set infosetLines 1")
-
-      shell.sendLine("break cell")
-      shell.sendLine("continue")
-      shell.expect(contains("..."))
-      shell.expect(contains("</tns:matrix>"))
-
-      shell.sendLine("set infosetLines 4")
-      shell.sendLine("continue")
-      shell.expect(contains("..."))
-      shell.expect(contains("<tns:cell>3</tns:cell>"))
-      shell.expect(contains("</tns:matrix>"))
-
-      shell.sendLine("set infosetLines 10")
-      shell.sendLine("continue")
-      shell.expect(contains("<tns:matrix"))
-
-      shell.sendLine("set infosetLines -900")
-      shell.sendLine("continue")
-      shell.expect(contains("<tns:matrix"))
-      shell.expect(contains("</tns:matrix>"))
-
-      shell.sendLine("disable breakpoint 1")
-      shell.sendLine("continue")
-
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input3.txt")
+
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
+
+      cli.sendLine("display info infoset")
+      cli.sendLine("set infosetLines 1")
+
+      cli.sendLine("break cell")
+      cli.sendLine("continue")
+      cli.expect("...")
+      cli.expect("</tns:matrix>")
+
+      cli.sendLine("set infosetLines 4")
+      cli.sendLine("continue")
+      cli.expect("...")
+      cli.expect("<tns:cell>3</tns:cell>")
+      cli.expect("</tns:matrix>")
+
+      cli.sendLine("set infosetLines 10")
+      cli.sendLine("continue")
+      cli.expect("<tns:matrix")
+
+      cli.sendLine("set infosetLines -900")
+      cli.sendLine("continue")
+      cli.expect("<tns:matrix")
+      cli.expect("</tns:matrix>")
+
+      cli.sendLine("disable breakpoint 1")
+      cli.sendLine("continue")
+
+    } (ExitCode.Success)
   }
 
   @Test def test_1334_CLI_Debugger_infoBitPosition(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      cli.sendLine("display info bitPosition")
+      cli.sendLine("display info data")
+      cli.sendLine("break cell")
 
-      shell.sendLine("display info bitPosition")
-      shell.sendLine("display info data")
-      shell.sendLine("break cell")
+      cli.sendLine("continue")
+      cli.expect("bitPosition: 0")
 
-      shell.sendLine("continue")
-      shell.expect(contains("bitPosition: 0"))
+      cli.sendLine("continue")
+      cli.expect("bitPosition: 16")
 
-      shell.sendLine("continue")
-      shell.expect(contains("bitPosition: 16"))
+      cli.sendLine("continue")
+      cli.expect("bitPosition: 32")
 
-      shell.sendLine("continue")
-      shell.expect(contains("bitPosition: 32"))
+      cli.sendLine("continue")
 
-      shell.sendLine("continue")
-
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_1337_CLI_Debugger_childIndex(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input4.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input4.txt")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-      shell.sendLine("break cell")
-      shell.sendLine("display info childIndex")
-      shell.sendLine("display info infoset")
+      cli.sendLine("break cell")
+      cli.sendLine("display info childIndex")
+      cli.sendLine("display info infoset")
 
-      shell.sendLine("continue")
-      shell.expect(contains("childIndex: 1"))
+      cli.sendLine("continue")
+      cli.expect("childIndex: 1")
 
-      shell.sendLine("continue")
-      shell.expect(contains("childIndex: 2"))
+      cli.sendLine("continue")
+      cli.expect("childIndex: 2")
 
-      shell.sendLine("continue")
-      shell.expect(contains("childIndex: 4"))
+      cli.sendLine("continue")
+      cli.expect("childIndex: 4")
 
-      shell.sendLine("disable breakpoint 1")
-      shell.sendLine("continue")
+      cli.sendLine("disable breakpoint 1")
+      cli.sendLine("continue")
 
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_1340_CLI_Debugger_infoPath(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-    val output1 = Util.getExpectedString("output1.txt")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      cli.sendLine("break cell")
+      cli.sendLine("display info path")
 
-      shell.sendLine("break cell")
-      shell.sendLine("display info path")
+      cli.sendLine("continue")
+      cli.expect("matrixType::sequence[1]::row::LocalComplexTypeDef::sequence[1]::cell")
 
-      shell.sendLine("continue")
-      shell.expect(contains("matrixType::sequence[1]::row::LocalComplexTypeDef::sequence[1]::cell"))
+      cli.sendLine("delete breakpoint 1")
+      cli.expect("debug")
+      cli.sendLine("continue")
 
-      shell.sendLine("delete breakpoint 1")
-      shell.expect(contains("debug"))
-      shell.sendLine("continue")
-      shell.expect(contains(output1))
+      cli.expect("""<tns:matrix xmlns:tns="http://www.example.org/example1/">""")
+      cli.expect("<tns:cell>2</tns:cell>")
 
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_1382_CLI_Debugger_dataAndWrapLength2(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
-
-      shell.sendLine("break cell")
-      shell.sendLine("continue")
-      shell.sendLine("info data")
-      shell.expect(contains("0~,~1~,~2~,~3~,~4~,~5~,~6~"))
-
-      //      shell.sendLine("set dataLength 2")
-      //      shell.sendLine("info data")
-      //      shell.expect(contains("0,"))
-
-      shell.sendLine("set dataLength -938")
-      shell.sendLine("info data")
-      shell.expect(contains("0~,~1~,~2~,~3~,~4~,~5~,~6~"))
-
-      //      shell.sendLine("set wrapLength 2")
-      //      shell.sendLine("info data")
-      //      shell.expect(contains("    0,"))
-      //      shell.expect(contains("    1,"))
-      //      shell.expect(contains("    2,"))
-      //      shell.expect(contains("    3,"))
-      //      shell.expect(contains("    4,"))
-      //      shell.expect(contains("    5,"))
-      //      shell.expect(contains("    6"))
-
-      shell.sendLine("disable breakpoint 1")
-      shell.sendLine("continue")
-
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
+
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
+
+      cli.sendLine("break cell")
+      cli.sendLine("continue")
+      cli.sendLine("info data")
+      cli.expect("0~,~1~,~2~,~3~,~4~,~5~,~6~")
+
+      cli.sendLine("set dataLength -938")
+      cli.sendLine("info data")
+      cli.expect("0~,~1~,~2~,~3~,~4~,~5~,~6~")
+
+      cli.sendLine("disable breakpoint 1")
+      cli.sendLine("continue")
+
+    } (ExitCode.Success)
   }
 
   @Test def test_1863_CLI_Debugger_groupIndex01(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema_03.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input9.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d parse -r list -s %s %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
-
-      shell.sendLine("display info groupIndex")
-      shell.sendLine("break price")
-      shell.expect(contains("1: price"))
-      shell.sendLine("break comment")
-      shell.expect(contains("2: comment"))
-
-      shell.sendLine("continue")
-      shell.expect(contains("groupIndex: 2"))
-      shell.sendLine("continue")
-      shell.expect(contains("groupIndex: 4"))
-      shell.sendLine("continue")
-      shell.expect(contains("groupIndex: 2"))
-      shell.sendLine("continue")
-      shell.expect(contains("groupIndex: 4"))
-      shell.sendLine("continue")
-      shell.expect(contains("<ex:price>89.99</ex:price>"))
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema_03.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input9.txt")
+
+    runCLI(args"-d parse -r list -s $schema $input") { cli =>
+      cli.expect("(debug)")
+
+      cli.sendLine("display info groupIndex")
+      cli.sendLine("break price")
+      cli.expect("1: price")
+      cli.sendLine("break comment")
+      cli.expect("2: comment")
+
+      cli.sendLine("continue")
+      cli.expect("groupIndex: 2")
+      cli.sendLine("continue")
+      cli.expect("groupIndex: 4")
+      cli.sendLine("continue")
+      cli.expect("groupIndex: 2")
+      cli.sendLine("continue")
+      cli.expect("groupIndex: 4")
+      cli.sendLine("continue")
+      cli.expect("<ex:price>89.99</ex:price>")
+    } (ExitCode.Success)
   }
 
   @Test def test_1029_CLI_Debugger_validation1(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema_03.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input9.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d parse -r list -s %s %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
-
-      shell.sendLine("display info dne1")
-      shell.expect(contains("error: undefined info command: dne1"))
-      shell.sendLine("display info bitLimit dne2")
-      shell.expect(contains("error: bitLimit command requires zero arguments"))
-      shell.sendLine("display break")
-      shell.expect(contains("error: undefined command: break"))
-      shell.sendLine("quit")
-      Util.expectExitCode(ExitCode.Failure, shell)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema_03.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input9.txt")
+
+    runCLI(args"-d parse -r list -s $schema $input") { cli =>
+      cli.expect("(debug)")
+
+      cli.sendLine("display info dne1")
+      cli.expect("error: undefined info command: dne1")
+      cli.sendLine("display info bitLimit dne2")
+      cli.expect("error: bitLimit command requires zero arguments")
+      cli.sendLine("display break")
+      cli.expect("error: undefined command: break")
+      cli.sendLine("quit")
+    } (ExitCode.Failure)
   }
 
   @Test def test_3258_CLI_Debugger_infodata(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
-
-      shell.sendLine("display info data")
-      shell.sendLine("step")
-      shell.expect(contains("│")) //  (0 to 0)
-      shell.expect(contains("0~,~1~,~2~,~3~,~4~,~5~,~6~"))
-
-      shell.sendLine("break cell")
-      shell.sendLine("condition 1 dfdl:occursIndex() eq 5")
-      shell.sendLine("continue")
-
-      // Gaak. Eclipse default font isn't monospaced. The visible space character is wider than a regular character!
-      shell.expect(contains("""                                  │                                    │"""))
-      shell.expect(contains("""    87654321  0011 2233 4455 6677 8899 aabb ccdd eeff  0~1~2~3~4~5~6~7~8~9~a~b~c~d~e~f~"""))
-      shell.expect(contains("""    00000000: 302c 312c 322c 332c 342c 352c 36         0~,~1~,~2~,~3~,~4~,~5~,~6~      """))
-      shell.sendLine("continue")
-//      shell.sendLine("quit")
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
+
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
+
+      cli.sendLine("display info data")
+      cli.sendLine("step")
+      cli.expect("│") //  (0 to 0)
+      cli.expect("0~,~1~,~2~,~3~,~4~,~5~,~6~")
+
+      cli.sendLine("break cell")
+      cli.sendLine("condition 1 dfdl:occursIndex() eq 5")
+      cli.sendLine("continue")
+
+      cli.expect("""                                  │                                    │""")
+      cli.expect("""    87654321  0011 2233 4455 6677 8899 aabb ccdd eeff  0~1~2~3~4~5~6~7~8~9~a~b~c~d~e~f~""")
+      cli.expect("""    00000000: 302c 312c 322c 332c 342c 352c 36         0~,~1~,~2~,~3~,~4~,~5~,~6~      """)
+      cli.sendLine("continue")
+    } (ExitCode.Success)
   }
 
   @Test def test_3264_CLI_Debugger_undefined_command(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      cli.sendLine("display data")
+      cli.expect("error: undefined command: data")
 
-      shell.sendLine("display data")
-      shell.expect(contains("error: undefined command: data"))
+      cli.sendLine("set breakonfailure true")
+      cli.expect("error: undefined command: breakonfailure")
 
-      shell.sendLine("set breakonfailure true")
-      shell.expect(contains("error: undefined command: breakonfailure"))
+      cli.sendLine("continue")
 
-      shell.sendLine("continue")
-
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
-    }
+    } (ExitCode.Success)
   }
 
   @Test def test_CLI_Debugger_delimiterStack(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input2.txt")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      cli.sendLine("break row")
+      cli.expect("(debug)")
 
-      shell.sendLine("break row")
-      shell.expect(contains("(debug)"))
+      cli.sendLine("continue")
+      cli.expect("(debug)")
 
-      shell.sendLine("continue")
-      shell.expect(contains("(debug)"))
+      cli.sendLine("info delimiterStack")
+      cli.expect("local:  %NL; (separator)")
+      cli.expect("(debug)")
 
-      shell.sendLine("info delimiterStack")
-      shell.expect(contains("""local:  %NL; (separator)"""))
-      shell.expect(contains("(debug)"))
+      cli.sendLine("break cell")
+      cli.expect("(debug)")
 
-      shell.sendLine("break cell")
-      shell.expect(contains("(debug)"))
+      cli.sendLine("continue")
+      cli.expect("(debug)")
 
-      shell.sendLine("continue")
-      shell.expect(contains("(debug)"))
+      cli.sendLine("info delimiterStack")
+      cli.expect("remote: %NL; (separator)")
+      cli.expect("local:  , (separator)")
 
-      shell.sendLine("info delimiterStack")
-      shell.expect(contains("""remote: %NL; (separator)"""))
-      shell.expect(contains("""local:  , (separator)"""))
-
-      shell.sendLine("quit")
-      Util.expectExitCode(ExitCode.Failure, shell)
-    } finally {
-      shell.close()
-    }
+      cli.sendLine("quit")
+    } (ExitCode.Failure)
   }
 
   @Test def test_CLI_Debugger_utf16_encoding(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/utf16schema.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/hextest.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/utf16schema.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/hextest.txt")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r e2 %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+    runCLI(args"-d parse -s $schema -r e2 $input") { cli =>
+      cli.expect("(debug)")
 
-      shell.sendLine("info data")
-      shell.expect(contains("\u240A"))
+      cli.sendLine("info data")
+      cli.expect("\u240A")
 
-      shell.sendLine("quit")
-      Util.expectExitCode(ExitCode.Failure, shell)
-    } finally {
-      shell.close()
-    }
+      cli.sendLine("quit")
+    } (ExitCode.Failure)
   }
 
   @Test def test_1337_CLI_Debugger_info_infoset(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input1.txt")
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r matrix %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+    runCLI(args"-d parse -s $schema -r matrix $input") { cli =>
+      cli.expect("(debug)")
 
-      shell.sendLine("info infoset")
-      shell.expect(contains("No Infoset"))
+      cli.sendLine("info infoset")
+      cli.expect("No Infoset")
 
-      shell.sendLine("step")
-      shell.sendLine("info infoset")
-      shell.expect(contains("matrix"))
+      cli.sendLine("step")
+      cli.sendLine("info infoset")
+      cli.expect("matrix")
 
-      shell.sendLine("quit")
-      Util.expectExitCode(ExitCode.Failure, shell)
-    } finally {
-      shell.close()
-    }
+      cli.sendLine("quit")
+    } (ExitCode.Failure)
   }
 
   @Test def test_CLI_Debugger_InfoHidden_1(): Unit = {
-    val schemaFile = Util.daffodilPath(
-      "daffodil-test/src/test/resources/org/apache/daffodil/section14/sequence_groups/SequencesWithHiddenRefs.dfdl.xsd")
-    val inputFile = Util.newTempFile("testInput_", ".tmp", optFileContents = Some("2~3"))
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) {
-      (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile.getAbsolutePath))
-    } else {
-      (schemaFile, inputFile)
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section14/sequence_groups/SequencesWithHiddenRefs.dfdl.xsd")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    withTempFile { input =>
+      Files.write(input, "2~3".getBytes(UTF_8))
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r e5 %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      runCLI(args"-d parse -s $schema -r e5 $input") { cli =>
+        cli.expect("(debug)")
 
-      shell.sendLine("break f")
-      shell.sendLine("display info hidden")
+        cli.sendLine("break f")
+        cli.sendLine("display info hidden")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: false"))
+        cli.sendLine("continue")
+        cli.expect("hidden: false")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: false"))
+        cli.sendLine("continue")
+        cli.expect("hidden: false")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains("<f xmlns=\"\">2</f>"))
+        cli.sendLine("continue")
+        cli.expect("<f xmlns=\"\">2</f>")
 
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
+      } (ExitCode.Success)
     }
   }
 
   @Test def test_CLI_Debugger_InfoHidden_2(): Unit = {
-    val schemaFile = Util.daffodilPath(
-      "daffodil-test/src/test/resources/org/apache/daffodil/section14/sequence_groups/SequencesWithHiddenRefs.dfdl.xsd")
-    val inputFile = Util.newTempFile("testInput_", ".tmp", optFileContents = Some("2~3"))
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) {
-      (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile.getAbsolutePath))
-    } else {
-      (schemaFile, inputFile)
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section14/sequence_groups/SequencesWithHiddenRefs.dfdl.xsd")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    withTempFile { input =>
+      Files.write(input, "2~3".getBytes(UTF_8))
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r e4 %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      runCLI(args"-d parse -s $schema -r e4 $input") { cli =>
+        cli.expect("(debug)")
 
-      shell.sendLine("break f")
-      shell.sendLine("display info hidden")
+        cli.sendLine("break f")
+        cli.sendLine("display info hidden")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: false"))
+        cli.sendLine("continue")
+        cli.expect("hidden: false")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: false"))
+        cli.sendLine("continue")
+        cli.expect("hidden: false")
 
-      shell.sendLine("continue")
-      shell.expect(contains("<f xmlns=\"\">3</f>"))
+        cli.sendLine("continue")
+        cli.expect("<f xmlns=\"\">3</f>")
 
-      Util.expectExitCode(ExitCode.Success, shell)
-    } finally {
-      shell.close()
+      } (ExitCode.Success)
     }
   }
 
   @Test def test_CLI_Debugger_InfoHidden_3(): Unit = {
-    val schemaFile = Util.daffodilPath(
-      "daffodil-test/src/test/resources/org/apache/daffodil/section15/choice_groups/ChoicesInHiddenContexts.dfdl.xsd")
-    val inputFile = Util.newTempFile("testInput_", ".tmp", optFileContents = Some("2,3"))
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) {
-      (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile.getAbsolutePath))
-    } else {
-      (schemaFile, inputFile)
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section15/choice_groups/ChoicesInHiddenContexts.dfdl.xsd")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    withTempFile { input =>
+      Files.write(input, "2,3".getBytes(UTF_8))
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r e8 %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      runCLI(args"-d parse -s $schema -r e8 $input") { cli =>
+        cli.expect("(debug)")
 
-      shell.sendLine("break a")
-      shell.sendLine("break h")
-      shell.sendLine("break g")
-      shell.sendLine("break e")
-      shell.sendLine("break f")
-      shell.sendLine("display info hidden")
+        cli.sendLine("break a")
+        cli.sendLine("break h")
+        cli.sendLine("break g")
+        cli.sendLine("break e")
+        cli.sendLine("break f")
+        cli.sendLine("display info hidden")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: false"))
+        cli.sendLine("continue")
+        cli.expect("hidden: false")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: false"))
+        cli.sendLine("continue")
+        cli.expect("hidden: false")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: false"))
+        cli.sendLine("continue")
+        cli.expect("hidden: false")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains("<a>2</a>"))
-      shell.expect(contains("<g></g>"))
-    } finally {
-      shell.close()
+        cli.sendLine("continue")
+        cli.expect("<a>2</a>")
+        cli.expect("<g></g>")
+      } (ExitCode.Success)
     }
   }
 
   @Test def test_CLI_Debugger_InfoHidden_4(): Unit = {
-    val schemaFile = Util.daffodilPath(
-      "daffodil-test/src/test/resources/org/apache/daffodil/section15/choice_groups/ChoicesInHiddenContexts.dfdl.xsd")
-    val inputFile = Util.newTempFile("testInput_", ".tmp", optFileContents = Some("[6~]9"))
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) {
-      (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile.getAbsolutePath))
-    } else {
-      (schemaFile, inputFile)
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section15/choice_groups/ChoicesInHiddenContexts.dfdl.xsd")
 
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
+    withTempFile { input =>
+      Files.write(input, "[6~]9".getBytes(UTF_8))
 
-    try {
-      val cmd = String.format("%s -d parse -s %s -r e9 %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
+      runCLI(args"-d parse -s $schema -r e9 $input") { cli =>
+        cli.expect("(debug)")
 
-      shell.sendLine("break e")
-      shell.sendLine("break f")
-      shell.sendLine("break g")
-      shell.sendLine("break h")
-      shell.sendLine("break i")
-      shell.sendLine("display info path hidden")
+        cli.sendLine("break e")
+        cli.sendLine("break f")
+        cli.sendLine("break g")
+        cli.sendLine("break h")
+        cli.sendLine("break i")
+        cli.sendLine("display info path hidden")
 
-      shell.sendLine("continue")
-      shell.expect(contains(":f"))
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect(":f")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains(":i"))
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect(":i")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains(":h"))
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect(":h")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains(":e"))
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect(":e")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains(":f"))
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect(":f")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains(":f"))
-      shell.expect(contains("hidden: false"))
+        cli.sendLine("continue")
+        cli.expect(":f")
+        cli.expect("hidden: false")
 
-      shell.sendLine("continue")
-      shell.expect(contains(":g"))
-      shell.expect(contains("hidden: false"))
+        cli.sendLine("continue")
+        cli.expect(":g")
+        cli.expect("hidden: false")
 
-      shell.sendLine("continue")
-      shell.expect(contains(":i"))
-      shell.expect(contains("hidden: false"))
+        cli.sendLine("continue")
+        cli.expect(":i")
+        cli.expect("hidden: false")
 
-      shell.sendLine("continue")
-      shell.expect(contains(":h"))
-      shell.expect(contains("hidden: false"))
+        cli.sendLine("continue")
+        cli.expect(":h")
+        cli.expect("hidden: false")
 
-      shell.sendLine("continue")
-      shell.expect(contains(":e"))
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect(":e")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains(":f"))
-      shell.expect(contains("hidden: true"))
+        cli.sendLine("continue")
+        cli.expect(":f")
+        cli.expect("hidden: true")
 
-      shell.sendLine("continue")
-      shell.expect(contains("<h></h>"))
-    } finally {
-      shell.close()
+        cli.sendLine("continue")
+        cli.expect("<h></h>")
+      } (ExitCode.Success)
     }
   }
-  /* See DFDL-1264
-  @Test def test_3585_CLI_Debugger_simpleDebugger_unparse() {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input12.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = if (Util.isWindows) Util.start("", envp = DAFFODIL_JAVA_OPTS) else Util.start("")
-
-    try {
-      val cmd = String.format("%s -d unparse -s %s -r e1 %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("(debug)"))
-      shell.sendLine("break e1")
-      shell.expect(contains("1: e1"))
-      shell sendLine ("continue")
-      shell.expect(contains("Hello  breakpoint 1: e1"))
-      shell.sendLine("info data")
-      shell.expect(contains(
-        """4865 6c6c 6f                             Hello"""))
-      shell.sendLine("quit")
-      Util.expectExitCode(ExitCode.Failure, shell)
-    } finally {
-      shell.close()
-    }
-  }*/
+
+  @Test def test_3585_CLI_Debugger_simpleDebugger_unparse(): Unit = {
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input12.txt")
+
+    runCLI(args"-d unparse -s $schema -r e1 $input") { cli =>
+      cli.expect("(debug)")
+      cli.sendLine("break e1")
+      cli.expect("1: e1")
+      cli.sendLine("continue")
+      cli.expect("Hello  breakpoint 1: e1")
+      cli.sendLine("info data")
+      cli.expect("4865 6c6c 6f                             Hello")
+    } (ExitCode.Failure)
+  }

Review Comment:
   The commented out test works now - nice!



##########
daffodil-cli/src/it/scala/org/apache/daffodil/unparsing/TestCLIUnparsing.scala:
##########
@@ -17,679 +17,319 @@
 
 package org.apache.daffodil.unparsing
 
+import java.nio.charset.StandardCharsets.UTF_8
+
+import org.apache.commons.io.FileUtils
+
 import org.junit.Assert._
 import org.junit.Test
-import java.io.File
-import org.apache.daffodil.CLI.Util
-import net.sf.expectit.matcher.Matchers.contains
-import net.sf.expectit.matcher.Matchers.eof
+
+import org.apache.daffodil.CLI.Util._
 import org.apache.daffodil.Main.ExitCode
 
 class TestCLIunparsing {
 
   @Test def test_3525_CLI_Unparsing_SimpleUnparse_inFile(): Unit = {
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input12.txt")
 
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input12.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s unparse -s %s --root e1 %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("Hello"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-      shell.close()
-    } finally {
-      shell.close()
-    }
+    runCLI(args"unparse -s $schema --root e1 $input") { cli =>
+      cli.expect("Hello")
+    } (ExitCode.Success)
   }
 
   @Test def test_3526_CLI_Unparsing_SimpleUnparse_inFile2(): Unit = {
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input13.txt")
 
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input13.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s unparse -s %s --root e3 %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("[1,2]"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-      shell.close()
-    } finally {
-      shell.close()
-    }
+    runCLI(args"unparse -s $schema --root e3 $input") { cli =>
+      cli.expect("[1,2]")
+    } (ExitCode.Success)
   }
 
   @Test def test_3527_CLI_Unparsing_SimpleUnparse_stdin(): Unit = {
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input14.txt")
 
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input14.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-	  val cmd = String.format(Util.cat(testInputFile) + "| %s unparse -s %s --root e3", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("[1,2]"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-      shell.close()
-    } finally {
-      shell.close()
-    }
+    runCLI(args"unparse -s $schema --root e3") { cli =>
+      cli.sendFile(input, inputDone = true)
+      cli.expect("[1,2]")
+    } (ExitCode.Success)
   }
 
   @Test def test_3528_CLI_Unparsing_SimpleUnparse_stdin2(): Unit = {
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
 
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
-    val (testSchemaFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile)) else (schemaFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format(Util.echoN("\"<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>\"") + "| %s unparse -s %s --root e1", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("Hello"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-      shell.close()
-    } finally {
-      shell.close()
-    }
+    runCLI(args"unparse -s $schema --root e1") { cli =>
+      cli.send("<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>", inputDone = true)
+      cli.expect("Hello")
+    } (ExitCode.Success)
   }
 
   @Test def test_3529_CLI_Unparsing_SimpleUnparse_stdin3(): Unit = {
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
 
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
-    val (testSchemaFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile)) else (schemaFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format(Util.echoN("\"<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>\"") + "| %s unparse -s %s --root e1 -", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("Hello"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-      shell.close()
-    } finally {
-      shell.close()
-    }
+    runCLI(args"unparse -s $schema --root e1 -") { cli =>
+      cli.send("<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>", inputDone = true)
+      cli.expect("Hello")
+    } (ExitCode.Success)
   }
 
   @Test def test_3584_CLI_Unparsing_SimpleUnparse_stdin4(): Unit = {
-
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val (testSchemaFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile)) else (schemaFile)
-
-    val shell = Util.start("")
-
-    try {
-      val input = "\"<tns:file xmlns:tns='http://www.example.org/example1/'><tns:header><tns:title>1</tns:title><tns:title>2</tns:title><tns:title>3</tns:title></tns:header><tns:record><tns:item>4</tns:item><tns:item>5</tns:item><tns:item>6</tns:item></tns:record></tns:file>\""
-      val cmd = String.format(Util.echoN(input) + "| %s unparse -s %s --root file", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("1,2,3"))
-      shell.expect(contains("4,5,6"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-      shell.close()
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
+
+    runCLI(args"unparse -s $schema --root file") { cli =>
+      val input = "<tns:file xmlns:tns='http://www.example.org/example1/'><tns:header><tns:title>1</tns:title><tns:title>2</tns:title><tns:title>3</tns:title></tns:header><tns:record><tns:item>4</tns:item><tns:item>5</tns:item><tns:item>6</tns:item></tns:record></tns:file>"
+      cli.send(input, inputDone = true)
+      cli.expect("1,2,3")
+      cli.expect("4,5,6")
+    } (ExitCode.Success)
   }
 
   @Test def test_3574_CLI_Unparsing_SimpleUnparse_extVars(): Unit = {
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section07/external_variables/external_variables.dfdl.xsd")
+    val config = path("daffodil-test/src/test/resources/org/apache/daffodil/section07/external_variables/daffodil_config_cli_test.xml")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input15.txt")
 
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section07/external_variables/external_variables.dfdl.xsd")
-    val configFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section07/external_variables/daffodil_config_cli_test.xml")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input15.txt")
-    val (testSchemaFile, testConfigFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(configFile), Util.cmdConvert(inputFile)) else (schemaFile, configFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s unparse -s %s -r row -D\"{http://example.com}var1=99\" -c %s %s", Util.binPath, testSchemaFile, testConfigFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("0"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-      shell.close()
-    } finally {
-      shell.close()
-    }
+    runCLI(args"unparse -s $schema -r row -D{http://example.com}var1=99 -c $config $input") { cli =>
+      cli.expect("0")
+    } (ExitCode.Success)
   }
 
   @Test def test_3575_CLI_Unparsing_SimpleUnparse_extVars2(): Unit = {
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section07/external_variables/external_variables.dfdl.xsd")
+    val config = path("daffodil-test/src/test/resources/org/apache/daffodil/section07/external_variables/daffodil_config_cli_test.xml")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input16.txt")
 
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section07/external_variables/external_variables.dfdl.xsd")
-    val configFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section07/external_variables/daffodil_config_cli_test.xml")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input16.txt")
-    val (testSchemaFile, testConfigFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(configFile), Util.cmdConvert(inputFile)) else (schemaFile, configFile, inputFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s unparse -s %s -r row -c %s %s", Util.binPath, testSchemaFile, testConfigFile, testInputFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("0"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-      shell.close()
-    } finally {
-      shell.close()
-    }
+    runCLI(args"unparse -s $schema -r row -c $config $input") { cli =>
+      cli.expect("0")
+    } (ExitCode.Success)
   }
 
   @Test def test_3582_CLI_Unparsing_SimpleUnparse_outFile(): Unit = {
-    val tmp_filename: String = (System.currentTimeMillis / 1000).toString()
-    val file = new File(tmp_filename)
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input13.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s unparse -s %s -r e3 -o %s %s", Util.binPath, testSchemaFile, tmp_filename, testInputFile)
-      shell.sendLine(cmd)
-
-      val catCmd = if (Util.isWindows) "type" else "cat"
-      val openCmd = String.format("%s %s", catCmd, tmp_filename)
-
-      shell.sendLine(openCmd)
-      shell.expect(contains("[1,2]"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-      assertTrue("Failed to remove temporary file: %s".format(file), file.delete)
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input13.txt")
+
+    withTempFile { output =>
+      runCLI(args"unparse -s $schema -r e3 -o $output $input") { cli =>
+      } (ExitCode.Success)
+
+      val res = FileUtils.readFileToString(output.toFile, UTF_8)
+      assertTrue(res.contains("[1,2]"))
     }
   }
 
   @Test def test_3581_CLI_Unparsing_SimpleUnparse_stOutDash(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
-    val inputFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input13.txt")
-    val (testSchemaFile, testInputFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile), Util.cmdConvert(inputFile)) else (schemaFile, inputFile)
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("%s unparse -s %s -r e3 -o - %s", Util.binPath, testSchemaFile, testInputFile)
-      shell.sendLine(cmd)
-
-      shell.expect(contains("[1,2]"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
+    val input = path("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/input/input13.txt")
+
+    runCLI(args"unparse -s $schema -r e3 -o - $input") { cli =>
+      cli.expect("[1,2]")
+    } (ExitCode.Success)
   }
 
   @Test def test_3580_CLI_Unparsing_SimpleUnparse_verboseMode(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
-    val (testSchemaFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile)) else (schemaFile)
-
-    val shell = Util.start("")
-
-    try {
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
 
-      shell.sendLine(String.format(Util.echoN("\"<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>\"") + "| %s -v unparse -s %s --root e1", Util.binPath, testSchemaFile))
-      shell.expectIn(1, contains("[info]"))
+    runCLI(args"-v unparse -s $schema --root e1") { cli =>
+      cli.send("<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>", inputDone = true)
+      cli.expectErr("[info]")
+    } (ExitCode.Success)
 
-      shell.sendLine(String.format(Util.echoN("\"<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>\"") + "| %s -vv unparse -s %s --root e1", Util.binPath, testSchemaFile))
-      shell.expectIn(1, contains("[debug]"))
-
-      shell.sendLine(String.format(Util.echoN("\"<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>\"") + "| %s -vvv unparse -s %s --root e1", Util.binPath, testSchemaFile))
-      shell.expectIn(1, contains("[trace]"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"-vv unparse -s $schema --root e1") { cli =>
+      cli.send("<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>", inputDone = true)
+      cli.expectErr("[debug]")
+    } (ExitCode.Success)
   }
 
   @Test def test_3579_CLI_Unparsing_negativeTest(): Unit = {
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format(Util.echoN("\"<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>\"") + "| %s unparse", Util.binPath)
-      shell.sendLine(cmd)
-      shell.expectIn(1, contains("There should be exactly one of the following options: schema, parser"))
-
-      Util.expectExitCode(ExitCode.Usage, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"unparse") { cli =>
+      cli.send("<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>", inputDone = true)
+      cli.expectErr("There should be exactly one of the following options: schema, parser")
+    } (ExitCode.Usage)
   }
 
   @Test def test_3578_CLI_Unparsing_SimpleUnparse_defaultRoot(): Unit = {
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
 
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd")
-    val (testSchemaFile) = if (Util.isWindows) (Util.cmdConvert(schemaFile)) else (schemaFile)
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format(Util.echoN("\"<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>\"") + "| %s unparse -s %s", Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("Hello"))
-
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.send("exit\n")
-      shell.expect(eof)
-      shell.close()
-    } finally {
-      shell.close()
-    }
+    runCLI(args"unparse -s $schema") { cli =>
+      cli.send("<tns:e1 xmlns:tns='http://example.com'>Hello</tns:e1>", inputDone = true)
+      cli.expect("Hello")
+    } (ExitCode.Success)
   }
 
   @Test def test_3583_CLI_Unparsing_SimpleUnparse_rootPath(): Unit = {
-    val schemaFile = Util.daffodilPath("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format(Util.echoN("\"<tns:hcp2 xmlns:tns='http://www.example.org/example1/'>12</tns:hcp2>\"") + "| %s unparse -s %s -r hcp2 -p /", Util.binPath, testSchemaFile)
-
-      shell.sendLine(cmd)
-      shell.expect(contains("12"))
+    val schema = path("daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd")
 
-      Util.expectExitCode(ExitCode.Success, shell)
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+    runCLI(args"unparse -s $schema -r hcp2 -p /") { cli =>
+      cli.send("<tns:hcp2 xmlns:tns='http://www.example.org/example1/'>12</tns:hcp2>", inputDone = true)
+      cli.expect("12")
+    } (ExitCode.Success)
   }
 
-  /*
-  // See DFDL-1346
-  @Test def test_3576_CLI_Unparsing_validate() {
-
-    val schemaFile = Util.daffodilPath("daffodil-cli/src/it/resources/org/apache/daffodil/CLI/cli_schema.dfdl.xsd")
-    val testSchemaFile = if (Util.isWindows) Util.cmdConvert(schemaFile) else schemaFile
-
-    val shell = Util.start("")
-
-    try {
-      val cmd = String.format("""echo '<ex:validation_check xmlns:ex="http://example.com">test</ex:validation_check>' | %s unparse -s %s -r validation_check --validate on """, Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("[warn] Validation Error: validation_check: cvc-pattern-valid"))
-      shell.expect(contains("[warn] Validation Error: validation_check failed"))
-
-      cmd = String.format("""echo '<ex:validation_check xmlns:ex="http://example.com">test</ex:validation_check>' | %s unparse -s %s -r validation_check --validate """, Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("[warn] Validation Error: validation_check: cvc-pattern-valid"))
-      shell.expect(contains("[warn] Validation Error: validation_check failed"))
-
-      cmd = String.format("""echo '<ex:validation_check xmlns:ex="http://example.com">test</ex:validation_check>' | %s unparse -s %s -r validation_check --validate limited """, Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-      shell.expect(contains("[warn] Validation Error: validation_check failed"))
-
-      cmd = String.format("""echo '<ex:validation_check xmlns:ex="http://example.com">test</ex:validation_check>' | %s unparse -s %s -r validation_check --validate off """, Util.binPath, testSchemaFile)
-      shell.sendLine(cmd)
-
-      shell.sendLine("exit")
-      shell.expect(eof)
-    } finally {
-      shell.close()
-    }
+  // DAFFODIL-1346
+  /*@Test*/ def test_3576_CLI_Unparsing_validate(): Unit = {

Review Comment:
   Again, nice job keeping a commented out test compiling.



##########
daffodil-cli/src/it/scala/org/apache/daffodil/CLI/Util.scala:
##########
@@ -17,183 +17,501 @@
 
 package org.apache.daffodil.CLI
 
-import org.apache.daffodil.util.Misc
-import net.sf.expectit.ExpectBuilder
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PipedInputStream
+import java.io.PipedOutputStream
+import java.io.PrintStream
+import java.lang.ProcessBuilder
+import java.math.BigInteger
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.MessageDigest
+import java.util.concurrent.TimeUnit
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+import com.fasterxml.jackson.core.io.JsonStringEncoder
+
 import net.sf.expectit.Expect
+import net.sf.expectit.ExpectBuilder
+import net.sf.expectit.Result
 import net.sf.expectit.filter.Filters.replaceInString
+import net.sf.expectit.matcher.Matcher
 import net.sf.expectit.matcher.Matchers.contains
-import org.apache.daffodil.Main.ExitCode
 
-import java.nio.file.Paths
-import java.io.{File, PrintWriter}
-import java.util.concurrent.TimeUnit
-import org.apache.daffodil.xml.XMLUtils
-import org.junit.Assert.fail
+import org.apache.commons.io.FileUtils
 
-object Util {
+import org.apache.logging.log4j.Level
+import org.apache.logging.log4j.core.appender.OutputStreamAppender
+import org.apache.logging.log4j.core.config.AbstractConfiguration
+import org.apache.logging.log4j.core.config.ConfigurationSource
+import org.apache.logging.log4j.core.config.Configurator
+import org.apache.logging.log4j.core.layout.PatternLayout
 
-  //val testDir = "daffodil-cli/src/it/resources/org/apache/daffodil/CLI/"
-  val testDir = "/org/apache/daffodil/CLI/"
-  val outputDir = testDir + "output/"
+import org.junit.Assert.assertEquals
 
-  val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
+import org.apache.daffodil.Main
+import org.apache.daffodil.Main.ExitCode
 
-  val dafRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
+object Util {
 
-  def daffodilPath(dafRelativePath: String): String = {
-    XMLUtils.slashify(dafRoot) + dafRelativePath
-  }
+  private val isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows")
 
-  val binPath = Paths.get(dafRoot, "daffodil-cli", "target", "universal", "stage", "bin", String.format("daffodil%s", (if (isWindows) ".bat" else ""))).toString()
+  private val daffodilRoot = sys.env.getOrElse("DAFFODIL_HOME", ".")
 
-  def getExpectedString(filename: String, convertToDos: Boolean = false): String = {
-    val rsrc = Misc.getRequiredResource(outputDir + filename)
-    val is = rsrc.toURL.openStream()
-    val source = scala.io.Source.fromInputStream(is)
-    val lines = source.mkString.trim()
-    source.close()
-    fileConvert(lines)
+  private val daffodilBinPath = {
+    val ext = if (isWindows) ".bat" else ""
+    Paths.get(daffodilRoot, s"daffodil-cli/target/universal/stage/bin/daffodil$ext")
   }
 
-  def start(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmdConvert(cmd)
-    } else {
-      "/bin/bash"
-    }
-
-    getShell(cmd, spawnCmd, envp, timeout)
+  /**
+   * Convert the daffodilRoot + parameter to a java Path. The string
+   * parameter should contain unix path sparators and it will be interpreted
+   * correctly regardless of operating system. When converted to a string to
+   * send to the CLI, it will use the correct line separator for the
+   * operating system
+   */
+  def path(string: String): Path = {
+    Paths.get(daffodilRoot, string)
   }
 
-  // This function will be used if you are providing two separate commands
-  // and doing the os check on the 'front end' (not within this utility class)
-  def startNoConvert(cmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long = 30): Expect = {
-    val spawnCmd = if (isWindows) {
-      "cmd /k" + cmd
-    } else {
-      "/bin/bash"
-    }
+  def devNull(): String = if (isWindows) "NUL" else "/dev/null"
 
-    return getShell(cmd, spawnCmd, envp = envp, timeout = timeout)
-  }
-
-  // Return a shell object with two streams
-  // The inputStream will be at index 0
-  // The errorStream will be at index 1
-  def getShell(cmd: String, spawnCmd: String, envp: Map[String, String] = Map.empty[String, String], timeout: Long): Expect = {
-    val newEnv = sys.env ++ envp
-
-    val envAsArray = newEnv.toArray.map { case (k, v) => k + "=" + v }
-    val process = Runtime.getRuntime().exec(spawnCmd, envAsArray)
-    val shell = new ExpectBuilder()
-      .withInputs(process.getInputStream(), process.getErrorStream())
-      .withInputFilters(replaceInString("\r\n", "\n"))
-      .withOutput(process.getOutputStream())
-      .withEchoOutput(System.out)
-      .withEchoInput(System.out)
-      .withTimeout(timeout, TimeUnit.SECONDS)
-      .withExceptionOnFailure()
-      .build();
-    if (!isWindows) {
-      shell.send(cmd)
+  def md5sum(path: Path): String = {
+    val md = MessageDigest.getInstance("MD5")
+    val buffer = new Array[Byte](8192)
+    val stream = Files.newInputStream(path)
+    var read = 0
+    while ({read = stream.read(buffer); read} > 0) {
+      md.update(buffer, 0, read)
     }
-    return shell
+    val md5sum = md.digest()
+    val bigInt = new BigInteger(1, md5sum)
+    bigInt.toString(16)
   }
 
-  def cmdConvert(str: String): String = {
-    if (isWindows)
-      str.replaceAll("/", "\\\\")
-    else
-      str
+  /**
+   * Create a temporary file in /tmp/daffodil/, call a user provided function
+   * passing in the Path to that new file, and delete the file when the
+   * function returns.
+   */
+  def withTempFile(f: (Path) => Unit) : Unit = withTempFile(null, f)
+
+  /**
+   * Create a temporary file in /tmp/daffodil/ with a givin suffix, call a user
+   * provided function passing in the Path to that new file, and delete the
+   * file when the function returns.
+   */
+  def withTempFile(suffix: String, f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempFile = Files.createTempFile(tempRoot, "daffodil-", suffix)
+    try {
+      f(tempFile)
+    } finally {
+      tempFile.toFile.delete()
+    }
   }
 
-  def fileConvert(str: String): String = {
-    val newstr = str.replaceAll("\r\n", "\n")
-    return newstr
+  /**
+   * Create a temporary directory in /tmp/daffodil/, call a user provided
+   * function passing in the Path to that new directory, and delete the
+   * directory and all of its contents when the function returns
+   */
+  def withTempDir(f: (Path) => Unit): Unit = {
+    val tempRoot = Paths.get(System.getProperty("java.io.tmpdir"), "daffodil")
+    Files.createDirectories(tempRoot)
+    val tempDir = Files.createTempDirectory(tempRoot, "daffodil-")
+    try {
+      f(tempDir)
+    } finally {
+      FileUtils.deleteDirectory(tempDir.toFile)
+    }
   }
 
-  def echoN(str: String): String = {
-    if (isWindows) {
-      "echo|set /p=" + str
-    } else {
-      "echo -n " + str
+  /**
+   * Set a system property using a provided key, value tuple, call a user
+   * provided function, and reset or clear the property when the function
+   * returns.
+   */
+  def withSysProp(keyVal: (String, String))(f: => Unit): Unit = {
+    val key = keyVal._1
+    val newVal = keyVal._2
+    val oldVal = System.setProperty(key, newVal)
+    try {
+      f
+    } finally {
+      if (oldVal == null) {
+        System.clearProperty(key)
+      } else {
+        System.setProperty(key, oldVal)
+      }
     }
   }
 
-  def devNull(): String = {
-    if (isWindows) {
-      "NUL"
-    } else {
-      "/dev/null"
+  /**
+   * Run a CLI test.
+   *
+   * Runs CLI logic using the provided arguments and classpath, creates a
+   * CLITester so that the user can send input and validate output, and
+   * verifies the expected exit code.
+   *
+   * For performance reasons, this defaults to running the CLI in a new thread
+   * unless the classpaths parameter is nonempty or he fork parameter is set to
+   * true. Otherwise a new process is spawned.
+   *
+   * @param args arguments to pass to the CLI. This should not include the
+   *   daffodil binary
+   * @param classpaths sequence of paths to add to the classpath. If non-empty,
+   *   runs the CLI in a new process instead of a thread and will likely decrease
+   *   performance
+   * @param fork if true, forces the the CLI in a new process
+   * @param timeout how long to wait, in seconds, for the CLI to exit after the
+   *   testFunc has returned. Also how long to wait for individual expect
+   *   operations in the CLITester
+   * @param debug if true, prints arguments and classpath information to
+   *   stdout. Also echos all CLITester input and output to stdout.
+   * @param testFunc function to call to send input to the CLI and validate
+   *   output from CLI stdout/stderr.
+   * @param expectedExitCode the expected exit code of the CLI. In the actual
+   *   exit code does not match
+   *
+   * @throws AssertionError if the actual exit code does not match the expected exit code
+   * @throws ExpectIOException if the an CLITester expect validation operation fails
+   */
+  def runCLI
+    (args: Array[String], classpaths: Seq[Path] = Seq(), fork: Boolean = false, timeout: Int = 10, debug: Boolean = false)
+    (testFunc: (CLITester) => Unit)
+    (expectedExitCode: ExitCode.Value): Unit = {
+
+    val (toIn, fromOut, fromErr, threadOrProc: Either[CLIThread, Process]) =
+      if (classpaths.nonEmpty || fork) {
+        // spawn a new process to run Daffodil, needed if a custom classpath is
+        // defined or if the caller explicitly wants to fork
+        val processBuilder = new ProcessBuilder()
+
+        if (classpaths.nonEmpty) {
+          val classpath = classpaths.mkString(File.pathSeparator)
+          if (debug) System.out.println(s"DAFFODIL_CLASSPATH=$classpath")
+          processBuilder.environment().put("DAFFODIL_CLASSPATH", classpath)
+        }
+
+        val cmd = daffodilBinPath.toString +: args
+        if (debug) System.out.println(cmd.mkString(" "))
+        processBuilder.command(cmd.toList.asJava)
+
+        val process = processBuilder.start()
+
+        val toIn = process.getOutputStream()
+        val fromOut = process.getInputStream()
+        val fromErr = process.getErrorStream()
+        (toIn, fromOut, fromErr, Right(process))
+      } else {
+        // create a new thread for the CLI test to run, using piped
+        // input/output streams to connected the thread and the CLItester
+        val in = new PipedInputStream()
+        val toIn = new PipedOutputStream(in)
+
+        val out = new PipedOutputStream()
+        val fromOut = new PipedInputStream(out)
+
+        val err = new PipedOutputStream()
+        val fromErr = new PipedInputStream(err)
+
+        if (debug) System.out.println("daffodil " + args.mkString(" "))
+
+        val thread = new CLIThread(args, in, out, err)
+        thread.start()
+        (toIn, fromOut, fromErr, Left(thread))
+      }
+
+    val eb = new ExpectBuilder()
+    eb.withOutput(toIn)
+    eb.withInputs(fromOut, fromErr)
+    eb.withInputFilters(replaceInString("\r\n", "\n"))
+    eb.withTimeout(timeout, TimeUnit.SECONDS)
+    eb.withExceptionOnFailure()
+    if (debug) {
+      eb.withEchoOutput(System.out)
+      eb.withEchoInput(System.out)
+    }
+    val expect = eb.build()
+    val tester = new CLITester(expect, toIn)
+
+    try {
+      testFunc(tester)
+    } finally {
+      threadOrProc match {
+        case Left(thread) => thread.join(timeout * 1000)
+        case Right(process) => process.waitFor(timeout, TimeUnit.SECONDS)
+      }
+      expect.close()
+      toIn.close()
+      fromOut.close()
+      fromErr.close()
     }
-  }
 
-  def makeMultipleCmds(cmds: Array[String]): String = {
-    if (isWindows) {
-      cmds.mkString(" & ")
-    } else {
-      cmds.mkString("; ")
+    val actualExitCode = threadOrProc match {
+      case Left(thread) => thread.exitCode
+      case Right(process) => ExitCode(process.exitValue)
     }
+    assertEquals("Incorrect exit code,", expectedExitCode, actualExitCode)
   }
 
-  def md5sum(blob_path: String): String = {
-    if (isWindows) {
-      String.format("certutil -hashfile %s MD5", blob_path)
-    } else {
-      String.format("md5sum %s", blob_path)
+  /**
+   * A class to run the CLI in a thread instead of a new process, given the
+   * arguments to use (excluded the daffodil binary) and streams to use for
+   * stdin/out/err.
+   */
+  private class CLIThread(args: Array[String], in: InputStream, out: OutputStream, err: OutputStream) extends Thread {
+    var exitCode = ExitCode.Failure
+
+    override def run(): Unit = {
+      val psOut = new PrintStream(out)
+      val psErr = new PrintStream(err)
+
+      // configure the CLI and log4j to use our custom streams, nothing should
+      // not actually use stdin/stdout/stderr
+      Main.setInputOutput(in, psOut, psErr)
+      configureLog4j(psErr)
+
+      exitCode = Main.run(args)
     }
-  }
 
-  def rmdir(path: String): String = {
-    if (Util.isWindows)
-      String.format("rmdir /Q /S %s", path)
-    else
-      String.format("rm -rf %s", path)
+    /**
+     * By default log4j outputs to stderr. This changes that so it writes to a
+     * provided PrintStream which is connected to the CLITester, allowing tests
+     * to expect contain written by log4j. This also defines the same pattern

Review Comment:
   contain -> content



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscribe@daffodil.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org