You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@daffodil.apache.org by ol...@apache.org on 2026/10/13 21:45:24 UTC

(daffodil) branch main updated: Add CLI option to pass options to infoset inputters and outputters

This is an automated email from the ASF dual-hosted git repository.

olabusayoT pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/daffodil.git


The following commit(s) were added to refs/heads/main by this push:
     new b5892e183 Add CLI option to pass options to infoset inputters and outputters
b5892e183 is described below

commit b5892e183284a0cd981d798541d45e0378611473
Author: olabusayoT <50...@users.noreply.github.com>
AuthorDate: Tue Oct 13 17:16:52 2026 -0400

    Add CLI option to pass options to infoset inputters and outputters
    
    Add --infoset-opts=key=value,... to the parse, unparse, and performance commands, parsed with Scallop's property converter. Each key is validated against the selected infoset type and collected into an InfosetOptions object that every infoset handler receives, so a new option needs only a key definition on InfosetOptions and a handler change.
    
    Initial keys are pretty (true or false, for xml, json, and sax), which previously could not be turned off, xmlTextEscape (Standard or CDATA, for xml), which exposes the existing XMLTextEscapeStyle, and exiCompression (true or false, for exi and exisa), which selects EXI compression. Unknown keys, invalid values, and keys that do not apply to the infoset type are reported as usage errors. The help lists the keys, their values, and defaults.
    
    The parse command now closes its output file and the performance command closes its input file, so files can be deleted on Windows when the CLI runs in the same process as a test.
    
    Library constructors are unchanged.
    
    DAFFODIL-2234
---
 .../org/apache/daffodil/cli/InfosetOptions.scala   | 123 +++++++++++++++++++++
 .../org/apache/daffodil/cli/InfosetTypes.scala     |  92 ++++++++++-----
 .../main/scala/org/apache/daffodil/cli/Main.scala  |  99 +++++++++++++++--
 .../daffodil/cli/cliTest/TestCLIParsing.scala      |  57 ++++++++++
 .../daffodil/cli/cliTest/TestCLIPerformance.scala  |  14 +++
 5 files changed, 348 insertions(+), 37 deletions(-)

