You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@creadur.apache.org by cl...@apache.org on 2026/03/22 15:33:05 UTC

(creadur-rat) branch create-UIOptionCollections created (now 2a05d1a9)

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

claude pushed a change to branch create-UIOptionCollections
in repository https://gitbox.apache.org/repos/asf/creadur-rat.git


      at 2a05d1a9 modified OptionCollection to use CLIOptionCollection

This branch includes the following new commits:

     new 3e47c432 initial working code
     new 2a05d1a9 modified OptionCollection to use CLIOptionCollection

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(creadur-rat) 02/02: modified OptionCollection to use CLIOptionCollection

Posted by cl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

claude pushed a commit to branch create-UIOptionCollections
in repository https://gitbox.apache.org/repos/asf/creadur-rat.git

commit 2a05d1a96d165cfbb329cf0fb6142fe529d055f0
Author: Claude Warren <cl...@xenei.com>
AuthorDate: Sun Mar 22 15:32:30 2026 +0000

    modified OptionCollection to use CLIOptionCollection
---
 apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java b/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java
index b16ebd09..e083ec08 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java
@@ -200,7 +200,7 @@ public final class OptionCollection {
      * @return the Options comprised of the Options defined in this class.
      */
     public static Options buildOptions() {
-        return Arg.getOptions().addOption(HELP);
+        return CLIOptionCollection.INSTANCE.getOptions();
     }
 
     /**


(creadur-rat) 01/02: initial working code

Posted by cl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

claude pushed a commit to branch create-UIOptionCollections
in repository https://gitbox.apache.org/repos/asf/creadur-rat.git

commit 3e47c4326df52e397032f7822426b6542f0b7b29
Author: Claude Warren <cl...@xenei.com>
AuthorDate: Sun Mar 22 08:21:54 2026 +0000

    initial working code
---
 apache-rat-core/pom.xml                            |  25 +-
 .../src/main/java/org/apache/rat}/CLIOption.java   |  20 +-
 .../java/org/apache/rat/CLIOptionCollection.java   |  41 ++
 .../main/java/org/apache/rat/OptionCollection.java |  12 +-
 ...Collection.java => OptionCollectionParser.java} | 156 ++---
 .../main/java/org/apache/rat/commandline/Arg.java  | 703 ++++++++++-----------
 .../apache/rat/commandline/ArgumentContext.java    |   7 +-
 .../java/org/apache/rat/help/AbstractHelp.java     |   5 +-
 .../org/apache/rat/ui/AbstractCodeGenerator.java   | 151 +++++
 .../java/org/apache/rat/ui/ArgumentTracker.java    | 239 +++++++
 .../src/main/java/org/apache/rat/ui/UI.java        |  36 ++
 .../src/main/java/org/apache/rat/ui/UIOption.java  | 224 +++++++
 .../java/org/apache/rat/ui/UIOptionCollection.java | 299 +++++++++
 .../org/apache/rat/ui/UpdatableOptionGroup.java    |  85 +++
 .../rat/ui/UpdatableOptionGroupCollection.java     | 110 ++++
 .../main/java/org/apache/rat/ui/package-info.java  |  23 +
 .../java/org/apache/rat/ui/spi/UIProvider.java     |  25 +
 .../java/org/apache/rat/ui/spi/package-info.java   |  22 +
 .../java/org/apache/rat/commandline/ArgTests.java  |   3 +-
 .../org/apache/rat/ui/ArgumentTrackerTest.java     | 122 ++++
 .../org/apache/rat/ui/UIOptionCollectionTest.java  | 160 +++++
 .../rat/documentation/options/CLIOption.java       |   2 +-
 pom.xml                                            |  10 +
 23 files changed, 1983 insertions(+), 497 deletions(-)

diff --git a/apache-rat-core/pom.xml b/apache-rat-core/pom.xml
index 4819f23b..2cce9a2a 100644
--- a/apache-rat-core/pom.xml
+++ b/apache-rat-core/pom.xml
@@ -37,7 +37,6 @@
         <directory>src/main/filtered-resources</directory>
       </resource>
     </resources>
-    <pluginManagement>
       <plugins>
         <plugin>
           <groupId>org.apache.rat</groupId>
@@ -56,9 +55,6 @@
             </inputExcludes>
           </configuration>
         </plugin>
-      </plugins>
-    </pluginManagement>
-    <plugins>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-jar-plugin</artifactId>
@@ -177,8 +173,20 @@
   </build>
   <dependencies>
     <dependency>
-      <groupId>org.apache.rat</groupId>
-      <artifactId>apache-rat-testdata</artifactId>
+      <groupId>com.github.spotbugs</groupId>
+      <artifactId>spotbugs-annotations</artifactId>
+    </dependency>
+    <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity-engine-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.velocity.tools</groupId>
+            <artifactId>velocity-tools-generic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.reflections</groupId>
+            <artifactId>reflections</artifactId>
       <scope>test</scope>
     </dependency>
     <dependency>
@@ -213,11 +221,6 @@
       <artifactId>junit-jupiter-api</artifactId>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>org.junit.vintage</groupId>
-      <artifactId>junit-vintage-engine</artifactId>
-      <scope>test</scope>
-    </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-params</artifactId>
diff --git a/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java b/apache-rat-core/src/main/java/org/apache/rat/CLIOption.java
similarity index 81%
copy from apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java
copy to apache-rat-core/src/main/java/org/apache/rat/CLIOption.java
index fc00e5e1..03b79044 100644
--- a/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/CLIOption.java
@@ -16,19 +16,21 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.rat.documentation.options;
+package org.apache.rat;
 
 import org.apache.commons.cli.Option;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.rat.ui.ArgumentTracker;
+import org.apache.rat.ui.UIOption;
+import org.apache.rat.ui.UIOptionCollection;
 
-public class CLIOption extends AbstractOption {
-
-    public static String createName(final Option option) {
-        return StringUtils.defaultIfBlank(option.getLongOpt(), option.getOpt());
-    }
+/**
+ * The CLI option definition.
+ */
+public final class CLIOption extends UIOption<CLIOption> {
 
-    public CLIOption(final Option option) {
-        super(option, createName(option));
+    public CLIOption(final UIOptionCollection<CLIOption> collection, final Option option) {
+        super(collection, option, ArgumentTracker.extractKey(option));
     }
 
     @Override
@@ -47,7 +49,7 @@ public class CLIOption extends AbstractOption {
 
     @Override
     protected String cleanupName(final Option option) {
-        return createName(option);
+        return ArgumentTracker.extractKey(option);
     }
 
     @Override
diff --git a/apache-rat-core/src/main/java/org/apache/rat/CLIOptionCollection.java b/apache-rat-core/src/main/java/org/apache/rat/CLIOptionCollection.java
new file mode 100644
index 00000000..feb1cbf5
--- /dev/null
+++ b/apache-rat-core/src/main/java/org/apache/rat/CLIOptionCollection.java
@@ -0,0 +1,41 @@
+/*
+ * 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
+ *
+ *   https://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.rat;
+
+import org.apache.commons.cli.Option;
+import org.apache.rat.ui.UIOptionCollection;
+
+public final class CLIOptionCollection extends UIOptionCollection<CLIOption> {
+    /** The Help option */
+    static final Option HELP = new Option("?", "help", false, "Print help for the RAT command line interface and exit.");
+
+    /** The instance of the collection */
+    public static final CLIOptionCollection INSTANCE = new CLIOptionCollection();
+
+    private CLIOptionCollection() {
+        super(new Builder().uiOption(HELP)
+                .mapper(CLIOption::new));
+    }
+
+    private static final class Builder extends UIOptionCollection.Builder<CLIOption, Builder> {
+        private Builder() {
+            super();
+        }
+    }
+}
diff --git a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java b/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java
index 6ce2e1c2..b16ebd09 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java
@@ -140,9 +140,8 @@ public final class OptionCollection {
             // for "commandLine"
         }
 
-        Arg.processLogLevel(commandLine);
-
         ArgumentContext argumentContext = new ArgumentContext(workingDirectory, commandLine);
+        Arg.processLogLevel(argumentContext, CLIOptionCollection.INSTANCE);
 
         if (commandLine.hasOption(HELP)) {
             helpCmd.accept(opts);
@@ -175,14 +174,15 @@ public final class OptionCollection {
      * @see #parseCommands(File, String[], Consumer, boolean)
      */
     static ReportConfiguration createConfiguration(final ArgumentContext argumentContext) {
-        argumentContext.processArgs();
+        argumentContext.processArgs(CLIOptionCollection.INSTANCE);
         final ReportConfiguration configuration = argumentContext.getConfiguration();
         final CommandLine commandLine = argumentContext.getCommandLine();
-        if (Arg.DIR.isSelected()) {
+        if (CLIOptionCollection.INSTANCE.isSelected(Arg.DIR)) {
             try {
-                configuration.addSource(getReportable(commandLine.getParsedOptionValue(Arg.DIR.getSelected()), configuration));
+                configuration.addSource(getReportable(commandLine.getParsedOptionValue(
+                        CLIOptionCollection.INSTANCE.getSelected(Arg.DIR).get()), configuration));
             } catch (ParseException e) {
-                throw new ConfigurationException("Unable to set parse " + Arg.DIR.getSelected(), e);
+                throw new ConfigurationException("Unable to set parse " + CLIOptionCollection.INSTANCE.getSelected(Arg.DIR).get(), e);
             }
         }
         for (String s : commandLine.getArgs()) {
diff --git a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java b/apache-rat-core/src/main/java/org/apache/rat/OptionCollectionParser.java
similarity index 70%
copy from apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java
copy to apache-rat-core/src/main/java/org/apache/rat/OptionCollectionParser.java
index 6ce2e1c2..5798b738 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/OptionCollectionParser.java
@@ -21,14 +21,11 @@ package org.apache.rat;
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.io.Serial;
 import java.io.Serializable;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Comparator;
-import java.util.Map;
-import java.util.TreeMap;
-import java.util.function.Consumer;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -49,49 +46,32 @@ import org.apache.rat.help.Licenses;
 import org.apache.rat.license.LicenseSetFactory;
 import org.apache.rat.report.IReportable;
 import org.apache.rat.report.claim.ClaimStatistic;
+import org.apache.rat.ui.UIOptionCollection;
 import org.apache.rat.utils.DefaultLog;
 import org.apache.rat.utils.Log.Level;
 import org.apache.rat.walker.ArchiveWalker;
 import org.apache.rat.walker.DirectoryWalker;
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
 import static java.lang.String.format;
 
 /**
- * The collection of standard options for the CLI as well as utility methods to manage them and methods to create the
- * ReportConfiguration from the options and an array of arguments.
+ * Uses the AbstractOptionCollection to parse the command line options.
+ * contains utility methods to ReportConfiguration from the options and an array of arguments.
  */
-public final class OptionCollection {
+@SuppressFBWarnings("EI_EXPOSE_REP2")
+public final class OptionCollectionParser {
+    /** The OptionCollection that we are working with */
+    private final UIOptionCollection<?> uiOptionCollection;
 
-    private OptionCollection() {
-        // do not instantiate
+    public OptionCollectionParser(final UIOptionCollection<?> optionCollection) {
+        this.uiOptionCollection = optionCollection;
     }
 
     /** The Option comparator to sort the help */
     public static final Comparator<Option> OPTION_COMPARATOR = new OptionComparator();
 
-    /** The Help option */
-    public static final Option HELP = new Option("?", "help", false, "Print help for the RAT command line interface and exit.");
-
-    /** A mapping of {@code argName(value)} values to a description of those values. */
-    @Deprecated
-    private static final Map<String, Supplier<String>> ARGUMENT_TYPES;
-    static {
-        ARGUMENT_TYPES = new TreeMap<>();
-        for (ArgumentType argType : ArgumentType.values()) {
-            ARGUMENT_TYPES.put(argType.getDisplayName(), argType.description);
-        }
-    }
-
-    /**
-     * Gets the mapping of {@code argName(value)} values to a description of those values.
-     * @return the mapping of {@code argName(value)} values to a description of those values.
-     * @deprecated use {@link ArgumentType}
-     */
-    @Deprecated
-    public static Map<String, Supplier<String>> getArgumentTypes() {
-        return Collections.unmodifiableMap(ARGUMENT_TYPES);
-    }
-
     /**
      * Join a collection of objects together as a comma separated list of their string values.
      * @param args the objects to join together.
@@ -106,63 +86,58 @@ public final class OptionCollection {
      *
      * @param workingDirectory The directory to resolve relative file names against.
      * @param args the arguments to parse
-     * @param helpCmd the help command to run when necessary.
-     * @return a ReportConfiguration or {@code null} if Help was printed.
+     * @return the ArgumentContext for the process.
      * @throws IOException on error.
+     * @throws ParseException on option parsing error.
      */
-    public static ReportConfiguration parseCommands(final File workingDirectory, final String[] args, final Consumer<Options> helpCmd) throws IOException {
-        return parseCommands(workingDirectory, args, helpCmd, false);
+    public ArgumentContext parseCommands(final File workingDirectory, final String[] args)
+            throws IOException, ParseException {
+        return parseCommands(workingDirectory, args, uiOptionCollection.getOptions());
     }
 
     /**
-     * Parses the standard options to create a ReportConfiguration.
-     *
-     * @param workingDirectory The directory to resolve relative file names against.
-     * @param args the arguments to parse.
-     * @param helpCmd the help command to run when necessary.
-     * @param noArgs If {@code true} then the commands do not need extra arguments.
-     * @return a ReportConfiguration or {@code null} if Help was printed.
-     * @throws IOException on error.
+     * Parse the options into the command line.
+     * @param opts the option definitions.
+     * @param args the argument to apply the definitions to.
+     * @return the CommandLine
+     * @throws ParseException on option parsing error.
      */
-    public static ReportConfiguration parseCommands(final File workingDirectory, final String[] args,
-                                                    final Consumer<Options> helpCmd, final boolean noArgs) throws IOException {
-
-        Options opts = buildOptions();
-        CommandLine commandLine;
+    //@VisibleForTesting
+    CommandLine parseCommandLine(final Options opts, final String[] args) throws ParseException {
         try {
-            commandLine = DefaultParser.builder().setDeprecatedHandler(DeprecationReporter.getLogReporter())
+            return DefaultParser.builder().setDeprecatedHandler(DeprecationReporter.getLogReporter())
                     .setAllowPartialMatching(true).build().parse(opts, args);
         } catch (ParseException e) {
             DefaultLog.getInstance().error(e.getMessage());
             DefaultLog.getInstance().error("Please use the \"--help\" option to see a list of valid commands and options.", e);
-            System.exit(1);
-            return null; // dummy return (won't be reached) to avoid Eclipse complaint about possible NPE
-            // for "commandLine"
+            throw e;
         }
+    }
 
-        Arg.processLogLevel(commandLine);
+    /**
+     * Parses the standard options to create a ReportConfiguration.
+     *
+     * @param workingDirectory The directory to resolve relative file names against.
+     * @param args the arguments to parse.
+     * @param options An Options object containing Apache command line options.
+     * @return the ArgumentContext for the process.
+     * @throws IOException on error.
+     * @throws ParseException on option parsing error.
+     */
+    private ArgumentContext parseCommands(final File workingDirectory, final String[] args,
+                                                                       final Options options) throws IOException, ParseException {
 
+        CommandLine commandLine = parseCommandLine(options, args);
         ArgumentContext argumentContext = new ArgumentContext(workingDirectory, commandLine);
-
-        if (commandLine.hasOption(HELP)) {
-            helpCmd.accept(opts);
-            return null;
+        Arg.processLogLevel(argumentContext, uiOptionCollection);
+        populateConfiguration(argumentContext);
+        if (uiOptionCollection.isSelected(Arg.HELP_LICENSES)) {
+            new Licenses(argumentContext.getConfiguration(),
+                    new PrintWriter(argumentContext.getConfiguration().getOutput().get(),
+                            false, StandardCharsets.UTF_8)).printHelp();
         }
 
-        if (commandLine.hasOption(Arg.HELP_LICENSES.option())) {
-            new Licenses(createConfiguration(argumentContext), new PrintWriter(System.out, false, StandardCharsets.UTF_8)).printHelp();
-            return null;
-        }
-
-        ReportConfiguration configuration = createConfiguration(argumentContext);
-        if (!noArgs && !configuration.hasSource()) {
-            String msg = "No directories or files specified for scanning. Did you forget to close a multi-argument option?";
-            DefaultLog.getInstance().error(msg);
-            helpCmd.accept(opts);
-            return null;
-        }
-
-        return configuration;
+        return argumentContext;
     }
 
     /**
@@ -171,38 +146,22 @@ public final class OptionCollection {
      * You probably want one of the {@code ParseCommands} methods.
      * @param argumentContext The context to execute in.
      * @return a ReportConfiguration
-     * @see #parseCommands(File, String[], Consumer)
-     * @see #parseCommands(File, String[], Consumer, boolean)
      */
-    static ReportConfiguration createConfiguration(final ArgumentContext argumentContext) {
-        argumentContext.processArgs();
+    private ReportConfiguration populateConfiguration(final ArgumentContext argumentContext) {
+        argumentContext.processArgs(uiOptionCollection);
         final ReportConfiguration configuration = argumentContext.getConfiguration();
         final CommandLine commandLine = argumentContext.getCommandLine();
-        if (Arg.DIR.isSelected()) {
-            try {
-                configuration.addSource(getReportable(commandLine.getParsedOptionValue(Arg.DIR.getSelected()), configuration));
-            } catch (ParseException e) {
-                throw new ConfigurationException("Unable to set parse " + Arg.DIR.getSelected(), e);
-            }
-        }
-        for (String s : commandLine.getArgs()) {
-            IReportable reportable = getReportable(new File(s), configuration);
-            if (reportable != null) {
-                configuration.addSource(reportable);
+        if (!configuration.hasSource()) {
+            for (String s : commandLine.getArgs()) {
+                IReportable reportable = getReportable(new File(s), configuration);
+                if (reportable != null) {
+                    configuration.addSource(reportable);
+                }
             }
         }
         return configuration;
     }
 
-    /**
-     * Create an {@code Options} object from the list of defined Options.
-     * Mutually exclusive options must be listed in an OptionGroup.
-     * @return the Options comprised of the Options defined in this class.
-     */
-    public static Options buildOptions() {
-        return Arg.getOptions().addOption(HELP);
-    }
-
     /**
      * Creates an IReportable object from the directory name and ReportConfiguration
      * object.
@@ -211,7 +170,7 @@ public final class OptionCollection {
      * @param config the ReportConfiguration.
      * @return the IReportable instance containing the files.
      */
-    static IReportable getReportable(final File base, final ReportConfiguration config) {
+    IReportable getReportable(final File base, final ReportConfiguration config) {
         File absBase = base.getAbsoluteFile();
         DocumentName documentName = DocumentName.builder(absBase).build();
         if (!absBase.exists()) {
@@ -238,6 +197,7 @@ public final class OptionCollection {
      */
     private static final class OptionComparator implements Comparator<Option>, Serializable {
         /** The serial version UID.  */
+        @Serial
         private static final long serialVersionUID = 5305467873966684014L;
 
         private String getKey(final Option opt) {
@@ -301,7 +261,7 @@ public final class OptionCollection {
         /**
          * A style sheet.
          */
-        STYLESHEET("StyleSheet", () -> format("Either an external xsl file or one of the internal named sheets. Internal sheets are: %n%s",
+        STYLESHEET("StyleSheet", () -> format("Either an external XSLT file or one of the internal named sheets. Internal sheets are: %n%s",
                 Arrays.stream(StyleSheets.values())
                         .map(v -> format("\t%s: %s%n", v.arg(), v.desc()))
                         .collect(Collectors.joining(System.lineSeparator())))),
diff --git a/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java b/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java
index a9a2d5e7..c7888b47 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/commandline/Arg.java
@@ -21,15 +21,13 @@ package org.apache.rat.commandline;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.lang.reflect.Array;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
+import java.util.function.BiConsumer;
 import java.util.function.Predicate;
 
 import org.apache.commons.cli.AlreadySelectedException;
@@ -40,10 +38,11 @@ import org.apache.commons.cli.OptionGroup;
 import org.apache.commons.cli.Options;
 import org.apache.commons.cli.ParseException;
 import org.apache.commons.io.IOUtils;
-import org.apache.commons.io.function.IOSupplier;
+import org.apache.commons.io.output.CloseShieldOutputStream;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.rat.ConfigurationException;
 import org.apache.rat.Defaults;
+import org.apache.rat.ImplementationException;
 import org.apache.rat.ReportConfiguration;
 import org.apache.rat.config.AddLicenseHeaders;
 import org.apache.rat.config.exclusion.ExclusionUtils;
@@ -52,6 +51,7 @@ import org.apache.rat.document.DocumentName;
 import org.apache.rat.document.DocumentNameMatcher;
 import org.apache.rat.license.LicenseSetFactory;
 import org.apache.rat.report.claim.ClaimStatistic.Counter;
+import org.apache.rat.ui.UIOptionCollection;
 import org.apache.rat.utils.DefaultLog;
 import org.apache.rat.utils.Log;
 
@@ -64,7 +64,6 @@ import static java.lang.String.format;
  * This allows us to deprecate options as we move forward in development.
  */
 public enum Arg {
-
     ///////////////////////// EDIT OPTIONS
     /**
      * Defines options to add copyright to files
@@ -79,7 +78,10 @@ public enum Arg {
             .addOption(Option.builder().longOpt("edit-copyright").hasArg()
                     .desc("The copyright message to use in the license headers. Usually in the form of \"Copyright 2008 Foo\".  "
                             + "Only valid with --edit-license")
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                throw new ImplementationException(String.format("'%s' should not be executed directly", selected));
+            }),
 
     /**
      * Causes file updates to overwrite existing files.
@@ -93,7 +95,11 @@ public enum Arg {
             .addOption(Option.builder().longOpt("edit-overwrite")
                     .desc("Forces any changes in files to be written directly to the source files so that new files are not created. "
                             + "Only valid with --edit-license.")
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                throw new ImplementationException(String.format("'%s' should not be executed directly", selected));
+            }
+    ),
 
     /**
      * Defines options to add licenses to files
@@ -112,7 +118,11 @@ public enum Arg {
                     "Add the Apache-2.0 license header to any file with an unknown license that is not in the exclusion list. "
                             + "By default new files will be created with the license header, "
                             + "to force the modification of existing files use the --edit-overwrite option.").build()
-            )),
+            ),
+            (context, selected) -> {
+                throw new ImplementationException(String.format("'%s' should not be executed directly", selected));
+            }
+    ),
 
     //////////////////////////// CONFIGURATION OPTIONS
     /**
@@ -129,7 +139,11 @@ public enum Arg {
                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--config")).get())
                     .converter(Converters.FILE_CONVERTER)
                     .type(File.class)
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                throw new ImplementationException(String.format("'%s' should not be executed directly", selected));
+            }
+    ),
 
     /**
      * Group of options that skip the default configuration file
@@ -143,16 +157,22 @@ public enum Arg {
                             .setForRemoval(true)
                             .setDescription(StdMsgs.useMsg("--configuration-no-defaults")).get())
                     .desc("Ignore default configuration.")
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                throw new ImplementationException(String.format("'%s' should not be executed directly", selected));
+            }),
 
     /**
      * Option that adds approved licenses to the list
      */
     LICENSES_APPROVED(new OptionGroup().addOption(Option.builder().longOpt("licenses-approved").hasArg().argName("LicenseID")
             .desc("A comma separated list of approved License IDs. These licenses will be added to the list of approved licenses.")
-                    .converter(Converters.TEXT_LIST_CONVERTER)
-                    .type(String[].class)
-            .build())),
+            .converter(Converters.TEXT_LIST_CONVERTER)
+            .type(String[].class)
+            .build()),
+            (context, selected) ->
+                context.getConfiguration().addApprovedLicenseIds(processArrayArg(context, selected))
+    ),
 
     /**
      * Option that adds approved licenses from a file
@@ -161,7 +181,9 @@ public enum Arg {
             .desc("Name of file containing comma separated lists of approved License IDs.")
             .converter(Converters.FILE_CONVERTER)
             .type(File.class)
-            .build())),
+            .build()),
+            (context, selected) ->
+                    context.getConfiguration().addApprovedLicenseIds(processArrayFile(context, selected))),
 
     /**
      * Option that specifies approved license families
@@ -170,7 +192,8 @@ public enum Arg {
             .desc("A comma separated list of approved license family IDs. These license families will be added to the list of approved license families.")
             .converter(Converters.TEXT_LIST_CONVERTER)
             .type(String[].class)
-            .build())),
+            .build()),
+            (context, selected) -> context.getConfiguration().addApprovedLicenseCategories(processArrayArg(context, selected))),
 
     /**
      * Option that specifies approved license families from a file
@@ -179,7 +202,9 @@ public enum Arg {
             .desc("Name of file containing comma separated lists of approved family IDs.")
             .converter(Converters.FILE_CONVERTER)
             .type(File.class)
-            .build())),
+            .build()),
+            (context, selected) -> context.getConfiguration().addApprovedLicenseCategories(processArrayFile(context, selected))
+    ),
 
     /**
      * Option to remove licenses from the approved list
@@ -190,7 +215,8 @@ public enum Arg {
                     "Once licenses are removed they can not be added back.")
             .converter(Converters.TEXT_LIST_CONVERTER)
             .type(String[].class)
-            .build())),
+            .build()),
+            (context, selected) -> context.getConfiguration().removeApprovedLicenseIds(processArrayArg(context, selected))),
 
     /**
      * Option to read a file licenses to be removed from the approved list.
@@ -201,7 +227,8 @@ public enum Arg {
             .desc("Name of file containing comma separated lists of the denied license IDs. " +
                     "These licenses will be removed from the list of approved licenses. " +
                     "Once licenses are removed they can not be added back.")
-            .build())),
+            .build()),
+            (context, selected) -> context.getConfiguration().removeApprovedLicenseIds(processArrayFile(context, selected))),
 
     /**
      * Option to list license families to remove from the approved list.
@@ -213,7 +240,8 @@ public enum Arg {
                     "Once license families are removed they can not be added back.")
             .converter(Converters.TEXT_LIST_CONVERTER)
             .type(String[].class)
-            .build())),
+            .build()),
+            (context, selected) -> context.getConfiguration().removeApprovedLicenseCategories(processArrayArg(context, selected))),
 
     /**
      * Option to read a list of license families to remove from the approved list.
@@ -224,7 +252,8 @@ public enum Arg {
                     "Once license families are removed they can not be added back.")
             .type(File.class)
             .converter(Converters.FILE_CONVERTER)
-            .build())),
+            .build()),
+            (context, selected) -> context.getConfiguration().removeApprovedLicenseCategories(processArrayFile(context, selected))),
 
     /**
      * Option to specify an acceptable number of various counters.
@@ -233,7 +262,14 @@ public enum Arg {
             .desc("The acceptable maximum number for the specified counter. A value of '-1' specifies an unlimited number.")
             .converter(Converters.COUNTER_CONVERTER)
             .type(Pair.class)
-            .build())),
+            .build()),
+            (context, selected) -> {
+                for (String arg : context.getCommandLine().getOptionValues(selected)) {
+                    Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg);
+                    int limit = pair.getValue();
+                    context.getConfiguration().getClaimValidator().setMax(pair.getKey(), limit < 0 ? Integer.MAX_VALUE : limit);
+                }
+            }),
 
     /**
      * Option to specify an acceptable number of various counters.
@@ -242,7 +278,13 @@ public enum Arg {
             .desc("The minimum number for the specified counter.")
             .converter(Converters.COUNTER_CONVERTER)
             .type(Pair.class)
-            .build())),
+            .build()),
+            (context, selected) -> {
+                for (String arg : context.getCommandLine().getOptionValues(selected)) {
+                    Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg);
+                    context.getConfiguration().getClaimValidator().setMin(pair.getKey(), pair.getValue());
+                }
+            }),
 
 ////////////////// INPUT OPTIONS
     /**
@@ -256,7 +298,13 @@ public enum Arg {
                             "argument is located.")
                     .converter(Converters.FILE_CONVERTER)
                     .type(File.class)
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                File[] files = getParsedOptionValues(selected, context.getCommandLine());
+                for (File f : files) {
+                    context.getConfiguration().addSource(f);
+                }
+            }),
 
     /**
      * Excludes files by expression
@@ -269,7 +317,13 @@ public enum Arg {
                     .build())
             .addOption(Option.builder().longOpt("input-exclude").hasArgs().argName("Expression")
                     .desc("Excludes files matching <Expression>.")
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                String[] excludes = context.getCommandLine().getOptionValues(selected);
+                if (excludes != null) {
+                    context.getConfiguration().addExcludedPatterns(Arrays.asList(excludes));
+                }
+            }),
 
     /**
      * Excludes files based on the contents of a file.
@@ -286,7 +340,17 @@ public enum Arg {
                     .argName("File").hasArg().type(File.class)
                     .converter(Converters.FILE_CONVERTER)
                     .desc("Reads <Expression> entries from a file. Entries will be excluded from processing.")
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                try {
+                    File excludeFileName = context.getCommandLine().getParsedOptionValue(selected);
+                    if (excludeFileName != null) {
+                        context.getConfiguration().addExcludedPatterns(ExclusionUtils.asIterable(excludeFileName, "#"));
+                    }
+                } catch (Exception e) {
+                    throw ConfigurationException.from(e);
+                }
+            }),
     /**
      * Excludes files based on standard groupings.
      */
@@ -296,8 +360,12 @@ public enum Arg {
                     .desc("Excludes files defined in standard collections based on commonly occurring groups. " +
                             "Excludes any path matcher actions but DOES NOT exclude any file processor actions.")
                     .type(StandardCollection.class)
-                    .build())
-    ),
+                    .build()),
+            (context, selected) -> {
+                for (String s : context.getCommandLine().getOptionValues(selected)) {
+                    context.getConfiguration().addExcludedCollection(StandardCollection.valueOf(s));
+                }
+            }),
 
     /**
      * Excludes files if they are smaller than the given threshold.
@@ -306,8 +374,20 @@ public enum Arg {
             .addOption(Option.builder().longOpt("input-exclude-size").argName("Integer")
                     .hasArg().type(Integer.class)
                     .desc("Excludes files with sizes less than the number of bytes specified.")
-                    .build())
-    ),
+                    .build()),
+            (context, selected) -> {
+                try {
+                    final int maxSize = context.getCommandLine().getParsedOptionValue(selected);
+                    DocumentNameMatcher matcher = new DocumentNameMatcher(String.format("File size < %s bytes", maxSize),
+                            (Predicate<DocumentName>) documentName -> {
+                                File f = new File(documentName.getName());
+                                return f.isFile() && f.length() < maxSize;
+                            });
+                    context.getConfiguration().addExcludedMatcher(matcher);
+                } catch (Exception e) {
+                    throw ConfigurationException.from(e);
+                }
+            }),
     /**
      * Excludes files by expression.
      */
@@ -319,8 +399,13 @@ public enum Arg {
                     .desc("Includes files matching <Expression>. Will override excluded files.")
                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
                             .setDescription(StdMsgs.useMsg("--input-include")).get())
-                    .build())
-    ),
+                    .build()),
+            (context, selected) -> {
+                String[] includes = context.getCommandLine().getOptionValues(selected);
+                if (includes != null) {
+                    context.getConfiguration().addIncludedPatterns(Arrays.asList(includes));
+                }
+            }),
 
     /**
      * Includes files based on the contents of a file.
@@ -337,7 +422,17 @@ public enum Arg {
                     .desc("Reads <Expression> entries from a file. Entries will be excluded from processing.")
                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
                             .setDescription(StdMsgs.useMsg("--input-include-file")).get())
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                try {
+                    File includeFileName = context.getCommandLine().getParsedOptionValue(selected);
+                    if (includeFileName != null) {
+                        context.getConfiguration().addIncludedPatterns(ExclusionUtils.asIterable(includeFileName, "#"));
+                    }
+                } catch (Exception e) {
+                    throw ConfigurationException.from(e);
+                }
+            }),
 
     /**
      * Includes files based on standard groups.
@@ -353,8 +448,17 @@ public enum Arg {
                     .desc("Scans hidden directories.")
                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
                             .setDescription(StdMsgs.useMsg("--input-include-std with 'HIDDEN_DIR' argument")).get()).build()
-            )
-    ),
+            ),
+            (context, selected) -> {
+                // display deprecation log if needed.
+                if (context.getCommandLine().hasOption("scan-hidden-directories")) {
+                    context.getConfiguration().addIncludedCollection(StandardCollection.HIDDEN_DIR);
+                } else {
+                    for (String s : context.getCommandLine().getOptionValues(selected)) {
+                        context.getConfiguration().addIncludedCollection(StandardCollection.valueOf(s));
+                    }
+                }
+            }),
 
     /**
      * Excludes files based on SCM exclusion file processing.
@@ -366,17 +470,32 @@ public enum Arg {
                     .desc("Parse SCM based exclusion files to exclude specified files and directories. " +
                             "This action can apply to any standard collection that implements a file processor.")
                     .type(StandardCollection.class)
-                    .build())
-    ),
+                    .build()),
+            (context, selected) -> {
+                StandardCollection[] collections = getParsedOptionValues(selected, context.getCommandLine());
+                final ReportConfiguration configuration = context.getConfiguration();
+                for (StandardCollection collection : collections) {
+                    if (collection == StandardCollection.ALL) {
+                        Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedFileProcessor);
+                        Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedCollection);
+                    } else {
+                        configuration.addExcludedFileProcessor(collection);
+                        configuration.addExcludedCollection(collection);
+                    }
+                }
+            }),
 
     /**
      * Stop processing an input stream and declare an input file.
      */
     DIR(new OptionGroup().addOption(Option.builder().option("d").longOpt("dir").hasArg()
-                    .type(File.class)
+            .type(File.class)
             .desc("Used to indicate end of list when using options that take multiple arguments.").argName("DirOrArchive")
             .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
-                    .setDescription("Use the standard '--' to signal the end of arguments.").get()).build())),
+                    .setDescription("Use the standard '--' to signal the end of arguments.").get()).build()),
+            (context, selected) -> {
+                throw new ImplementationException(String.format("'%s' should not be executed directly", selected));
+            }),
 
     /////////////// OUTPUT OPTIONS
     /**
@@ -397,7 +516,22 @@ public enum Arg {
                             .setForRemoval(true)
                             .setDescription(StdMsgs.useMsg("--output-style with the 'xml' argument")).get())
                     .desc("forces XML output rather than the textual report.")
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                String key = selected.getKey(); // is not null due to above isSelected()-call
+                if ("x".equals(key)) {
+                    // display deprecated message.
+                    context.getCommandLine().hasOption("x");
+                    context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet("xml"));
+                } else {
+                    String[] style = context.getCommandLine().getOptionValues(selected);
+                    if (style.length != 1) {
+                        DefaultLog.getInstance().error("Please specify a single stylesheet");
+                        throw new ConfigurationException("Please specify a single stylesheet");
+                    }
+                    context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet(style[0]));
+                }
+            }),
 
     /**
      * Specifies the license definitions that should be included in the output.
@@ -411,7 +545,14 @@ public enum Arg {
                     .desc("List the defined licenses.")
                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-licenses")).get())
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                try {
+                    context.getConfiguration().listLicenses(context.getCommandLine().getParsedOptionValue(selected));
+                } catch (ParseException e) {
+                    context.logParseException(e, selected, Defaults.LIST_LICENSES);
+                }
+            }),
 
     /**
      * Specifies the license families that should be included in the output.
@@ -425,7 +566,14 @@ public enum Arg {
                     .desc("List the defined license families.")
                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-families")).get())
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                try {
+                    context.getConfiguration().listFamilies(context.getCommandLine().getParsedOptionValue(selected));
+                } catch (ParseException e) {
+                    context.logParseException(e, selected, Defaults.LIST_FAMILIES);
+                }
+            }),
 
     /**
      * Specifies the log level to log messages at.
@@ -434,14 +582,25 @@ public enum Arg {
             .hasArg().argName("LogLevel")
             .desc("Sets the log level.")
             .converter(s -> Log.Level.valueOf(s.toUpperCase()))
-            .build())),
+            .build()),
+            (context, selected) -> {
+                Log dLog = DefaultLog.getInstance();
+                try {
+                    dLog.setLevel(context.getCommandLine().getParsedOptionValue(selected));
+                } catch (ParseException e) {
+                    logParseException(DefaultLog.getInstance(), e, selected, context.getCommandLine(), dLog.getLevel());
+                }
+            }),
 
     /**
      * Specifies that the run should not perform any updates to files.
      */
     DRY_RUN(new OptionGroup().addOption(Option.builder().longOpt("dry-run")
             .desc("If set do not update the files but generate the reports.")
-            .build())),
+            .build()),
+            (context, selected) ->
+                    context.getConfiguration().setDryRun(true)
+    ),
 
     /**
      * Specifies where the output should be written.
@@ -457,7 +616,20 @@ public enum Arg {
                     .desc("Define the output file where to write a report to.")
                     .type(File.class)
                     .converter(Converters.FILE_CONVERTER)
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                try {
+                    File file = context.getCommandLine().getParsedOptionValue(selected);
+                    File parent = file.getParentFile();
+                    if (!parent.mkdirs() && !parent.isDirectory()) {
+                        DefaultLog.getInstance().error("Could not create report parent directory " + file);
+                    }
+                    context.getConfiguration().setOut(file);
+                } catch (ParseException e) {
+                    context.logParseException(e, selected, "System.out");
+                    context.getConfiguration().setOut(() -> CloseShieldOutputStream.wrap(System.out));
+                }
+            }),
 
     /**
      * Specifies the level of reporting detail for archive files.
@@ -466,7 +638,15 @@ public enum Arg {
             .addOption(Option.builder().longOpt("output-archive").hasArg().argName("ProcessingType")
                     .desc("Specifies the level of detail in ARCHIVE file reporting.")
                     .converter(s -> ReportConfiguration.Processing.valueOf(s.toUpperCase()))
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                try {
+                    context.getConfiguration().setArchiveProcessing(context.getCommandLine().getParsedOptionValue(selected));
+                } catch (ParseException e) {
+                    context.logParseException(e, selected, Defaults.ARCHIVE_PROCESSING);
+                }
+            }
+    ),
 
     /**
      * Specifies the level of reporting detail for standard files.
@@ -475,51 +655,57 @@ public enum Arg {
             .addOption(Option.builder().longOpt("output-standard").hasArg().argName("ProcessingType")
                     .desc("Specifies the level of detail in STANDARD file reporting.")
                     .converter(s -> ReportConfiguration.Processing.valueOf(s.toUpperCase()))
-                    .build())),
+                    .build()),
+            (context, selected) -> {
+                try {
+                    context.getConfiguration().setStandardProcessing(context.getCommandLine().getParsedOptionValue(selected));
+                } catch (ParseException e) {
+                    context.logParseException(e, selected, Defaults.STANDARD_PROCESSING);
+                }
+            }),
 
     /**
      * Provide license definition listing of registered licenses.
      */
     HELP_LICENSES(new OptionGroup()
             .addOption(Option.builder().longOpt("help-licenses") //
-                    .desc("Print information about registered licenses.").build()));
+                    .desc("Print information about registered licenses.").build()),
+            (context, selected) -> {
+                throw new ImplementationException(String.format("'%s' should not be executed directly", selected));
+            });
 
-    /** The option group for the argument */
+    /**
+     * The option group for the argument
+     */
     private final OptionGroup group;
 
+    /**
+     * The apply the option to update the state of the context.configuration.
+     */
+    private final BiConsumer<ArgumentContext, Option> process;
+
     /**
      * Creates an Arg from an Option group.
      *
      * @param group The option group.
      */
-    Arg(final OptionGroup group) {
+    Arg(final OptionGroup group, final BiConsumer<ArgumentContext, Option> process) {
         this.group = group;
+        this.process = process;
     }
 
-    /**
-     * Determines if the group has a selected element.
-     *
-     * @return {@code true} if the group has a selected element.
-     */
-    public boolean isSelected() {
-        return group.getSelected() != null;
+    private void execute(final ArgumentContext context, final UIOptionCollection<?> optionCollection) {
+        optionCollection.getSelected(this)
+                .ifPresent(selected -> this.process.accept(context, selected));
     }
 
     /**
-     * Gets the select element from the group.
+     * Determines if all the options have been removed from this argument.
      *
-     * @return the selected element or {@code null} if no element is selected.
-     */
-    public Option getSelected() {
-        String s = group.getSelected();
-        if (s != null) {
-            for (Option result : group.getOptions()) {
-                if (result.getKey().equals(s)) {
-                    return result;
-                }
-            }
-        }
-        return null;
+     * @return {@code true} if all the options have been removed from this argument.
+     */
+    public boolean isEmpty() {
+        return this.group().getOptions().isEmpty();
     }
 
     /**
@@ -538,14 +724,6 @@ public enum Arg {
         throw new IllegalArgumentException("Can not find " + key);
     }
 
-    /**
-     * Gets the default value for this arg.
-     * @return default value of this arg.
-     */
-    public String defaultValue() {
-        return DEFAULT_VALUES.get(this);
-    }
-
     /**
      * Gets the group for this arg.
      *
@@ -558,20 +736,25 @@ public enum Arg {
     /**
      * Returns the first non-deprecated option from the group.
      *
-     * @return the first non-deprecated option or {@code null} if no non-deprecated option is available.
+     * @return the first non-deprecated option or, if no non-deprecated option is available, the first option.
      */
     public Option option() {
+        Option first = null;
         for (Option result : group.getOptions()) {
+            if (first == null) {
+                first = result;
+            }
             if (!result.isDeprecated()) {
                 return result;
             }
         }
-        return null;
+        return first;
     }
 
     /**
      * Gets the full set of options.
-     * @return  the full set of options for this Arg.
+     *
+     * @return the full set of options for this Arg.
      */
     public static Options getOptions() {
         Options options = new Options();
@@ -586,36 +769,44 @@ public enum Arg {
      *
      * @param context the context to work with.
      */
-    private static void processEditArgs(final ArgumentContext context) {
-        if (EDIT_ADD.isSelected()) {
-            context.getCommandLine().hasOption(Arg.EDIT_ADD.getSelected());
-            boolean force = EDIT_OVERWRITE.isSelected();
+    private static void processEditArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) {
+        optionCollection.getSelected(EDIT_ADD).ifPresent(option -> {
+            // prints deprecation
+            context.getCommandLine().hasOption(option);
+            boolean force = optionCollection.isSelected(EDIT_OVERWRITE);
             if (force) {
-                context.getCommandLine().hasOption(EDIT_OVERWRITE.getSelected());
+                // prints deprecation
+                optionCollection.getSelected(EDIT_OVERWRITE).ifPresent(context.getCommandLine()::hasOption);
             }
             context.getConfiguration().setAddLicenseHeaders(force ? AddLicenseHeaders.FORCED : AddLicenseHeaders.TRUE);
-            if (EDIT_COPYRIGHT.isSelected()) {
-                context.getConfiguration().setCopyrightMessage(context.getCommandLine().getOptionValue(EDIT_COPYRIGHT.getSelected()));
-            }
-        }
+            optionCollection.getSelected(EDIT_COPYRIGHT).
+                    ifPresent(editOption -> context.getConfiguration().setCopyrightMessage(context.getCommandLine().getOptionValue(editOption)));
+        });
     }
 
-    private static List<String> processArrayArg(final ArgumentContext context, final Arg arg) throws ParseException {
-        String[] ids = context.getCommandLine().getParsedOptionValue(arg.getSelected());
-        return Arrays.asList(ids);
+    private static List<String> processArrayArg(final ArgumentContext context, final Option selected) {
+        try {
+            return Arrays.asList(context.getCommandLine().getParsedOptionValue(selected));
+        } catch (ParseException e) {
+            throw new ConfigurationException(e);
+        }
     }
 
-    private static List<String> processArrayFile(final ArgumentContext context, final Arg arg) throws ParseException {
+    private static List<String> processArrayFile(final ArgumentContext context, final Option selected) {
         List<String> result = new ArrayList<>();
-        File file = context.getCommandLine().getParsedOptionValue(arg.getSelected());
-        try (InputStream in = Files.newInputStream(file.toPath())) {
-            for (String line : IOUtils.readLines(in, StandardCharsets.UTF_8)) {
-                String[] ids = Converters.TEXT_LIST_CONVERTER.apply(line);
-                result.addAll(Arrays.asList(ids));
+        try {
+            File file = context.getCommandLine().getParsedOptionValue(selected);
+            try (InputStream in = Files.newInputStream(file.toPath())) {
+                for (String line : IOUtils.readLines(in, StandardCharsets.UTF_8)) {
+                    String[] ids = Converters.TEXT_LIST_CONVERTER.apply(line);
+                    result.addAll(Arrays.asList(ids));
+                }
+                return result;
+            } catch (IOException e) {
+                throw new ConfigurationException(e);
             }
-            return result;
-        } catch (IOException e) {
-            throw new ConfigurationException(e);
+        } catch (ParseException e) {
+            throw ConfigurationException.from(e);
         }
     }
 
@@ -625,64 +816,28 @@ public enum Arg {
      * @param context the context to process.
      * @throws ConfigurationException if configuration files can not be read.
      */
-    private static void processConfigurationArgs(final ArgumentContext context) throws ConfigurationException {
-        try {
-            Defaults.Builder defaultBuilder = Defaults.builder();
-            if (CONFIGURATION.isSelected()) {
-                File[] files = CONFIGURATION.getParsedOptionValues(context.getCommandLine());
-                for (File file : files) {
-                    defaultBuilder.add(file);
-                }
-            }
-            if (CONFIGURATION_NO_DEFAULTS.isSelected()) {
-                // display deprecation log if needed.
-                context.getCommandLine().hasOption(CONFIGURATION_NO_DEFAULTS.getSelected());
-                defaultBuilder.noDefault();
-            }
-            context.getConfiguration().setFrom(defaultBuilder.build());
-
-            if (FAMILIES_APPROVED.isSelected()) {
-                context.getConfiguration().addApprovedLicenseCategories(processArrayArg(context, FAMILIES_APPROVED));
-            }
-            if (FAMILIES_APPROVED_FILE.isSelected()) {
-                context.getConfiguration().addApprovedLicenseCategories(processArrayFile(context, FAMILIES_APPROVED_FILE));
-            }
-            if (FAMILIES_DENIED.isSelected()) {
-                context.getConfiguration().removeApprovedLicenseCategories(processArrayArg(context, FAMILIES_DENIED));
-            }
-            if (FAMILIES_DENIED_FILE.isSelected()) {
-                context.getConfiguration().removeApprovedLicenseCategories(processArrayFile(context, FAMILIES_DENIED_FILE));
-            }
-
-            if (LICENSES_APPROVED.isSelected()) {
-                context.getConfiguration().addApprovedLicenseIds(processArrayArg(context, LICENSES_APPROVED));
-            }
+    private static void processConfigurationArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException {
 
-            if (LICENSES_APPROVED_FILE.isSelected()) {
-                context.getConfiguration().addApprovedLicenseIds(processArrayFile(context, LICENSES_APPROVED_FILE));
-            }
-            if (LICENSES_DENIED.isSelected()) {
-                context.getConfiguration().removeApprovedLicenseIds(processArrayArg(context, LICENSES_DENIED));
-            }
+        Defaults.Builder defaultBuilder = Defaults.builder();
 
-            if (LICENSES_DENIED_FILE.isSelected()) {
-                context.getConfiguration().removeApprovedLicenseIds(processArrayFile(context, LICENSES_DENIED_FILE));
-            }
-            if (COUNTER_MAX.isSelected()) {
-                for (String arg : context.getCommandLine().getOptionValues(COUNTER_MAX.getSelected())) {
-                    Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg);
-                    int limit = pair.getValue();
-                    context.getConfiguration().getClaimValidator().setMax(pair.getKey(), limit < 0 ? Integer.MAX_VALUE : limit);
-                }
-            }
-            if (COUNTER_MIN.isSelected()) {
-                for (String arg : context.getCommandLine().getOptionValues(COUNTER_MIN.getSelected())) {
-                    Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg);
-                    context.getConfiguration().getClaimValidator().setMin(pair.getKey(), pair.getValue());
-                }
-            }
-        } catch (Exception e) {
-            throw ConfigurationException.from(e);
+        optionCollection.getSelected(CONFIGURATION).ifPresent(
+                selected -> {
+                    File[] files = getParsedOptionValues(selected, context.getCommandLine());
+                    for (File file : files) {
+                        defaultBuilder.add(file);
+                    }
+                });
+        optionCollection.getSelected(CONFIGURATION_NO_DEFAULTS).ifPresent(selected -> {
+            // display deprecation log if needed.
+            context.getCommandLine().hasOption(selected);
+            defaultBuilder.noDefault();
+        });
+        context.getConfiguration().setFrom(defaultBuilder.build());
+
+        for (Arg arg : new Arg[]{FAMILIES_APPROVED, FAMILIES_APPROVED_FILE, FAMILIES_DENIED, FAMILIES_DENIED_FILE,
+                LICENSES_APPROVED, LICENSES_APPROVED_FILE, LICENSES_DENIED, LICENSES_DENIED_FILE,
+                COUNTER_MAX, COUNTER_MIN}) {
+            arg.execute(context, optionCollection);
         }
     }
 
@@ -692,91 +847,21 @@ public enum Arg {
      * @param context the context to work in.
      * @throws ConfigurationException if an exclude file can not be read.
      */
-    private static void processInputArgs(final ArgumentContext context) throws ConfigurationException {
-        try {
-            if (SOURCE.isSelected()) {
-                File[] files = SOURCE.getParsedOptionValues(context.getCommandLine());
-                for (File f : files) {
-                    context.getConfiguration().addSource(f);
-                }
-            }
-            // TODO when include/exclude processing is updated check calling methods to ensure that all specified
-            // directories are handled in the list of directories.
-            if (EXCLUDE.isSelected()) {
-                String[] excludes = context.getCommandLine().getOptionValues(EXCLUDE.getSelected());
-                if (excludes != null) {
-                    context.getConfiguration().addExcludedPatterns(Arrays.asList(excludes));
-                }
-            }
-            if (EXCLUDE_FILE.isSelected()) {
-                File excludeFileName = context.getCommandLine().getParsedOptionValue(EXCLUDE_FILE.getSelected());
-                if (excludeFileName != null) {
-                    context.getConfiguration().addExcludedPatterns(ExclusionUtils.asIterable(excludeFileName, "#"));
-                }
-            }
-            if (EXCLUDE_STD.isSelected()) {
-                for (String s : context.getCommandLine().getOptionValues(EXCLUDE_STD.getSelected())) {
-                    context.getConfiguration().addExcludedCollection(StandardCollection.valueOf(s));
-                }
-            }
-            if (EXCLUDE_PARSE_SCM.isSelected()) {
-                StandardCollection[] collections = EXCLUDE_PARSE_SCM.getParsedOptionValues(context.getCommandLine());
-                final ReportConfiguration configuration = context.getConfiguration();
-                for (StandardCollection collection : collections) {
-                    if (collection == StandardCollection.ALL) {
-                        Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedFileProcessor);
-                        Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedCollection);
-                    } else {
-                        configuration.addExcludedFileProcessor(collection);
-                        configuration.addExcludedCollection(collection);
-                    }
-                }
-            }
-            if (EXCLUDE_SIZE.isSelected()) {
-                final int maxSize = EXCLUDE_SIZE.getParsedOptionValue(context.getCommandLine());
-                DocumentNameMatcher matcher = new DocumentNameMatcher(String.format("File size < %s bytes", maxSize),
-                        (Predicate<DocumentName>) documentName -> {
-                        File f = new File(documentName.getName());
-                        return f.isFile() && f.length() < maxSize;
-                });
-                context.getConfiguration().addExcludedMatcher(matcher);
-            }
-            if (INCLUDE.isSelected()) {
-                String[] includes = context.getCommandLine().getOptionValues(INCLUDE.getSelected());
-                if (includes != null) {
-                    context.getConfiguration().addIncludedPatterns(Arrays.asList(includes));
-                }
-            }
-            if (INCLUDE_FILE.isSelected()) {
-                File includeFileName = context.getCommandLine().getParsedOptionValue(INCLUDE_FILE.getSelected());
-                if (includeFileName != null) {
-                    context.getConfiguration().addIncludedPatterns(ExclusionUtils.asIterable(includeFileName, "#"));
-                }
-            }
-            if (INCLUDE_STD.isSelected()) {
-                Option selected = INCLUDE_STD.getSelected();
-                // display deprecation log if needed.
-                if (context.getCommandLine().hasOption("scan-hidden-directories")) {
-                    context.getConfiguration().addIncludedCollection(StandardCollection.HIDDEN_DIR);
-                } else {
-                    for (String s : context.getCommandLine().getOptionValues(selected)) {
-                        context.getConfiguration().addIncludedCollection(StandardCollection.valueOf(s));
-                    }
-                }
-            }
-        } catch (Exception e) {
-            throw ConfigurationException.from(e);
+    private static void processInputArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException {
+        for (Arg arg : new Arg[]{SOURCE, EXCLUDE, EXCLUDE_FILE, EXCLUDE_STD, EXCLUDE_PARSE_SCM, EXCLUDE_SIZE,
+                INCLUDE, INCLUDE_FILE, INCLUDE_STD}) {
+            arg.execute(context, optionCollection);
         }
     }
 
     /**
      * Logs a ParseException as a warning.
      *
-     * @param log       the Log to write to
+     * @param log the Log to write to
      * @param exception the parse exception to log
-     * @param opt       the option being processed
-     * @param cl        the command line being processed
-     * @param defaultValue      The default value the option is being set to.
+     * @param opt the option being processed
+     * @param cl the command line being processed
+     * @param defaultValue The default value the option is being set to.
      */
     private static void logParseException(final Log log, final ParseException exception, final Option opt, final CommandLine cl, final Object defaultValue) {
         log.warn(format("Invalid %s specified: %s ", opt.getOpt(), cl.getOptionValue(opt)));
@@ -787,17 +872,10 @@ public enum Arg {
     /**
      * Process the log level setting.
      *
-     * @param commandLine The command line to process.
+     * @param context The argument context
      */
-    public static void processLogLevel(final CommandLine commandLine) {
-        if (LOG_LEVEL.getSelected() != null) {
-            Log dLog = DefaultLog.getInstance();
-            try {
-                dLog.setLevel(commandLine.getParsedOptionValue(LOG_LEVEL.getSelected()));
-            } catch (ParseException e) {
-                logParseException(DefaultLog.getInstance(), e, LOG_LEVEL.getSelected(), commandLine, dLog.getLevel());
-            }
-        }
+    public static void processLogLevel(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException {
+        LOG_LEVEL.execute(context, optionCollection);
     }
 
     /**
@@ -806,12 +884,12 @@ public enum Arg {
      * @param context the context in which to process the args.
      * @throws ConfigurationException on error
      */
-    public static void processArgs(final ArgumentContext context) throws ConfigurationException {
+    public static void processArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException {
         Converters.FILE_CONVERTER.setWorkingDirectory(context.getWorkingDirectory());
-        processOutputArgs(context);
-        processEditArgs(context);
-        processInputArgs(context);
-        processConfigurationArgs(context);
+        processOutputArgs(context, optionCollection);
+        processEditArgs(context, optionCollection);
+        processInputArgs(context, optionCollection);
+        processConfigurationArgs(context, optionCollection);
     }
 
     /**
@@ -819,69 +897,9 @@ public enum Arg {
      *
      * @param context the context in which to process the args.
      */
-    private static void processOutputArgs(final ArgumentContext context) {
-        context.getConfiguration().setDryRun(DRY_RUN.isSelected());
-
-        if (OUTPUT_FAMILIES.isSelected()) {
-            try {
-                context.getConfiguration().listFamilies(context.getCommandLine().getParsedOptionValue(OUTPUT_FAMILIES.getSelected()));
-            } catch (ParseException e) {
-                context.logParseException(e, OUTPUT_FAMILIES.getSelected(), Defaults.LIST_FAMILIES);
-            }
-        }
-
-        if (OUTPUT_LICENSES.isSelected()) {
-            try {
-                context.getConfiguration().listLicenses(context.getCommandLine().getParsedOptionValue(OUTPUT_LICENSES.getSelected()));
-            } catch (ParseException e) {
-                context.logParseException(e, OUTPUT_LICENSES.getSelected(), Defaults.LIST_LICENSES);
-            }
-        }
-
-        if (OUTPUT_ARCHIVE.isSelected()) {
-            try {
-                context.getConfiguration().setArchiveProcessing(context.getCommandLine().getParsedOptionValue(OUTPUT_ARCHIVE.getSelected()));
-            } catch (ParseException e) {
-                context.logParseException(e, OUTPUT_ARCHIVE.getSelected(), Defaults.ARCHIVE_PROCESSING);
-            }
-        }
-
-        if (OUTPUT_STANDARD.isSelected()) {
-            try {
-                context.getConfiguration().setStandardProcessing(context.getCommandLine().getParsedOptionValue(OUTPUT_STANDARD.getSelected()));
-            } catch (ParseException e) {
-                context.logParseException(e, OUTPUT_STANDARD.getSelected(), Defaults.STANDARD_PROCESSING);
-            }
-        }
-
-        if (OUTPUT_FILE.isSelected()) {
-            try {
-                File file = context.getCommandLine().getParsedOptionValue(OUTPUT_FILE.getSelected());
-                File parent = file.getParentFile();
-                if (!parent.mkdirs() && !parent.isDirectory()) {
-                    DefaultLog.getInstance().error("Could not create report parent directory " + file);
-                }
-                context.getConfiguration().setOut(file);
-            } catch (ParseException e) {
-                context.logParseException(e, OUTPUT_FILE.getSelected(), "System.out");
-                context.getConfiguration().setOut((IOSupplier<OutputStream>) null);
-            }
-        }
-
-        if (OUTPUT_STYLE.isSelected()) {
-            String selected = OUTPUT_STYLE.getSelected().getKey(); // is not null due to above isSelected()-call
-            if ("x".equals(selected)) {
-                // display deprecated message.
-                context.getCommandLine().hasOption("x");
-                context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet("xml"));
-            } else {
-                String[] style = context.getCommandLine().getOptionValues(OUTPUT_STYLE.getSelected());
-                if (style.length != 1) {
-                    DefaultLog.getInstance().error("Please specify a single stylesheet");
-                    throw new ConfigurationException("Please specify a single stylesheet");
-                }
-                context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet(style[0]));
-            }
+    private static void processOutputArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException {
+        for (Arg arg : new Arg[]{DRY_RUN, OUTPUT_FAMILIES, OUTPUT_LICENSES, OUTPUT_ARCHIVE, OUTPUT_STANDARD, OUTPUT_FILE, OUTPUT_STYLE}) {
+            arg.execute(context, optionCollection);
         }
     }
 
@@ -898,27 +916,9 @@ public enum Arg {
         }
     }
 
-    /**
-     * Finds the Arg that an Option is in.
-     *
-     * @param optionToFind the Option to locate.
-     * @return The Arg or {@code null} if no Arg is found.
-     */
-    public static Arg findArg(final Option optionToFind) {
-        if (optionToFind != null) {
-            for (Arg arg : Arg.values()) {
-                for (Option candidate : arg.group.getOptions()) {
-                    if (optionToFind.equals(candidate)) {
-                        return arg;
-                    }
-                }
-            }
-        }
-        return null;
-    }
-
     /**
      * Finds the Arg that contains an Option with the specified key.
+     *
      * @param key the key for the Option to locate.
      * @return The Arg or {@code null} if no Arg is found.
      */
@@ -935,31 +935,18 @@ public enum Arg {
         return null;
     }
 
-    private <T> T getParsedOptionValue(final CommandLine commandLine) throws ParseException {
-        return commandLine.getParsedOptionValue(this.getSelected());
-    }
-
-    private String getOptionValue(final CommandLine commandLine) {
-        return commandLine.getOptionValue(this.getSelected());
-    }
-
-    private String[] getOptionValues(final CommandLine commandLine) {
-        return commandLine.getOptionValues(this.getSelected());
-    }
-
-    private <T> T[] getParsedOptionValues(final CommandLine commandLine)  {
-        Option option = getSelected();
+    private static <T> T[] getParsedOptionValues(final Option selected, final CommandLine commandLine) {
         try {
-            Class<? extends T> clazz = (Class<? extends T>) option.getType();
-            String[] values = commandLine.getOptionValues(option);
+            Class<? extends T> clazz = (Class<? extends T>) selected.getType();
+            String[] values = commandLine.getOptionValues(selected);
             T[] result = (T[]) Array.newInstance(clazz, values.length);
             for (int i = 0; i < values.length; i++) {
-                result[i] = clazz.cast(option.getConverter().apply(values[i]));
+                result[i] = clazz.cast(selected.getConverter().apply(values[i]));
             }
             return result;
         } catch (Throwable t) {
-            throw new ConfigurationException(format("'%s' converter for %s '%s' does not produce a class of type %s", this,
-                    option.getKey(), option.getConverter().getClass().getName(), option.getType()), t);
+            throw new ConfigurationException(format("'%s' converter for %s '%s' does not produce a class of type %s", selected,
+                    selected.getKey(), selected.getConverter().getClass().getName(), selected.getType()), t);
         }
     }
 
@@ -981,18 +968,4 @@ public enum Arg {
             return format("Use %s instead.", name);
         }
     }
-
-    /**
-     * The default values description map
-     */
-    private static final Map<Arg, String> DEFAULT_VALUES = new HashMap<>();
-
-    static {
-        DEFAULT_VALUES.put(OUTPUT_FILE, "System.out");
-        DEFAULT_VALUES.put(LOG_LEVEL, Log.Level.WARN.name());
-        DEFAULT_VALUES.put(OUTPUT_ARCHIVE, Defaults.ARCHIVE_PROCESSING.name());
-        DEFAULT_VALUES.put(OUTPUT_STANDARD, Defaults.STANDARD_PROCESSING.name());
-        DEFAULT_VALUES.put(OUTPUT_LICENSES, Defaults.LIST_LICENSES.name());
-        DEFAULT_VALUES.put(OUTPUT_FAMILIES, Defaults.LIST_FAMILIES.name());
-    }
 }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java b/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java
index ea672bc8..d4374a57 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/commandline/ArgumentContext.java
@@ -25,6 +25,7 @@ import org.apache.commons.cli.Option;
 import org.apache.commons.cli.ParseException;
 import org.apache.rat.ReportConfiguration;
 import org.apache.rat.document.DocumentName;
+import org.apache.rat.ui.UIOptionCollection;
 import org.apache.rat.utils.DefaultLog;
 
 import static java.lang.String.format;
@@ -33,7 +34,7 @@ import static java.lang.String.format;
  * Provides the context necessary to process various arguments.
  * @since 0.17
  */
-public class ArgumentContext {
+public final class ArgumentContext {
     /** The report configuration that is being built */
     private final ReportConfiguration configuration;
     /** The command line that is building the configuration */
@@ -65,8 +66,8 @@ public class ArgumentContext {
     /**
      * Process the arguments specified in this context.
      */
-    public void processArgs() {
-        Arg.processArgs(this);
+    public void processArgs(final UIOptionCollection<?> uiOptionCollection) {
+        Arg.processArgs(this,  uiOptionCollection);
     }
 
     /**
diff --git a/apache-rat-core/src/main/java/org/apache/rat/help/AbstractHelp.java b/apache-rat-core/src/main/java/org/apache/rat/help/AbstractHelp.java
index eff2b49a..e7a8e336 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/help/AbstractHelp.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/help/AbstractHelp.java
@@ -30,9 +30,9 @@ import org.apache.commons.cli.Option;
 import org.apache.commons.cli.Options;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.text.WordUtils;
+import org.apache.rat.CLIOptionCollection;
 import org.apache.rat.OptionCollection;
 import org.apache.rat.VersionInfo;
-import org.apache.rat.commandline.Arg;
 
 import static java.lang.String.format;
 
@@ -185,8 +185,7 @@ public abstract class AbstractHelp {
                     optBuf.append(END_OF_OPTION_MSG);
                 }
                 // check for default value
-                Arg arg = Arg.findArg(option);
-                String defaultValue = arg == null ? null : arg.defaultValue();
+                String defaultValue = CLIOptionCollection.INSTANCE.defaultValue(option);
                 if (defaultValue != null) {
                     optBuf.append(format(" (Default value = %s)", defaultValue));
                 }
diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/AbstractCodeGenerator.java b/apache-rat-core/src/main/java/org/apache/rat/ui/AbstractCodeGenerator.java
new file mode 100644
index 00000000..3eb5cdae
--- /dev/null
+++ b/apache-rat-core/src/main/java/org/apache/rat/ui/AbstractCodeGenerator.java
@@ -0,0 +1,151 @@
+/*
+ * 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.rat.ui;
+
+import java.io.IOException;
+import java.util.function.Function;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.text.WordUtils;
+import org.apache.rat.DeprecationReporter;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.runtime.RuntimeConstants;
+
+import static java.lang.String.format;
+import static org.apache.rat.OptionCollectionParser.ArgumentType.NONE;
+
+/**
+ * Generates the ${code org.apache.rat.maven.AbstractMaven} source code.
+ * @param <T> The concrete implementation of the AbstractOption.
+ */
+public abstract class AbstractCodeGenerator<T extends UIOption<?>> {
+    /** The base source directory */
+    protected final String baseDirectory;
+    /** the velocity engine to generate files with */
+    protected final VelocityEngine velocityEngine;
+    /**
+     * private constructor.
+     * @param baseDirectory The base source directory.
+     */
+    protected AbstractCodeGenerator(final String baseDirectory) {
+        this.baseDirectory = baseDirectory;
+        velocityEngine = new VelocityEngine();
+        velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
+        velocityEngine.setProperty("classpath.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
+        velocityEngine.init();
+    }
+
+    /**
+     * Gets the options for the command line.
+     * @return the command line options.
+     */
+    private static Options getOptions() {
+        return new Options()
+                .addOption(Option.builder("h").longOpt("help").desc("Print this message").build())
+                .addOption(Option.builder("p").longOpt("path").required().hasArg().desc("The path to the base of the generated java code directory").build());
+    }
+
+
+    /**
+     * Executable entry point.
+     * @param args the arguments for the executable
+     * @throws IOException on IO error.
+     */
+    protected static void processArgs(final String syntax,
+                                   final Function<String, AbstractCodeGenerator<?>> instance,
+                                   final String[] args)
+            throws IOException {
+        CommandLine commandLine = null;
+        try {
+            commandLine = DefaultParser.builder().setDeprecatedHandler(DeprecationReporter.getLogReporter())
+                    .build().parse(getOptions(), args);
+        } catch (ParseException pe) {
+            HelpFormatter formatter = new HelpFormatter();
+            formatter.printHelp(syntax, pe.getMessage(), getOptions(), "");
+            System.exit(1);
+        }
+
+        if (commandLine.hasOption("h")) {
+            HelpFormatter formatter = new HelpFormatter();
+            formatter.printHelp(syntax, getOptions());
+            System.exit(0);
+        }
+        AbstractCodeGenerator<?> codeGenerator = instance.apply(commandLine.getOptionValue("p"));
+        codeGenerator.execute();
+    }
+
+    /**
+     * Executes the code generation.
+     * @throws IOException on IO error
+     */
+    protected abstract void execute() throws IOException;
+
+    /**
+     * Creates the description for a method.
+     * @param abstractOption the option generating the method.
+     * @return the description for the method in {@code AbstractMaven.java}.
+     */
+    protected final String createDesc(final T abstractOption) {
+        String desc = abstractOption.getDescription();
+        if (desc == null) {
+            throw new IllegalStateException(format("Description for %s may not be null", abstractOption.getName()));
+        }
+        if (!desc.contains(".")) {
+            throw new IllegalStateException(format("First sentence of description for %s must end with a '.'", abstractOption.getName()));
+        }
+        if (abstractOption.getArgType() != NONE) {
+            desc = format("%s Argument%s should be %s%s. (See Argument Types for clarification)", desc, abstractOption.hasArgs() ? "s" : "",
+                    abstractOption.hasArgs() ? "" : "a ", abstractOption.getArgName());
+        }
+        return desc;
+    }
+
+    /**
+     * Gets the argument description for the method returned from ${link createMethodName}.
+     * @param abstractOption the maven option generating the method.
+     * @param desc the description of the argument.
+     * @return the argument description for the method in {@code AbstractMaven.java}.
+     */
+    protected String createArgDesc(final T abstractOption, final String desc) {
+        if (abstractOption.hasArg()) {
+            String argDesc = desc.substring(desc.indexOf(" ") + 1, desc.indexOf(".") + 1);
+            return WordUtils.capitalize(argDesc.substring(0, 1)) + argDesc.substring(1);
+        } else {
+            return "The state";
+        }
+    }
+
+    /**
+     * Gets method name for the option.
+     * @param abstractOption the maven option generating the method.
+     * @return the method name description for the method in {@code AbstractMaven.java}.
+     */
+    protected abstract String createMethodName(T abstractOption);
+
+    /**
+     * Gathers all method definitions into a single string.
+     * @return the definition of all the methods.
+     */
+    protected abstract String gatherMethods();
+}
diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/ArgumentTracker.java b/apache-rat-core/src/main/java/org/apache/rat/ui/ArgumentTracker.java
new file mode 100644
index 00000000..d1203b2b
--- /dev/null
+++ b/apache-rat-core/src/main/java/org/apache/rat/ui/ArgumentTracker.java
@@ -0,0 +1,239 @@
+/*
+ * 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
+ *
+ *   https://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.rat.ui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+import org.apache.commons.cli.Option;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.rat.DeprecationReporter;
+import org.apache.rat.commandline.Arg;
+import org.apache.rat.utils.DefaultLog;
+import org.apache.rat.utils.Log;
+
+/**
+ * Tracks Arg values that are set and their values for conversion from native UI to
+ * Apache Commons command line values.
+ */
+public final class ArgumentTracker {
+
+    /**
+     * List of deprecated arguments and their deprecation notice.
+     */
+    private final Map<String, String> deprecatedArgs = new HashMap<>();
+
+    /**
+     * A map of CLI-based arguments to values.
+     */
+    private final Map<String, List<String>> args = new HashMap<>();
+
+    /**
+     * The arguments understood by the UI for the current report execution.
+     * @param optionCollection The AbstractOptionCollection for this UI.
+     */
+    public ArgumentTracker(final UIOptionCollection<?> optionCollection) {
+        for (UIOption<?> abstractOption : optionCollection.getMappedOptions().toList()) {
+            if (abstractOption.isDeprecated()) {
+                deprecatedArgs.put(abstractOption.getName(),
+                        String.format("Use of deprecated option '%s'. %s", abstractOption.getName(), abstractOption.getDeprecated()));
+            }
+        }
+    }
+
+    /**
+     * Extract the core name from the option.  This is the {@link Option#getLongOpt()} if defined, otherwise
+     * the {@link Option#getOpt()}.
+     * @param option the commons cli option.
+     * @return the common cli based name.
+     */
+    public static String extractKey(final Option option) {
+        return StringUtils.defaultIfBlank(option.getLongOpt(), option.getOpt());
+    }
+
+    /**
+     * Sets the deprecation report method in the Apache Commons CLI processes.
+     */
+    private void setDeprecationReporter() {
+        DeprecationReporter.setLogReporter(opt -> {
+            String msg = deprecatedArgs.get(extractKey(opt));
+            if (msg == null) {
+                DeprecationReporter.getDefault().accept(opt);
+            } else {
+                DefaultLog.getInstance().warn(msg);
+            }
+        });
+    }
+
+    /**
+     * Gets the list of arguments prepared for the CLI code to parse.
+     * @return the List of arguments for the CLI command line.
+     */
+    public List<String> args() {
+        final List<String> result = new ArrayList<>();
+        for (Map.Entry<String, List<String>> entry : args.entrySet()) {
+            result.add("--" + entry.getKey());
+            result.addAll(entry.getValue().stream().filter(Objects::nonNull).collect(Collectors.toList()));
+        }
+        return result;
+    }
+
+    /**
+     * Applies the consumer to each arg and list in turn.
+     */
+    public void apply(final BiConsumer<String, List<String>> consumer) {
+        args.forEach((key, value) -> consumer.accept(key, new ArrayList<>(value)));
+    }
+
+    /**
+     * Validate that the option is defined in Args and has not already been set.
+     * This check will verify tha only one of the keys in the group can be set.
+     * @param key the key to check
+     * @return true if the key may be set.
+     */
+    private boolean validateSet(final String key) {
+        final Arg arg = Arg.findArg(key);
+        if (arg != null) {
+            final Option opt = arg.find(key);
+            final Option main = arg.option();
+            if (opt.isDeprecated()) {
+                args.remove(extractKey(main));
+                // deprecated options must be explicitly set so let it go.
+                return true;
+            }
+            // non-deprecated options may have default so ignore it if another option has already been set.
+            for (Option o : arg.group().getOptions()) {
+                if (!o.equals(main)) {
+                    if (args.containsKey(extractKey(o))) {
+                        return false;
+                    }
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Set a key and value into the argument list.
+     * Replaces any existing value.
+     * @param key the key for the map.
+     * @param value the value to set.
+     */
+    public void setArg(final UIOption<?> key, final String value) {
+        setArg(key.keyValue(), value);
+    }
+
+    /**
+     * Set a key and value into the argument list.
+     * Replaces any existing value.
+     * @param trackerKey the key for the map.
+     * @param value the value to set.
+     */
+    public void setArg(final String trackerKey, final String value) {
+        if (value == null || StringUtils.isNotBlank(value)) {
+            if (validateSet(trackerKey)) {
+                Option ratOption = Arg.findArg(trackerKey).find(trackerKey);
+                if (ratOption.hasArg()) {
+                    List<String> values = new ArrayList<>();
+                    if (DefaultLog.getInstance().isEnabled(Log.Level.DEBUG)) {
+                        DefaultLog.getInstance().debug(String.format("Setting %s to '%s'", trackerKey, value));
+                    }
+                    values.add(value);
+                    args.put(trackerKey, values);
+                } else {
+                    DefaultLog.getInstance().warn(String.format("Key '%s' does not accept arguments.", trackerKey));
+                }
+            } else {
+                DefaultLog.getInstance().warn(String.format("Key '%s' is unknown", trackerKey));
+            }
+        }
+    }
+
+    /**
+     * Get the list of values for a key.
+     * @param key the key for the map.
+     * @return the list of values for the key or {@code null} if not set.
+     */
+    public Optional<List<String>> getArg(final String key) {
+        return Optional.ofNullable(args.get(key));
+    }
+
+    /**
+     * Add values to the key in the argument list.
+     * empty values are ignored. If no non-empty values are present no change is made.
+     * If the key does not exist, adds it.
+     * @param option the option to add values for.
+     * @param value the array of values to set.
+     */
+    public void addArg(final UIOption<?> option, final String... value) {
+        addArg(option.keyValue(), value);
+    }
+
+    /**
+     * Add values to the key in the argument list.
+     * empty values are ignored. If no non-empty values are present no change is made.
+     * If the key does not exist, adds it.
+     * @param trackerKey the key add values for.
+     * @param value the array of values to set.
+     */
+    public void addArg(final String trackerKey, final String... value) {
+        List<String> newValues = Arrays.stream(value).filter(StringUtils::isNotBlank).collect(Collectors.toList());
+        if (!newValues.isEmpty()) {
+            if (validateSet(trackerKey)) {
+                Option ratOption = Arg.findArg(trackerKey).find(trackerKey);
+                if (ratOption.hasArgs()) {
+                    if (DefaultLog.getInstance().isEnabled(Log.Level.DEBUG)) {
+                        DefaultLog.getInstance().debug(String.format("Adding [%s] to %s", String.join(", ", Arrays.asList(value)), trackerKey));
+                    }
+                    List<String> values = args.computeIfAbsent(trackerKey, k -> new ArrayList<>());
+                    values.addAll(newValues);
+                } else {
+                    DefaultLog.getInstance().warn(String.format("Key '%s' does not accept %sarguments.", trackerKey,
+                            ratOption.hasArg() ? "more that one " : ""));
+                }
+            } else {
+                DefaultLog.getInstance().warn(String.format("Key '%s' is unknown", trackerKey));
+            }
+        }
+    }
+
+    /**
+     * Remove a key from the argument list.
+     * @param option the option to remove the key for.
+     */
+    public void removeArg(final UIOption<?> option) {
+        args.remove(option.keyValue());
+    }
+
+    /**
+     * Remove a key from the argument list.
+     * @param trackerKey the key remove.
+     */
+    public void removeArg(final String trackerKey) {
+        args.remove(trackerKey);
+    }
+}
diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/UI.java b/apache-rat-core/src/main/java/org/apache/rat/ui/UI.java
new file mode 100644
index 00000000..db1df6a5
--- /dev/null
+++ b/apache-rat-core/src/main/java/org/apache/rat/ui/UI.java
@@ -0,0 +1,36 @@
+/*
+ * 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.rat.ui;
+
+public interface UI<T extends UIOption<T>> {
+    /**
+     * Gets the common name of this UI.
+     * @return the common name of this UI.
+     */
+    default String name() {
+        return getClass().getSimpleName();
+    }
+
+    /**
+     * Gets the OptionFactory configuration for this UI.
+     *
+     * @return the OptionFactory configuration for this UI.
+     */
+    UIOptionCollection<T> getOptionCollection();
+}
diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/UIOption.java b/apache-rat-core/src/main/java/org/apache/rat/ui/UIOption.java
new file mode 100644
index 00000000..677eafe2
--- /dev/null
+++ b/apache-rat-core/src/main/java/org/apache/rat/ui/UIOption.java
@@ -0,0 +1,224 @@
+/*
+ * 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.rat.ui;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.cli.Option;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.rat.OptionCollectionParser;
+
+import static java.lang.String.format;
+
+/**
+ * Abstract class that provides the framework for UI-specific RAT options.
+ * In this context UI option means an option expressed in the specific UI.
+ * @param <T> the concrete implementation of AbstractOption.
+ */
+public abstract class UIOption<T extends UIOption<T>> {
+    /** The pattern to match CLI options in text */
+    protected static final Pattern PATTERN = Pattern.compile("-(-[a-z0-9]+)+");
+    /** The actual UI-specific name for the option */
+    protected final Option option;
+    /** The name for the option */
+    protected final String name;
+    /** The argument type for this option */
+    protected final OptionCollectionParser.ArgumentType argumentType;
+    /** The AbstractOptionCollection associated with this AbstractOption */
+    protected final UIOptionCollection<T> optionCollection;
+
+    /**
+     * Constructor.
+     *
+     * @param option The CLI option
+     * @param name the UI-specific name for the option.
+     */
+    protected <C extends UIOptionCollection<T>> UIOption(final C optionCollection, final Option option, final String name) {
+        this.optionCollection = optionCollection;
+        this.option = option;
+        this.name = name;
+        argumentType = option.hasArg() ?
+                option.getArgName() == null ? OptionCollectionParser.ArgumentType.ARG :
+                OptionCollectionParser.ArgumentType.valueOf(option.getArgName().toUpperCase(Locale.ROOT)) :
+                OptionCollectionParser.ArgumentType.NONE;
+    }
+
+    /**
+     * Gets the AbstractOptionCollection that this option is a member of.
+     * @return the AbstractOptionCollection that this option is a member of.
+     */
+    public final UIOptionCollection<T> getOptionCollection() {
+        return optionCollection;
+    }
+
+    /**
+     * Gets the option this abstract option is wrapping.
+     * @return the original Option.
+     */
+    public final Option getOption() {
+        return option;
+    }
+
+    /**
+     * Return default value.
+     * @return default value or {@code null} if no argument given.
+     */
+    public final String getDefaultValue() {
+        return optionCollection.defaultValue(option);
+    }
+
+    /**
+     * Provide means to wrap the given option depending on the UI-specific option implementation.
+     * @param option The CLI option
+     * @return the cleaned up option name.
+     */
+    protected abstract String cleanupName(Option option);
+
+    /**
+     * Gets an example of how to use this option in the native UI.
+     * @return An example of how to use this option in the native UI.
+     */
+    public abstract String getExample();
+
+    /**
+     * Replaces CLI pattern options with implementation specific pattern options.
+     * @param str the string to clean.
+     * @return the string with CLI names replaced with implementation specific names.
+     */
+    public String cleanup(final String str) {
+        String workingStr = str;
+        if (StringUtils.isNotBlank(workingStr)) {
+            Map<String, String> maps = new HashMap<>();
+            Matcher matcher = PATTERN.matcher(workingStr);
+            while (matcher.find()) {
+                String key = matcher.group();
+                String optKey = key.substring(2);
+                Optional<Option> maybeResult = getOptionCollection().getOptions().getOptions().stream()
+                                .filter(o -> optKey.equals(o.getOpt()) || optKey.equals(o.getLongOpt())).findFirst();
+                maybeResult.ifPresent(value -> maps.put(key, cleanupName(value)));
+            }
+            for (Map.Entry<String, String> entry : maps.entrySet()) {
+                workingStr = workingStr.replaceAll(Pattern.quote(format("%s", entry.getKey())), entry.getValue());
+            }
+        }
+        return workingStr;
+    }
+
+    /**
+     * Gets the implementation specific name for the CLI option.
+     * @return The implementation specific name for the CLI option.
+     */
+    public final String getName() {
+        return name;
+    }
+
+    /**
+     * return a string showing long and short options if they are available. Will return
+     * a string.
+     * @return A string showing long and short options if they are available. Never {@code null}.
+     */
+    public abstract String getText();
+
+    /**
+     * Gets the description in implementation specific format.
+     *
+     * @return the description or an empty string.
+     */
+    public final String getDescription() {
+        return cleanup(option.getDescription());
+    }
+
+    /**
+     * Gets the simple class name for the data type for this option.
+     * Normally "String".
+     * @return the simple class name for the type.
+     */
+    public final Class<?> getType() {
+        return option.hasArg() ? ((Class<?>) option.getType()) : boolean.class;
+    }
+
+    /**
+     * Gets the argument name if there is one.
+     * @return the Argument name
+     */
+    public final String getArgName() {
+        return argumentType.getDisplayName();
+    }
+
+    /**
+     * Gets the argument type if there is one.
+     * @return the Argument name
+     */
+    public final OptionCollectionParser.ArgumentType getArgType() {
+        return argumentType;
+    }
+
+    /**
+     * Determines if the option is deprecated.
+     * @return {@code true} if the option is deprecated
+     */
+    public final boolean isDeprecated() {
+        return option.isDeprecated();
+    }
+
+    /**
+     * Determines if the option is required.
+     * @return {@code true} if the option is required.
+     */
+    public final boolean isRequired() {
+        return option.isRequired();
+    }
+
+    /**
+     * Determine if the enclosed option expects an argument.
+     * @return {@code true} if the enclosed option expects at least one argument.
+     */
+    public final boolean hasArg() {
+        return option.hasArg();
+    }
+
+    /**
+     * Returns {@code true} if the option has multiple arguments.
+     * @return {@code true} if the option has multiple arguments.
+     */
+    public final boolean hasArgs() {
+        return option.hasArgs();
+    }
+
+    /**
+     * The key value for the option.
+     * @return the key value for the CLI argument map.
+     */
+    public final String keyValue() {
+        return StringUtils.defaultIfEmpty(option.getLongOpt(), option.getOpt());
+    }
+
+    /**
+     * Gets the deprecated string if the option is deprecated, or an empty string otherwise.
+     * @return the deprecated string if the option is deprecated, or an empty string otherwise.
+     */
+    public final String getDeprecated() {
+        return  option.isDeprecated() ? cleanup(StringUtils.defaultIfEmpty(option.getDeprecated().toString(), StringUtils.EMPTY)) : StringUtils.EMPTY;
+    }
+}
diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/UIOptionCollection.java b/apache-rat-core/src/main/java/org/apache/rat/ui/UIOptionCollection.java
new file mode 100644
index 00000000..ab6dcf94
--- /dev/null
+++ b/apache-rat-core/src/main/java/org/apache/rat/ui/UIOptionCollection.java
@@ -0,0 +1,299 @@
+/*
+ * 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   *
+ *                                                              *
+ *   https://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.rat.ui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.TreeMap;
+import java.util.function.BiFunction;
+import java.util.stream.Stream;
+
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.rat.Defaults;
+import org.apache.rat.commandline.Arg;
+import org.apache.rat.utils.Log;
+
+/**
+ * A collection of options supported by the UI.  This includes RAT options and UI specific options.
+ * @param <T> the AbstractOption implementation.
+ */
+public class UIOptionCollection<T extends UIOption<T>> {
+    /** map of ARG to the associated UpdatableOptionGroup */
+    private final Map<Arg, UpdatableOptionGroup> argMap;
+    /** set of RAT OptionGroups with unsupported options for this UI removed */
+    private final UpdatableOptionGroupCollection supportedRatOptions;
+    /** set of UI specific options */
+    private final Map<Option, T> uiOptions;
+    /**
+     * Map of option to overridden default value.  Generally applies to supported rat options but may be ui
+     * specific options as well
+     */
+    private final Map <Option, String> defaultValues;
+
+    /**
+     * The function to generate a concrete BaseOption instance.
+     */
+    private final BiFunction<UIOptionCollection<T>, Option, T> mapper;
+
+    /**
+     * Construct the UIOptionCollection from the builder.
+     * @param builder the builder to build from.
+     */
+    protected UIOptionCollection(final Builder<T, ?> builder) {
+        Objects.requireNonNull(builder.mapper, "Builder.mapper");
+        argMap = new TreeMap<>();
+        mapper = builder.mapper;
+        supportedRatOptions = new UpdatableOptionGroupCollection();
+
+        for (Arg arg : Arg.values()) {
+            argMap.put(arg, supportedRatOptions.add(arg.group()));
+        }
+
+        for (Option opt : builder.unsupportedRatOptions) {
+            supportedRatOptions.findGroups(opt).forEach(group -> group.disableOption(opt));
+        }
+        uiOptions = new HashMap<>();
+        supportedRatOptions.options().getOptions()
+                .forEach(option -> uiOptions.put(option, mapper.apply(this, option)));
+        builder.uiOptions.stream().filter(option -> !uiOptions.containsKey(option))
+                .forEach(option -> uiOptions.put(option, mapper.apply(this, option)));
+        defaultValues = new HashMap<>(builder.defaultValues);
+    }
+
+    /**
+     * Checks if an Arg is selected.
+     * @param arg the Arg to check.
+     * @return {@code true} if the arg is selected.
+     */
+    public final boolean isSelected(final Arg arg) {
+        UpdatableOptionGroup group = argMap.get(arg);
+        return group != null && group.getSelected() != null;
+    }
+
+    /**
+     * Gets the selected Option for the arg.
+     * @param arg the arg to check.
+     * @return an Optional containing the selected option, or an empty Optional if none was selected.
+     */
+    public final Optional<Option> getSelected(final Arg arg) {
+        UpdatableOptionGroup group = argMap.get(arg);
+        String s = group == null ? null : group.getSelected();
+        if (s != null) {
+            for (Option result : group.getOptions()) {
+                if (result.getKey().equals(s)) {
+                    return Optional.of(result);
+                }
+            }
+        }
+        return Optional.empty();
+    }
+
+    /**
+     * Gets the collection of unsupported Options.
+     * @return the Options comprised for the unsupported options.
+     */
+    public final Options getUnsupportedOptions() {
+        return supportedRatOptions.unsupportedOptions();
+    }
+
+    /**
+     * Gets the UiOption instance for the Option.
+     * @param option the option to find the instance of.
+     * @return an UIOption instance that wraps the option.
+     */
+    public final T getMappedOption(final Option option) {
+        return uiOptions.get(option);
+    }
+
+    /**
+     * Gets an Options that contains the RAT Arg defined Option instances that are understood by this collection.
+     * OptionGroups are registered in the resulting Options object.
+     * @return an Options that contains the RAT Arg defined Option instances that are understood by this collection.
+     */
+    public final Options getOptions() {
+        return supportedRatOptions.options().addOptions(additionalOptions());
+    }
+
+    /**
+     * Gets the Stream of AbstractOption implementations  understood by this collection.
+     * @return the Stream of AbstractOption implementations understood by this collection.
+     */
+    public final Stream<T> getMappedOptions() {
+        return uiOptions.values().stream();
+    }
+
+    /**
+     * Gets a map client option name to specified AbstractOption implementation.
+     * @return a map client option name to specified AbstractOption implementation
+     */
+    public final Map<String, T> getOptionMap() {
+        Map<String, T> result = new TreeMap<>();
+        getMappedOptions().forEach(mappedOption -> result.put(ArgumentTracker.extractKey(mappedOption.getOption()), mappedOption));
+        return result;
+    }
+
+    /**
+     * Gets the additional options understood by this collection.
+     * @return the additional options understood by this collection.
+     */
+    public final Options additionalOptions() {
+        Options options = new Options();
+        uiOptions.keySet().stream()
+                .filter(option -> !supportedRatOptions.contains(option))
+                .forEach(options::addOption);
+        return options;
+    }
+
+    /**
+     * Gets the default value for the option.
+     * @param option the option to lookup.
+     * @return the default value or {@code null} if not set.
+     */
+    public final String defaultValue(final Option option) {
+        return defaultValues.get(option);
+    }
+
+    /**
+     * Builder for a BaseOptionCollection.
+     * @param <T> the concreate type of the BaseOption.
+     * @param <S> the concrete type being built.
+     */
+    protected static class Builder<T extends UIOption<T>, S extends Builder<T, S>> {
+        /** set of additional UI specific options */
+        private final List<Option> uiOptions;
+        /**
+         * Map of option to overridden default value.  Generally applies to supported rat options but may be ui
+         * specific options as well
+         */
+        private final Map <Option, String> defaultValues;
+        /** The list of unsupported Rat options */
+        protected final List<Option> unsupportedRatOptions;
+        /** The function to convert an option into a UIOption. */
+        private BiFunction<UIOptionCollection<T>, Option, T> mapper;
+
+        /**
+         * Constructor for the builder.
+         */
+        protected Builder() {
+            uiOptions = new ArrayList<>();
+            defaultValues = new HashMap<>();
+            unsupportedRatOptions = new ArrayList<>();
+            defaultValue(Arg.LOG_LEVEL,  Log.Level.WARN.name());
+            defaultValue(Arg.OUTPUT_ARCHIVE, Defaults.ARCHIVE_PROCESSING.name());
+            defaultValue(Arg.OUTPUT_STANDARD, Defaults.STANDARD_PROCESSING.name());
+            defaultValue(Arg.OUTPUT_LICENSES, Defaults.LIST_LICENSES.name());
+            defaultValue(Arg.OUTPUT_FAMILIES, Defaults.LIST_FAMILIES.name());
+        }
+
+        /**
+         * build the UIOptionCollection.
+         * @return the UIOptionCollection.
+         */
+        public UIOptionCollection<T> build() {
+            return new UIOptionCollection<>(this);
+        }
+
+        /**
+         * Returns this cast to {@code <S>} class.
+         * @return this as {@code <S>} class.
+         */
+        protected final S self() {
+            return (S) this;
+        }
+
+        /**
+         * Set the mapper for the builder.
+         * @param mapper the function to convert an option into a UIOption ({@code <T>} object).
+         * @return this
+         */
+        public S mapper(final BiFunction<UIOptionCollection<T>, Option, T> mapper) {
+            this.mapper = mapper;
+            return self();
+        }
+
+        /**
+         * Add a UI option to the collection.
+         * @param uiOption the UI Option to add.
+         * @return this
+         */
+        public S uiOption(final Option uiOption) {
+            uiOptions.add(uiOption);
+            return self();
+        }
+
+        /**
+         * Add a UI options to the collection.
+         * @param uiOption the UIOptions ({@code <T>} objects) to add.
+         * @return this
+         */
+        public S uiOptions(final Option... uiOption) {
+            uiOptions.addAll(Arrays.asList(uiOption));
+            return self();
+        }
+
+        /**
+         * Register an option as unsupported.
+         * @param option the option that is not be supported.  This should be an option in the
+         * {@link Arg} collection.
+         * @return this
+         */
+        public S unsupported(final Option option) {
+            unsupportedRatOptions.add(option);
+            return self();
+        }
+
+        /**
+         * Register multiple options as unsupported.
+         * Will ignore all the options associated with the specified Arg.
+         * @param arg The Arg to ignore.
+         * @return this
+         */
+        public S unsupported(final Arg arg) {
+            unsupportedRatOptions.addAll(arg.group().getOptions());
+            return self();
+        }
+
+        /**
+         * Specify the default values for an option.
+         * @param option the option to specify the default value for.
+         * @param value the value for the option.
+         * @return this
+         */
+        public S defaultValue(final Option option, final String value) {
+            defaultValues.put(option, value);
+            return self();
+        }
+
+        /**
+         * Specify the default values for an Arg.
+         * @param arg the Arg to specify the default value for.
+         * @param value the value for the option.
+         * @return this
+         */
+        public S defaultValue(final Arg arg, final String value) {
+            return defaultValue(arg.option(), value);
+        }
+    }
+}
diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroup.java b/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroup.java
new file mode 100644
index 00000000..e936357e
--- /dev/null
+++ b/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroup.java
@@ -0,0 +1,85 @@
+/*
+ * 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
+ *
+ *   https://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.rat.ui;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.OptionGroup;
+
+/**
+ * An implementation of Apache Commons CLI OptionGroup that allows options to be removed (disabled).
+ */
+public final class UpdatableOptionGroup extends OptionGroup {
+    /** The set of options to remove */
+    private final Set<Option> disabledOptions = new HashSet<>();
+
+    /**
+     * Converts the group into an UpdatableOptionGroup if it is not already an instance
+     * @param group the group to convert.
+     * @return an UpdatableOptionGroup.
+     */
+    public static UpdatableOptionGroup create(final OptionGroup group) {
+        return group instanceof UpdatableOptionGroup ? (UpdatableOptionGroup) group : new UpdatableOptionGroup(group);
+    }
+
+    private UpdatableOptionGroup(final OptionGroup group) {
+        group.getOptions().forEach(super::addOption);
+    }
+
+    /**
+     * Disable an option in the group.
+     * @param option The option to disable.
+     */
+    public void disableOption(final Option option) {
+        disabledOptions.add(option);
+    }
+
+    public boolean isEmpty() {
+        return getOptions().isEmpty();
+    }
+
+    /**
+     * Gets the disabled options for this group.
+     * @return the set of disabled options for this group.
+     */
+    public Stream<Option> getDisableOptions() {
+        return disabledOptions.stream();
+    }
+    /**
+     * Reset the group so that all disabled options are re-enabled.
+     */
+    public void reset() {
+        disabledOptions.clear();
+    }
+
+    @Override
+    public Collection<Option> getOptions() {
+        return super.getOptions().stream().filter(opt -> !disabledOptions.contains(opt)).toList();
+    }
+
+    @Override
+    public UpdatableOptionGroup addOption(final Option option) {
+        super.addOption(option);
+        return this;
+    }
+}
diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroupCollection.java b/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroupCollection.java
new file mode 100644
index 00000000..893db66a
--- /dev/null
+++ b/apache-rat-core/src/main/java/org/apache/rat/ui/UpdatableOptionGroupCollection.java
@@ -0,0 +1,110 @@
+/*
+ * 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   *
+ *                                                              *
+ *   https://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.rat.ui;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.OptionGroup;
+import org.apache.commons.cli.Options;
+
+/**
+ * A collection of UpdatableOptionGroups.
+ */
+public class UpdatableOptionGroupCollection {
+    /** the contained UpdatableOptionGroups */
+    private final List<UpdatableOptionGroup> updatableOptionGroups;
+
+    /**
+     * Creates an empty collection.
+     */
+    public UpdatableOptionGroupCollection() {
+        updatableOptionGroups = new ArrayList<>();
+    }
+
+    /**
+     * Adds an OptionGroup to the collection.  If the OptionGroup is not an UpdatableOptionGroup
+     * it is converted first.
+     * @param optionGroup an OptionGroup to add.
+     * @return the UpdatableOptionGroup that was added.
+     */
+    public UpdatableOptionGroup add(final OptionGroup optionGroup) {
+        UpdatableOptionGroup uog = UpdatableOptionGroup.create(optionGroup);
+        updatableOptionGroups.add(uog);
+        return uog;
+    }
+
+    /**
+     * Gets an Options object from this collection.
+     * @return an Options object.
+     */
+    public Options options() {
+        Options result = new Options();
+        updatableOptionGroups.forEach(result::addOptionGroup);
+        return result;
+    }
+
+    /**
+     * Gets ll the UpdatableOptionGroups that the option is in.
+     * @param option the option to searhc for.
+     * @return the stream of UpdatableOptionGroups the option is in.
+     */
+    public Stream<UpdatableOptionGroup> findGroups(final Option option) {
+        return updatableOptionGroups.stream().filter(og -> og.getOptions().contains(option));
+    }
+
+    /**
+     * Gets the set of removed Options from the collection.
+     * @return the set of removed options.
+     */
+    public Set<Option> removedOptions() {
+        Set<Option> result = new HashSet<>();
+        updatableOptionGroups.forEach(uog -> uog.getDisableOptions().forEach(result::add));
+        return result;
+    }
+
+    /**
+     * Gets the unsupported options
+     * If multiple options from the a group are disabled they will be added to the
+     * options in a group together.
+     * @return the Options object containing all the unsupported options.
+     */
+    public Options unsupportedOptions() {
+        Options result = new Options();
+        for (UpdatableOptionGroup uog : updatableOptionGroups) {
+            OptionGroup group = new OptionGroup();
+            uog.getDisableOptions().forEach(group::addOption);
+            result.addOptionGroup(group);
+        }
+        return result;
+    }
+
+    /**
+     * Returns true if the option is in any of the groups.
+     * @param option the option.
+     * @return {@code true} if the option is in any of the groups.
+     */
+    public boolean contains(final Option option) {
+        return updatableOptionGroups.stream().anyMatch(og -> og.getOptions().contains(option));
+    }
+}
diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/package-info.java b/apache-rat-core/src/main/java/org/apache/rat/ui/package-info.java
new file mode 100644
index 00000000..765d92c1
--- /dev/null
+++ b/apache-rat-core/src/main/java/org/apache/rat/ui/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.                                           *
+ */
+
+/**
+ * Classes that support UI generation and interoperability.
+ */
+package org.apache.rat.ui;
diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/spi/UIProvider.java b/apache-rat-core/src/main/java/org/apache/rat/ui/spi/UIProvider.java
new file mode 100644
index 00000000..9870b3cf
--- /dev/null
+++ b/apache-rat-core/src/main/java/org/apache/rat/ui/spi/UIProvider.java
@@ -0,0 +1,25 @@
+/*
+ * 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.rat.ui.spi;
+
+import org.apache.rat.ui.UI;
+
+public interface UIProvider {
+    UI<?> create();
+}
diff --git a/apache-rat-core/src/main/java/org/apache/rat/ui/spi/package-info.java b/apache-rat-core/src/main/java/org/apache/rat/ui/spi/package-info.java
new file mode 100644
index 00000000..9d724675
--- /dev/null
+++ b/apache-rat-core/src/main/java/org/apache/rat/ui/spi/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+/**
+ * The SPI implementation for the UIs
+ */
+package org.apache.rat.ui.spi;
diff --git a/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java b/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java
index 7f27cb8d..28e856b0 100644
--- a/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java
+++ b/apache-rat-core/src/test/java/org/apache/rat/commandline/ArgTests.java
@@ -23,6 +23,7 @@ import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.DefaultParser;
 import org.apache.commons.cli.Options;
 import org.apache.commons.cli.ParseException;
+import org.apache.rat.CLIOptionCollection;
 import org.apache.rat.DeprecationReporter;
 import org.apache.rat.OptionCollection;
 import org.apache.rat.ReportConfiguration;
@@ -58,7 +59,7 @@ public class ArgTests {
         CommandLine commandLine = createCommandLine(new String[] {"--output-file", fileName});
         OutputFileConfig configuration = new OutputFileConfig();
         ArgumentContext ctxt = new ArgumentContext(new File("."), configuration, commandLine);
-        Arg.processArgs(ctxt);
+        Arg.processArgs(ctxt, CLIOptionCollection.INSTANCE);
         assertThat(configuration.actual.getAbsolutePath()).isEqualTo(expected.getCanonicalPath());
     }
 }
diff --git a/apache-rat-core/src/test/java/org/apache/rat/ui/ArgumentTrackerTest.java b/apache-rat-core/src/test/java/org/apache/rat/ui/ArgumentTrackerTest.java
new file mode 100644
index 00000000..80c5e1cc
--- /dev/null
+++ b/apache-rat-core/src/test/java/org/apache/rat/ui/ArgumentTrackerTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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
+ *
+ *   https://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.rat.ui;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Predicate;
+import org.apache.commons.cli.Option;
+import org.apache.rat.commandline.Arg;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.apache.rat.ui.UIOptionCollectionTest.TestingUIOptionCollection;
+import static org.apache.rat.ui.UIOptionCollectionTest.TestingUIOption;
+
+public class ArgumentTrackerTest {
+    private ArgumentTracker underTest;
+    private TestingUIOptionCollection testingUIOptionCollection;
+
+    @BeforeEach
+    public void setUp() {
+        testingUIOptionCollection = new TestingUIOptionCollection();
+        underTest = new ArgumentTracker(testingUIOptionCollection);
+    }
+
+    @Test
+    void extractKey() {
+        assertThat(ArgumentTracker.extractKey(Option.builder().longOpt("foo").build())).isEqualTo("foo");
+        assertThat(ArgumentTracker.extractKey(Option.builder("b").build())).isEqualTo("b");
+        assertThat(ArgumentTracker.extractKey(Option.builder("b").longOpt("foo").build())).isEqualTo("foo");
+        assertThatThrownBy(() -> ArgumentTracker.extractKey(Option.builder().build()))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("Either opt or longOpt must be specified");
+    }
+
+    @Test
+    void args() {
+        // no args to start
+        assertThat(underTest.args()).isEmpty();
+        Option option = findOptionWithArgs(1);
+        String string1 = String.format("--%s foo", ArgumentTracker.extractKey(option));
+        TestingUIOption mappedOption = testingUIOptionCollection.getMappedOption(option);
+        underTest.setArg(mappedOption, "foo");
+        option = findOptionWithArgs(2);
+        String string2 = String.format("--%s bar baz", ArgumentTracker.extractKey(option));
+        mappedOption = testingUIOptionCollection.getMappedOption(option);
+        underTest.addArg(mappedOption, "bar");
+        underTest.addArg(mappedOption, "baz");
+        String join = String.join(" ", underTest.args());
+        assertThat(join).contains(string1);
+        assertThat(join).contains(string2);
+    }
+
+    @Test
+    void setArg() {
+        Option option = findOptionWithArgs(1);
+        TestingUIOption mappedOption = testingUIOptionCollection.getMappedOption(option);
+        underTest.setArg(mappedOption, "foo");
+        assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("foo"));
+    }
+
+    private Option findOptionWithArgs(int number) {
+        Predicate<Option> filter;
+        if (number <= 0) {
+            filter = opt -> !opt.hasArg();
+        } else if (number == 1) {
+            filter = opt -> opt.hasArg() && ! opt.hasArgs();
+        } else {
+            filter = opt -> opt.hasArgs();
+        }
+        return Arrays.stream(Arg.values()).map(Arg::option).filter(filter).findAny().orElseThrow();
+    }
+
+    @Test
+    void addArg() {
+        Option option = findOptionWithArgs(2);
+        TestingUIOption mappedOption = testingUIOptionCollection.getMappedOption(option);
+        underTest.addArg(mappedOption, "foo");
+        assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("foo"));
+        underTest.addArg(mappedOption, "bar");
+        assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("foo", "bar"));
+    }
+
+    @Test
+    void setOverridesAddArg() {
+        Option option = findOptionWithArgs(2);
+        TestingUIOption mappedOption = testingUIOptionCollection.getMappedOption(option);
+        underTest.addArg(mappedOption, "foo");
+        assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("foo"));
+        underTest.addArg(mappedOption, "bar");
+        assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("foo", "bar"));
+        underTest.setArg(mappedOption, "baz");
+        assertThat(underTest.getArg(mappedOption.keyValue())).contains(List.of("baz"));
+    }
+
+    @Test
+    void invalidAbstractOption() {
+        Option option = Option.builder().longOpt("notAValidOption").build();
+        TestingUIOption invalidOption = new TestingUIOption(testingUIOptionCollection, option);
+        underTest.addArg(invalidOption, "foo");
+        assertThat(underTest.getArg(invalidOption.keyValue())).isEmpty();
+    }
+
+}
diff --git a/apache-rat-core/src/test/java/org/apache/rat/ui/UIOptionCollectionTest.java b/apache-rat-core/src/test/java/org/apache/rat/ui/UIOptionCollectionTest.java
new file mode 100644
index 00000000..9325df75
--- /dev/null
+++ b/apache-rat-core/src/test/java/org/apache/rat/ui/UIOptionCollectionTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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
+ *
+ *   https://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.rat.ui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+import org.apache.commons.cli.AlreadySelectedException;
+import org.apache.commons.cli.DeprecatedAttributes;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.OptionGroup;
+import org.apache.commons.cli.Options;
+import org.apache.rat.commandline.Arg;
+import org.apache.rat.utils.CasedString;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class UIOptionCollectionTest {
+
+    public static final Option UI_OPTION = Option.builder("ui-option1").build();
+    public static final Option DEPRECATED_UI_OPTION = Option.builder("ui-option2").deprecated().build();
+
+    public static class TestingUIOptionCollection extends UIOptionCollection<TestingUIOption> {
+        public TestingUIOptionCollection() {
+            super(new Builder());
+
+        }
+        private static class Builder extends UIOptionCollection.Builder<TestingUIOption, Builder> {
+            Builder() {
+                mapper(TestingUIOption::new)
+                        .uiOption(UI_OPTION)
+                        .uiOption(DEPRECATED_UI_OPTION)
+                        .unsupported(Arg.COUNTER_MAX)
+                        .unsupported(Arg.EXCLUDE.option())
+                        .defaultValue(UI_OPTION, "foo");
+
+            }
+        }
+    }
+
+    static class TestingUIOption extends UIOption<TestingUIOption> {
+
+        TestingUIOption(final UIOptionCollection<TestingUIOption> collection, final Option option) {
+            super(collection, option, new CasedString(CasedString.StringCase.KEBAB, ArgumentTracker.extractKey(option)).toCase(CasedString.StringCase.DOT));
+        }
+
+        @Override
+        protected String cleanupName(Option option) {
+            return new CasedString(CasedString.StringCase.KEBAB, ArgumentTracker.extractKey(option)).toCase(CasedString.StringCase.DOT);
+        }
+
+        @Override
+        public String getExample() {
+            return String.format("The example for $s", cleanupName(option));
+        }
+
+        @Override
+        public String getText() {
+            return String.format("Short and long options for ", cleanupName(option));
+        }
+    }
+
+    private final TestingUIOptionCollection underTest = new TestingUIOptionCollection();
+
+    private Optional<Option> findDeprecatedArgOption() {
+        Collection<Option> options = underTest.getOptions().getOptions();
+        return Arg.getOptions().getOptions().stream().filter(Option::isDeprecated)
+                .filter(option -> options.contains(option)).findAny();
+    }
+
+    private Optional<Option> findDeprecatedOption() {
+        Collection<Option> options = underTest.getOptions().getOptions();
+        return Arg.getOptions().getOptions().stream().filter(Option::isDeprecated)
+                .filter(option -> !options.contains(option)).findAny();
+    }
+
+    @Test
+    void getMappedOption() {
+        TestingUIOption one = underTest.getMappedOption(UI_OPTION);
+        assertThat(one.option).isEqualTo(UI_OPTION);
+        assertThat(one.name).isEqualTo("ui.option1");
+        assertThat(one.isDeprecated()).isFalse();
+        TestingUIOption two = underTest.getMappedOption(DEPRECATED_UI_OPTION);
+        assertThat(two.option).isEqualTo(DEPRECATED_UI_OPTION);
+        assertThat(two.name).isEqualTo("ui.option2");
+        assertThat(two.isDeprecated()).isTrue();
+
+        assertThat(underTest.getMappedOption(Arg.EXCLUDE.option())).isNull();
+        for (Option option : Arg.COUNTER_MAX.group().getOptions()) {
+            assertThat(underTest.getMappedOption(option)).isNull();
+        }
+
+        TestingUIOption config = underTest.getMappedOption(Arg.CONFIGURATION.option());
+        assertThat(config).isNotNull();
+        assertThat(config.option).isEqualTo(Arg.CONFIGURATION.option());
+        assertThat(config.name).isEqualTo("config");
+
+        config = underTest.getMappedOption(Option.builder("foo").build());
+        assertThat(config).isNull();
+    }
+
+    @Test
+    void getSelected() throws AlreadySelectedException {
+        assertThat(underTest.isSelected(Arg.CONFIGURATION)).isFalse();
+        assertThat(underTest.getSelected(Arg.CONFIGURATION)).isEmpty();
+
+        Option option = Arg.CONFIGURATION.option();
+        OptionGroup group = underTest.getOptions().getOptionGroup(Arg.CONFIGURATION.option());
+        group.setSelected(option);
+
+        assertThat(underTest.isSelected(Arg.CONFIGURATION)).isTrue();
+        assertThat(underTest.getSelected(Arg.CONFIGURATION)).contains(option);
+    }
+
+    @Test
+    void getUnsupportedOptions() {
+        Options options = underTest.getUnsupportedOptions();
+        Collection<Option> OptionCollection = options.getOptions();
+        List<Option> expected = new ArrayList<Option>();
+        expected.addAll(Arg.COUNTER_MAX.group().getOptions());
+        expected.add(Arg.EXCLUDE.option());
+        assertThat(options.getOptions()).containsExactlyInAnyOrderElementsOf(expected);
+    }
+
+    @Test
+    void getAdditionalOptions() {
+        assertThat(underTest.additionalOptions().getOptions()).containsExactlyInAnyOrder(UI_OPTION, DEPRECATED_UI_OPTION);
+    }
+
+    @Test
+    void defaultValue() {
+        assertThat(underTest.defaultValue(UI_OPTION)).isEqualTo("foo");
+        assertThat(underTest.defaultValue(DEPRECATED_UI_OPTION)).isNull();
+    }
+}
diff --git a/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java b/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java
index fc00e5e1..5ac78b82 100644
--- a/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java
+++ b/apache-rat-tools/src/main/java/org/apache/rat/documentation/options/CLIOption.java
@@ -7,7 +7,7 @@
  * "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
+ *   https://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
diff --git a/pom.xml b/pom.xml
index 253189e0..8865a5f0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -85,6 +85,16 @@ agnostic home for software distribution comprehension and audit tools.
   </distributionManagement>
   <dependencyManagement>
     <dependencies>
+      <dependency>
+        <groupId>com.github.spotbugs</groupId>
+        <artifactId>spotbugs-annotations</artifactId>
+        <version>4.9.8</version>
+      </dependency>
+      <dependency>
+        <groupId>org.reflections</groupId>
+        <artifactId>reflections</artifactId>
+        <version>0.10.2</version>
+      </dependency>
       <!-- used to render the site and make skin updates more transparent -->
       <dependency>
         <groupId>org.apache.maven.skins</groupId>