diff --git a/daffodil-cli/src/main/scala/org/apache/daffodil/cli/InfosetOptions.scala b/daffodil-cli/src/main/scala/org/apache/daffodil/cli/InfosetOptions.scala
new file mode 100644
index 000000000..2f790c72b
--- /dev/null
+++ b/daffodil-cli/src/main/scala/org/apache/daffodil/cli/InfosetOptions.scala
@@ -0,0 +1,123 @@
+/*
+ * 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.cli
+
+import org.apache.daffodil.api.infoset.XMLTextEscapeStyle
+
+/**
+ * One supported key: the infoset types it applies to, its allowed values for help
+ * text, its default, and how to parse its value text (throwing an Exception with
+ * a message if the text is invalid). Every InfosetOptions creates its own keys, so
+ * keys with the same name are the same key.
+ */
+case class InfosetOptionKey[V](
+  name: String,
+  applicableInfosetTypes: Set[InfosetType.Type],
+  valueDescription: String,
+  defaultValue: V,
+  parseValue: String => V
+) {
+
+  override def equals(that: Any): Boolean = that match {
+    case key: InfosetOptionKey[?] => name == key.name
+    case _ => false
+  }
+
+  override def hashCode: Int = name.hashCode
+}
+
+type AnyInfosetOptionKey = InfosetOptionKey[?]
+
+/**
+ * The options given on the command line, as each key mapped to its parsed value.
+ */
+type ParsedInfosetOptions = Map[AnyInfosetOptionKey, Any]
+
+/**
+ * Options for infoset inputters and outputters, given on the command line as a
+ * list of key=value pairs. Each option is read from the parsed pairs once, when
+ * this is created, so using an option is a field access. A key that was not given
+ * has its default value.
+ */
+final case class InfosetOptions(parsed: ParsedInfosetOptions) {
+
+  private val optionKeys = scala.collection.mutable.ArrayBuffer.empty[AnyInfosetOptionKey]
+
+  val pretty: Boolean = value(
+    InfosetOptionKey(
+      name = "pretty",
+      applicableInfosetTypes = Set(InfosetType.XML, InfosetType.JSON, InfosetType.SAX),
+      valueDescription = "true|false",
+      defaultValue = true,
+      parseValue = _.toBoolean
+    )
+  )
+
+  val exiCompression: Boolean = value(
+    InfosetOptionKey(
+      name = "exiCompression",
+      applicableInfosetTypes = Set(InfosetType.EXI, InfosetType.EXISA),
+      valueDescription = "true|false",
+      defaultValue = false,
+      parseValue = _.toBoolean
+    )
+  )
+
+  val xmlTextEscapeStyle: XMLTextEscapeStyle = value(
+    InfosetOptionKey(
+      name = "xmlTextEscape",
+      applicableInfosetTypes = Set(InfosetType.XML),
+      valueDescription = XMLTextEscapeStyle.values.mkString("|"),
+      defaultValue = XMLTextEscapeStyle.Standard,
+      parseValue = text => InfosetOptions.enumValue(XMLTextEscapeStyle.values, text)
+    )
+  )
+
+  /**
+   * The keys of the options above.
+   */
+  lazy val supportedKeys: Seq[AnyInfosetOptionKey] = optionKeys.toSeq
+
+  private def value[V](key: InfosetOptionKey[V]): V = {
+    optionKeys += key
+    parsed.get(key).map(_.asInstanceOf[V]).getOrElse(key.defaultValue)
+  }
+}
+
+object InfosetOptions {
+
+  private def enumValue[E <: java.lang.Enum[E]](values: Array[E], text: String): E =
+    values
+      .find(_.toString.toLowerCase == text.toLowerCase)
+      .getOrElse(throw new Exception("Must be one of " + values.mkString(", ")))
+
+  val Default: InfosetOptions = InfosetOptions(Map.empty)
+
+  /**
+   * The supported keys.
+   */
+  val supportedKeys: Seq[AnyInfosetOptionKey] = Default.supportedKeys
+
+  /**
+   * Description of the supported keys, one per line, for help text.
+   */
+  val keysDescription: String =
+    supportedKeys
+      .map(k => "%s=%s (default: %s)".format(k.name, k.valueDescription, k.defaultValue))
+      .mkString("\n")
+}
diff --git a/daffodil-cli/src/main/scala/org/apache/daffodil/cli/InfosetTypes.scala b/daffodil-cli/src/main/scala/org/apache/daffodil/cli/InfosetTypes.scala
index d22085b3c..45bed77ae 100644
--- a/daffodil-cli/src/main/scala/org/apache/daffodil/cli/InfosetTypes.scala
+++ b/daffodil-cli/src/main/scala/org/apache/daffodil/cli/InfosetTypes.scala
@@ -51,6 +51,7 @@ import org.apache.daffodil.runtime1.infoset.XMLTextInfosetInputter
 import org.apache.daffodil.runtime1.infoset.XMLTextInfosetOutputter
 import org.apache.daffodil.runtime1.processors.DaffodilParseOutputStreamContentHandler
 
+import com.siemens.ct.exi.core.CodingMode
 import com.siemens.ct.exi.core.EXIFactory
 import com.siemens.ct.exi.core.helpers.DefaultEXIFactory
 import com.siemens.ct.exi.grammars.GrammarFactory
@@ -99,19 +100,20 @@ object InfosetType extends Enumeration {
     infosetType: InfosetType.Type,
     dataProcessor: api.DataProcessor,
     schemaUri: Option[URI],
-    forPerformance: Boolean
+    forPerformance: Boolean,
+    infosetOptions: InfosetOptions
   ): InfosetHandler = {
 
     infosetType match {
-      case InfosetType.EXI => EXIInfosetHandler(dataProcessor)
-      case InfosetType.EXISA => EXIInfosetHandler(dataProcessor, schemaUri.get)
-      case InfosetType.JDOM => JDOMInfosetHandler(dataProcessor)
-      case InfosetType.JSON => JsonInfosetHandler(dataProcessor)
-      case InfosetType.NULL => NULLInfosetHandler(dataProcessor)
-      case InfosetType.SAX => SAXInfosetHandler(dataProcessor, forPerformance)
-      case InfosetType.SCALA_XML => ScalaXMLInfosetHandler(dataProcessor)
-      case InfosetType.W3CDOM => W3CDOMInfosetHandler(dataProcessor)
-      case InfosetType.XML => XMLTextInfosetHandler(dataProcessor)
+      case InfosetType.EXI => EXIInfosetHandler(dataProcessor, infosetOptions)
+      case InfosetType.EXISA => EXIInfosetHandler(dataProcessor, schemaUri.get, infosetOptions)
+      case InfosetType.JDOM => JDOMInfosetHandler(dataProcessor, infosetOptions)
+      case InfosetType.JSON => JsonInfosetHandler(dataProcessor, infosetOptions)
+      case InfosetType.NULL => NULLInfosetHandler(dataProcessor, infosetOptions)
+      case InfosetType.SAX => SAXInfosetHandler(dataProcessor, forPerformance, infosetOptions)
+      case InfosetType.SCALA_XML => ScalaXMLInfosetHandler(dataProcessor, infosetOptions)
+      case InfosetType.W3CDOM => W3CDOMInfosetHandler(dataProcessor, infosetOptions)
+      case InfosetType.XML => XMLTextInfosetHandler(dataProcessor, infosetOptions)
     }
   }
 }
@@ -282,10 +284,17 @@ sealed class InfosetParseResult(val parseResult: api.ParseResult) {
 /**
  * InfosetType.XML
  */
-case class XMLTextInfosetHandler(dataProcessor: api.DataProcessor) extends InfosetHandler {
+case class XMLTextInfosetHandler(
+  dataProcessor: api.DataProcessor,
+  infosetOptions: InfosetOptions
+) extends InfosetHandler {
 
   def parse(input: InputSourceDataInputStream, os: OutputStream): InfosetParseResult = {
-    val output = new XMLTextInfosetOutputter(os, pretty = true)
+    val output = new XMLTextInfosetOutputter(
+      os,
+      infosetOptions.pretty,
+      infosetOptions.xmlTextEscapeStyle
+    )
     val pr = parseWithInfosetOutputter(input, output)
     new InfosetParseResult(pr)
   }
@@ -308,10 +317,13 @@ case class XMLTextInfosetHandler(dataProcessor: api.DataProcessor) extends Infos
 /**
  * InfosetType.JSON
  */
-case class JsonInfosetHandler(dataProcessor: api.DataProcessor) extends InfosetHandler {
+case class JsonInfosetHandler(
+  dataProcessor: api.DataProcessor,
+  infosetOptions: InfosetOptions
+) extends InfosetHandler {
 
   def parse(input: InputSourceDataInputStream, os: OutputStream): InfosetParseResult = {
-    val output = new JsonInfosetOutputter(os, pretty = true)
+    val output = new JsonInfosetOutputter(os, infosetOptions.pretty)
     val pr = parseWithInfosetOutputter(input, output)
     new InfosetParseResult(pr)
   }
@@ -334,7 +346,10 @@ case class JsonInfosetHandler(dataProcessor: api.DataProcessor) extends InfosetH
 /**
  * InfosetType.JDOM
  */
-case class JDOMInfosetHandler(dataProcessor: api.DataProcessor) extends InfosetHandler {
+case class JDOMInfosetHandler(
+  dataProcessor: api.DataProcessor,
+  infosetOptions: InfosetOptions
+) extends InfosetHandler {
 
   def parse(input: InputSourceDataInputStream, os: OutputStream): InfosetParseResult = {
     val output = new JDOMInfosetOutputter()
@@ -375,7 +390,10 @@ class JDOMInfosetParseResult(parseResult: api.ParseResult, output: JDOMInfosetOu
 /**
  * InfosetType.SCALA_XML
  */
-case class ScalaXMLInfosetHandler(dataProcessor: api.DataProcessor) extends InfosetHandler {
+case class ScalaXMLInfosetHandler(
+  dataProcessor: api.DataProcessor,
+  infosetOptions: InfosetOptions
+) extends InfosetHandler {
 
   def parse(input: InputSourceDataInputStream, os: OutputStream): InfosetParseResult = {
     val output = new ScalaXMLInfosetOutputter()
@@ -419,7 +437,10 @@ class ScalaXMLInfosetParseResult(
 /**
  * InfosetType.W3CDOM
  */
-case class W3CDOMInfosetHandler(dataProcessor: api.DataProcessor) extends InfosetHandler {
+case class W3CDOMInfosetHandler(
+  dataProcessor: api.DataProcessor,
+  infosetOptions: InfosetOptions
+) extends InfosetHandler {
 
   def parse(input: InputSourceDataInputStream, os: OutputStream): InfosetParseResult = {
     val output = new W3CDOMInfosetOutputter()
@@ -472,7 +493,10 @@ class W3CDOMInfosetParseResult(parseResult: api.ParseResult, output: W3CDOMInfos
 /**
  * InfosetType.NULL
  */
-case class NULLInfosetHandler(dataProcessor: api.DataProcessor) extends InfosetHandler {
+case class NULLInfosetHandler(
+  dataProcessor: api.DataProcessor,
+  infosetOptions: InfosetOptions
+) extends InfosetHandler {
 
   def parse(input: InputSourceDataInputStream, os: OutputStream): InfosetParseResult = {
     val output = new NullInfosetOutputter()
@@ -498,15 +522,18 @@ case class NULLInfosetHandler(dataProcessor: api.DataProcessor) extends InfosetH
 /**
  * InfosetType.SAX
  */
-case class SAXInfosetHandler(dataProcessor: api.DataProcessor, forPerformance: Boolean)
-  extends InfosetHandler {
+case class SAXInfosetHandler(
+  dataProcessor: api.DataProcessor,
+  forPerformance: Boolean,
+  infosetOptions: InfosetOptions
+) extends InfosetHandler {
 
   def parse(input: InputSourceDataInputStream, os: OutputStream): InfosetParseResult = {
     val contentHandler =
       if (forPerformance) {
         new DefaultHandler() // ignores all SAX events
       } else {
-        new DaffodilParseOutputStreamContentHandler(os, pretty = true)
+        new DaffodilParseOutputStreamContentHandler(os, infosetOptions.pretty)
       }
 
     val pr = parseWithSax(input, contentHandler)
@@ -679,19 +706,32 @@ case class SAXInfosetHandler(dataProcessor: api.DataProcessor, forPerformance: B
 object EXIInfosetHandler {
 
   /** non-schema aware EXI **/
-  def apply(dataProcessor: api.DataProcessor): InfosetHandler = {
-    val exiFactory = createEXIFactory(None)
+  def apply(
+    dataProcessor: api.DataProcessor,
+    infosetOptions: InfosetOptions
+  ): InfosetHandler = {
+    val exiFactory = createEXIFactory(None, infosetOptions)
     EXIInfosetHandler(dataProcessor, exiFactory)
   }
 
   /** schema aware EXI **/
-  def apply(dataProcessor: api.DataProcessor, schemaUri: URI): InfosetHandler = {
-    val exiFactory = createEXIFactory(Some(schemaUri))
+  def apply(
+    dataProcessor: api.DataProcessor,
+    schemaUri: URI,
+    infosetOptions: InfosetOptions
+  ): InfosetHandler = {
+    val exiFactory = createEXIFactory(Some(schemaUri), infosetOptions)
     EXIInfosetHandler(dataProcessor, exiFactory)
   }
 
-  def createEXIFactory(optSchema: Option[URI]): EXIFactory = {
+  def createEXIFactory(
+    optSchema: Option[URI],
+    infosetOptions: InfosetOptions
+  ): EXIFactory = {
     val exiFactory = DefaultEXIFactory.newInstance
+    if (infosetOptions.exiCompression) {
+      exiFactory.setCodingMode(CodingMode.COMPRESSION)
+    }
     if (optSchema.isDefined) {
       val grammarFactory = GrammarFactory.newInstance
       val grammar =
diff --git a/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala b/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala
index 6cb4db47a..0dd06bbda 100644
--- a/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala
+++ b/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala
@@ -191,6 +191,21 @@ class CLIConf(arguments: Array[String], stdout: PrintStream, stderr: PrintStream
       }
     })
 
+  implicit def infosetOptionsConverter: ValueConverter[InfosetOptions] =
+    propsConverter[String](stringConverter).map { pairs =>
+      InfosetOptions(pairs.map { case (name, text) =>
+        val key = InfosetOptions.supportedKeys
+          .find(_.name == name)
+          .getOrElse(
+            throw new Exception(
+              "Unrecognized infoset option '%s'. Must be one of %s"
+                .format(name, InfosetOptions.supportedKeys.map(_.name).mkString(", "))
+            )
+          )
+        key -> key.parseValue(text)
+      })
+    }
+
   implicit def implementationConverter: ValueConverter[TDMLImplementation] =
     singleArgConverter[TDMLImplementation]((s: String) => {
       val optImplementation = TDMLImplementation.optionStringToEnum("implementation", s)
@@ -361,6 +376,14 @@ class CLIConf(arguments: Array[String], stdout: PrintStream, stderr: PrintStream
       ) + ". Defaults to 'xml'.",
       default = Some(InfosetType.XML)
     )
+    val infosetOptions = opt[InfosetOptions](
+      name = "infoset-opts",
+      noshort = true,
+      argName = "key=value,...",
+      descr = "List of key=value options for the infoset inputter or outputter. Keys:\n" +
+        InfosetOptions.keysDescription,
+      default = Some(InfosetOptions.Default)
+    )
     val output = opt[String](
       argName = "file",
       descr =
@@ -439,6 +462,18 @@ class CLIConf(arguments: Array[String], stdout: PrintStream, stderr: PrintStream
     // --config must be a file that exists
     validateFileIsFile(config)
 
+    validateOpt(infosetType, infosetOptions) {
+      case (Some(t), Some(options)) =>
+        options.parsed.keys.find(!_.applicableInfosetTypes.contains(t)) match {
+          case Some(key) =>
+            Left(
+              "Infoset option '%s' does not apply to the %s infoset type".format(key.name, t)
+            )
+          case None => Right(())
+        }
+      case _ => Right(())
+    }
+
     validateOpt(debug, infile) {
       case (Some(_), Some("-")) | (Some(_), None) =>
         Left("Input must not be stdin during interactive debugging")
@@ -488,6 +523,14 @@ class CLIConf(arguments: Array[String], stdout: PrintStream, stderr: PrintStream
       ) + ". Defaults to 'xml'.",
       default = Some(InfosetType.XML)
     )
+    val infosetOptions = opt[InfosetOptions](
+      name = "infoset-opts",
+      noshort = true,
+      argName = "key=value,...",
+      descr = "List of key=value options for the infoset inputter or outputter. Keys:\n" +
+        InfosetOptions.keysDescription,
+      default = Some(InfosetOptions.Default)
+    )
     val output = opt[String](
       argName = "file",
       descr = "Output file to write data to. If not given or is -, data is written to stdout."
@@ -564,6 +607,18 @@ class CLIConf(arguments: Array[String], stdout: PrintStream, stderr: PrintStream
     // --config must be a file that exists
     validateFileIsFile(config)
 
+    validateOpt(infosetType, infosetOptions) {
+      case (Some(t), Some(options)) =>
+        options.parsed.keys.find(!_.applicableInfosetTypes.contains(t)) match {
+          case Some(key) =>
+            Left(
+              "Infoset option '%s' does not apply to the %s infoset type".format(key.name, t)
+            )
+          case None => Right(())
+        }
+      case _ => Right(())
+    }
+
     validateOpt(debug, infile) {
       case (Some(_), Some("-")) | (Some(_), None) =>
         Left("Input must not be stdin during interactive debugging")
@@ -719,6 +774,14 @@ class CLIConf(arguments: Array[String], stdout: PrintStream, stderr: PrintStream
       ) + ". Defaults to 'xml'.",
       default = Some(InfosetType.XML)
     )
+    val infosetOptions = opt[InfosetOptions](
+      name = "infoset-opts",
+      noshort = true,
+      argName = "key=value,...",
+      descr = "List of key=value options for the infoset inputter or outputter. Keys:\n" +
+        InfosetOptions.keysDescription,
+      default = Some(InfosetOptions.Default)
+    )
     val number = opt[Int](
       short = 'N',
       argName = "number",
@@ -794,6 +857,18 @@ class CLIConf(arguments: Array[String], stdout: PrintStream, stderr: PrintStream
         Left("A schema must be specified to use schema-aware compression with EXI")
       case _ => Right(())
     }
+
+    validateOpt(infosetType, infosetOptions) {
+      case (Some(t), Some(options)) =>
+        options.parsed.keys.find(!_.applicableInfosetTypes.contains(t)) match {
+          case Some(key) =>
+            Left(
+              "Infoset option '%s' does not apply to the %s infoset type".format(key.name, t)
+            )
+          case None => Right(())
+        }
+      case _ => Right(())
+    }
   }
 
   // Generate Subcommand Options
@@ -1263,17 +1338,19 @@ class Main(
                 }
               }
             }
-            Using.resource(input) { inStream =>
+            Using.Manager { use =>
+              val inStream = use(input)
               val output = parseOpts.output.toOption match {
                 case Some("-") | None => STDOUT
-                case Some(file) => new FileOutputStream(file)
+                case Some(file) => use(new FileOutputStream(file))
               }
 
               val infosetHandler = InfosetType.getInfosetHandler(
                 parseOpts.infosetType(),
                 processor,
                 parseOpts.schema.map(_.uri).toOption,
-                forPerformance = false
+                forPerformance = false,
+                infosetOptions = parseOpts.infosetOptions()
               )
 
               var lastParseBitPosition = 0L
@@ -1399,7 +1476,7 @@ class Main(
                 }
               }
               exitCode
-            }
+            }.get
           }
         }
         rc
@@ -1452,7 +1529,8 @@ class Main(
               performanceOpts.infosetType(),
               processor,
               performanceOpts.schema.map(_.uri).toOption,
-              forPerformance = true
+              forPerformance = true,
+              infosetOptions = performanceOpts.infosetOptions()
             )
 
             val isUnparse = performanceOpts.unparse()
@@ -1465,10 +1543,7 @@ class Main(
                 // same input data multiple times in different performance runs.
                 // So read the file data into an Array[Byte], and use that for
                 // everything.
-                val input = (new FileInputStream(filePath))
-                val dataSize = filePath.length()
-                val bytes = new Array[Byte](dataSize.toInt)
-                input.read(bytes)
+                val bytes = Files.readAllBytes(filePath.toPath)
                 val data =
                   if (isUnparse) {
                     infosetHandler.dataToInfoset(bytes)
@@ -1614,7 +1689,8 @@ class Main(
               unparseOpts.infosetType(),
               processor,
               unparseOpts.schema.map(_.uri).toOption,
-              forPerformance = false
+              forPerformance = false,
+              infosetOptions = unparseOpts.infosetOptions()
             )
 
             while (keepUnparsing) {
@@ -1899,7 +1975,8 @@ class Main(
           try {
             Some(
               EXIInfosetHandler.createEXIFactory(
-                exiOpts.schema.map(_.uri).toOption
+                exiOpts.schema.map(_.uri).toOption,
+                InfosetOptions.Default
               )
             )
           } catch {
diff --git a/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/TestCLIParsing.scala b/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/TestCLIParsing.scala
index 17d8b99de..6bc68f1d8 100644
--- a/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/TestCLIParsing.scala
+++ b/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/TestCLIParsing.scala
@@ -878,6 +878,63 @@ class TestCLIParsing {
     }(ExitCode.Success)
   }
 
+  @Test def test_CLI_Parsing_InfosetOption_exiCompression(): Unit = {
+    val schema = path(
+      "daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd"
+    )
+
+    withTempDir { tempDir =>
+      val plain = java.nio.file.Paths.get(tempDir.toString, "plain.exi")
+      val compressed = java.nio.file.Paths.get(tempDir.toString, "compressed.exi")
+
+      runCLI(args"parse -I exi -s $schema -r e1 -o $plain") { cli =>
+        cli.send("Hello", inputDone = true)
+      }(ExitCode.Success)
+
+      runCLI(
+        args"parse -I exi --infoset-opts=exiCompression=true -s $schema -r e1 -o $compressed"
+      ) { cli =>
+        cli.send("Hello", inputDone = true)
+      }(ExitCode.Success)
+
+      assertFalse(
+        java.util.Arrays.equals(
+          java.nio.file.Files.readAllBytes(plain),
+          java.nio.file.Files.readAllBytes(compressed)
+        )
+      )
+
+      runCLI(
+        args"unparse -I exi --infoset-opts=exiCompression=true -s $schema --root e1 $compressed"
+      ) { cli =>
+        cli.expect("Hello")
+      }(ExitCode.Success)
+    }
+  }
+
+  @Test def test_CLI_Parsing_InfosetOption_json_notPretty(): Unit = {
+    val schema = path(
+      "daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd"
+    )
+
+    runCLI(args"parse -I json --infoset-opts=pretty=false -s $schema -r e1") { cli =>
+      cli.send("Hello", inputDone = true)
+      cli.expect("""{"e1": "Hello"}""")
+    }(ExitCode.Success)
+  }
+
+  @Test def test_CLI_Parsing_InfosetOption_xml_cdata(): Unit = {
+    val schema = path(
+      "daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd"
+    )
+
+    runCLI(args"parse --infoset-opts=xmlTextEscape=CDATA,pretty=false -s $schema -r e1") {
+      cli =>
+        cli.send("a b c", inputDone = true)
+        cli.expect("<![CDATA[a b c]]>")
+    }(ExitCode.Success)
+  }
+
   @Test def test_CLI_Parsing_SimpleParse_scala_xml(): Unit = {
     val schema = path(
       "daffodil-test/src/test/resources/org/apache/daffodil/section00/general/generalSchema.dfdl.xsd"
diff --git a/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/TestCLIPerformance.scala b/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/TestCLIPerformance.scala
index 8fbb507e6..438973cfd 100644
--- a/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/TestCLIPerformance.scala
+++ b/daffodil-cli/src/test/scala/org/apache/daffodil/cli/cliTest/TestCLIPerformance.scala
@@ -60,6 +60,20 @@ class TestCLIPerformance {
     }(ExitCode.Success)
   }
 
+  @Test def test_CLI_Performance_2_Threads_2_Times_exi_compression(): Unit = {
+    val schema = path(
+      "daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd"
+    )
+    val input = path("daffodil-cli/src/test/resources/org/apache/daffodil/cli/input/input1.txt")
+
+    runCLI(
+      args"performance -I exi --infoset-opts=exiCompression=true -N 2 -t 2 -s $schema -r matrix $input"
+    ) { cli =>
+      cli.expect("total parse time (sec):")
+      cli.expect("average throughput (files/sec):")
+    }(ExitCode.Success)
+  }
+
   @Test def test_CLI_Performance_2_Threads_2_Times_exisa(): Unit = {
     val schema = path(
       "daffodil-test/src/test/resources/org/apache/daffodil/section06/entities/charClassEntities.dfdl.xsd"