View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   */
19  package org.apache.rat.commandline;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.lang.reflect.Array;
25  import java.nio.charset.StandardCharsets;
26  import java.nio.file.Files;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.List;
30  import java.util.function.BiConsumer;
31  import java.util.function.Predicate;
32  
33  import org.apache.commons.cli.AlreadySelectedException;
34  import org.apache.commons.cli.CommandLine;
35  import org.apache.commons.cli.DeprecatedAttributes;
36  import org.apache.commons.cli.Option;
37  import org.apache.commons.cli.OptionGroup;
38  import org.apache.commons.cli.Options;
39  import org.apache.commons.cli.ParseException;
40  import org.apache.commons.io.IOUtils;
41  import org.apache.commons.lang3.tuple.Pair;
42  import org.apache.rat.ConfigurationException;
43  import org.apache.rat.Defaults;
44  import org.apache.rat.ImplementationException;
45  import org.apache.rat.ReportConfiguration;
46  import org.apache.rat.config.AddLicenseHeaders;
47  import org.apache.rat.config.exclusion.ExclusionUtils;
48  import org.apache.rat.config.exclusion.StandardCollection;
49  import org.apache.rat.document.DocumentName;
50  import org.apache.rat.document.DocumentNameMatcher;
51  import org.apache.rat.license.LicenseSetFactory;
52  import org.apache.rat.report.claim.ClaimStatistic.Counter;
53  import org.apache.rat.ui.UIOptionCollection;
54  import org.apache.rat.utils.DefaultLog;
55  import org.apache.rat.utils.Log;
56  
57  import static java.lang.String.format;
58  
59  /**
60   * An enumeration of options that are recommended across all UIs. A UI may not implement some options if they are unsupportable
61   * within the UI.
62   * Each Arg contains:
63   * <ul>
64   *      <li>An OptionGroup that contains the individual options that all resolve to the same option.
65   * This allows us to deprecate options as we move forward in development.</li>
66   * <li>A {@code BiConsumer<ArgumentContext, Option>} that defines the process to configure the option in
67   * the {@code ArgumentContext.configuration}.</li>
68   * </ul>
69   */
70  public enum Arg {
71      ///////////////////////// EDIT OPTIONS
72      /**
73       * Defines options to add copyright to files.
74       */
75      EDIT_COPYRIGHT(new OptionGroup()
76              .addOption(Option.builder("c")
77                      .longOpt("copyright").hasArg()
78                      .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
79                              .setDescription(StdMsgs.useMsg("--edit-copyright")).get())
80                      .desc("The copyright message to use in the license headers.")
81                      .build())
82              .addOption(Option.builder().longOpt("edit-copyright").hasArg()
83                      .desc("The copyright message to use in the license headers. Usually in the form of \"Copyright 2008 Foo\".  "
84                              + "Only valid with --edit-license")
85                      .build()),
86      Arg::doNotExecute
87      ),
88  
89      /**
90       * Causes file updates to overwrite existing files.
91       */
92      EDIT_OVERWRITE(new OptionGroup()
93              .addOption(Option.builder("f").longOpt("force")
94                      .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
95                              .setDescription(StdMsgs.useMsg("--edit-overwrite")).get())
96                      .desc("Forces any changes in files to be written directly to the source files so that new files are not created.")
97                      .build())
98              .addOption(Option.builder().longOpt("edit-overwrite")
99                      .desc("Forces any changes in files to be written directly to the source files so that new files are not created. "
100                             + "Only valid with --edit-license.")
101                     .build()),
102             Arg::doNotExecute
103     ),
104 
105     /**
106      * Defines options to add licenses to files.
107      */
108     EDIT_ADD(new OptionGroup()
109             .addOption(Option.builder("a")
110                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
111                             .setDescription(StdMsgs.useMsg("--edit-license")).get())
112                     .build())
113             .addOption(Option.builder("A").longOpt("addLicense")
114                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
115                             .setDescription(StdMsgs.useMsg("--edit-license")).get())
116                     .desc("Add the Apache-2.0 license header to any file with an unknown license that is not in the exclusion list.")
117                     .build())
118             .addOption(Option.builder().longOpt("edit-license").desc(
119                     "Add the Apache-2.0 license header to any file with an unknown license that is not in the exclusion list. "
120                             + "By default new files will be created with the license header, "
121                             + "to force the modification of existing files use the --edit-overwrite option.").build()
122             ),
123             Arg::doNotExecute
124     ),
125 
126     //////////////////////////// CONFIGURATION OPTIONS
127     /**
128      * Group of options that read a configuration file.
129      */
130     CONFIGURATION(new OptionGroup()
131             .addOption(Option.builder().longOpt("config").hasArgs().argName("File")
132                     .desc("File names for system configuration.")
133                     .converter(Converters.FILE_CONVERTER)
134                     .type(DocumentName.class)
135                     .build())
136             .addOption(Option.builder().longOpt("licenses").hasArgs().argName("File")
137                     .desc("File names for system configuration.")
138                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--config")).get())
139                     .converter(Converters.FILE_CONVERTER)
140                     .type(DocumentName.class)
141                     .build()),
142             Arg::doNotExecute
143     ),
144 
145     /**
146      * Group of options that skip the default configuration file.
147      */
148     CONFIGURATION_NO_DEFAULTS(new OptionGroup()
149             .addOption(Option.builder().longOpt("configuration-no-defaults")
150                     .desc("Ignore default configuration.").build())
151             .addOption(Option.builder().longOpt("no-default-licenses")
152                     .deprecated(DeprecatedAttributes.builder()
153                             .setSince("0.17")
154                             .setForRemoval(true)
155                             .setDescription(StdMsgs.useMsg("--configuration-no-defaults")).get())
156                     .desc("Ignore default configuration.")
157                     .build()),
158             Arg::doNotExecute
159     ),
160 
161     /**
162      * Option that adds approved licenses to the list.
163      */
164     LICENSES_APPROVED(new OptionGroup().addOption(Option.builder().longOpt("licenses-approved").hasArg().argName("LicenseID")
165             .desc("A comma separated list of approved License IDs. These licenses will be added to the list of approved licenses.")
166             .converter(Converters.TEXT_LIST_CONVERTER)
167             .type(String[].class)
168             .build()),
169             (context, selected) ->
170                 context.getConfiguration().addApprovedLicenseIds(processArrayArg(context, selected))
171     ),
172 
173     /**
174      * Option that adds approved licenses from a file.
175      */
176     LICENSES_APPROVED_FILE(new OptionGroup().addOption(Option.builder().longOpt("licenses-approved-file").hasArg().argName("File")
177             .desc("Name of file containing comma separated lists of approved License IDs.")
178             .converter(Converters.FILE_CONVERTER)
179             .type(DocumentName.class)
180             .build()),
181             (context, selected) ->
182                     context.getConfiguration().addApprovedLicenseIds(processArrayFile(context, selected))),
183 
184     /**
185      * Option that specifies approved license families.
186      */
187     FAMILIES_APPROVED(new OptionGroup().addOption(Option.builder().longOpt("license-families-approved").hasArg().argName("FamilyID")
188             .desc("A comma separated list of approved license family IDs. These license families will be added to the list of approved license families.")
189             .converter(Converters.TEXT_LIST_CONVERTER)
190             .type(String[].class)
191             .build()),
192             (context, selected) -> context.getConfiguration().addApprovedLicenseCategories(processArrayArg(context, selected))),
193 
194     /**
195      * Option that specifies approved license families from a file.
196      */
197     FAMILIES_APPROVED_FILE(new OptionGroup().addOption(Option.builder().longOpt("license-families-approved-file").hasArg().argName("File")
198             .desc("Name of file containing comma separated lists of approved family IDs.")
199             .converter(Converters.FILE_CONVERTER)
200             .type(DocumentName.class)
201             .build()),
202             (context, selected) -> context.getConfiguration().addApprovedLicenseCategories(processArrayFile(context, selected))
203     ),
204 
205     /**
206      * Option to remove licenses from the approved list.
207      */
208     LICENSES_DENIED(new OptionGroup().addOption(Option.builder().longOpt("licenses-denied").hasArg().argName("LicenseID")
209             .desc("A comma separated list of denied License IDs. " +
210                     "These licenses will be removed from the list of approved licenses. " +
211                     "Once licenses are removed they can not be added back.")
212             .converter(Converters.TEXT_LIST_CONVERTER)
213             .type(String[].class)
214             .build()),
215             (context, selected) -> context.getConfiguration().removeApprovedLicenseIds(processArrayArg(context, selected))),
216 
217     /**
218      * Option to read a file licenses to be removed from the approved list.
219      */
220     LICENSES_DENIED_FILE(new OptionGroup().addOption(Option.builder().longOpt("licenses-denied-file")
221             .hasArg().argName("File").type(File.class)
222             .converter(Converters.FILE_CONVERTER)
223             .desc("Name of file containing comma separated lists of the denied license IDs. " +
224                     "These licenses will be removed from the list of approved licenses. " +
225                     "Once licenses are removed they can not be added back.")
226             .build()),
227             (context, selected) -> context.getConfiguration().removeApprovedLicenseIds(processArrayFile(context, selected))),
228 
229     /**
230      * Option to list license families to remove from the approved list.
231      */
232     FAMILIES_DENIED(new OptionGroup().addOption(Option.builder().longOpt("license-families-denied")
233             .hasArg().argName("FamilyID")
234             .desc("A comma separated list of denied License family IDs. " +
235                     "These license families will be removed from the list of approved licenses. " +
236                     "Once license families are removed they can not be added back.")
237             .converter(Converters.TEXT_LIST_CONVERTER)
238             .type(String[].class)
239             .build()),
240             (context, selected) -> context.getConfiguration().removeApprovedLicenseCategories(processArrayArg(context, selected))),
241 
242     /**
243      * Option to read a list of license families to remove from the approved list.
244      */
245     FAMILIES_DENIED_FILE(new OptionGroup().addOption(Option.builder().longOpt("license-families-denied-file").hasArg().argName("File")
246             .desc("Name of file containing comma separated lists of denied license IDs. " +
247                     "These license families will be removed from the list of approved licenses. " +
248                     "Once license families are removed they can not be added back.")
249             .type(DocumentName.class)
250             .converter(Converters.FILE_CONVERTER)
251             .build()),
252             (context, selected) -> context.getConfiguration().removeApprovedLicenseCategories(processArrayFile(context, selected))),
253 
254     /**
255      * Option to specify an acceptable number of various counters.
256      */
257     COUNTER_MAX(new OptionGroup().addOption(Option.builder().longOpt("counter-max").hasArgs().argName("CounterPattern")
258             .desc("The acceptable maximum number for the specified counter. A value of '-1' specifies an unlimited number.")
259             .converter(Converters.COUNTER_CONVERTER)
260             .type(Pair.class)
261             .build()),
262             (context, selected) -> {
263                 for (String arg : context.getCommandLine().getOptionValues(selected)) {
264                     Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg);
265                     int limit = pair.getValue();
266                     context.getConfiguration().getClaimValidator().setMax(pair.getKey(), limit < 0 ? Integer.MAX_VALUE : limit);
267                 }
268             }),
269 
270     /**
271      * Option to specify an acceptable number of various counters.
272      */
273     COUNTER_MIN(new OptionGroup().addOption(Option.builder().longOpt("counter-min").hasArgs().argName("CounterPattern")
274             .desc("The minimum number for the specified counter.")
275             .converter(Converters.COUNTER_CONVERTER)
276             .type(Pair.class)
277             .build()),
278             (context, selected) -> {
279                 for (String arg : context.getCommandLine().getOptionValues(selected)) {
280                     Pair<Counter, Integer> pair = Converters.COUNTER_CONVERTER.apply(arg);
281                     context.getConfiguration().getClaimValidator().setMin(pair.getKey(), pair.getValue());
282                 }
283             }),
284 
285 ////////////////// INPUT OPTIONS
286     /**
287      * Reads files to test from a file.
288      */
289     SOURCE(new OptionGroup()
290             .addOption(Option.builder().longOpt("input-source").hasArgs().argName("File")
291                     .desc("A file containing file names to process. " +
292                             "File names must use linux directory separator ('/') or none at all. " +
293                             "File names that do not start with '/' are relative to the directory where the " +
294                             "argument is located.")
295                     .converter(Converters.FILE_CONVERTER)
296                     .type(DocumentName.class)
297                     .build()),
298             (context, selected) -> {
299                 DocumentName[] documentNames = getParsedOptionValues(selected, context.getCommandLine());
300                 for (DocumentName documentName : documentNames) {
301                     context.getConfiguration().addSource(documentName.asFile());
302                 }
303             }),
304 
305     /**
306      * Excludes files by expression.
307      */
308     EXCLUDE(new OptionGroup()
309             .addOption(Option.builder("e").longOpt("exclude").hasArgs().argName("Expression")
310                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
311                             .setDescription(StdMsgs.useMsg("--input-exclude")).get())
312                     .desc("Excludes files matching <Expression>.")
313                     .build())
314             .addOption(Option.builder().longOpt("input-exclude").hasArgs().argName("Expression")
315                     .desc("Excludes files matching <Expression>.")
316                     .build()),
317             (context, selected) -> {
318                 String[] excludes = context.getCommandLine().getOptionValues(selected);
319                 if (excludes != null) {
320                     context.getConfiguration().addExcludedPatterns(Arrays.asList(excludes));
321                 }
322             }),
323 
324     /**
325      * Excludes files based on the contents of a file.
326      */
327     EXCLUDE_FILE(new OptionGroup()
328             .addOption(Option.builder("E").longOpt("exclude-file")
329                     .argName("File").hasArg().type(File.class)
330                     .converter(Converters.FILE_CONVERTER)
331                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
332                             .setDescription(StdMsgs.useMsg("--input-exclude-file")).get())
333                     .desc("Reads <Expression> entries from a file. Entries will be excluded from processing.")
334                     .build())
335             .addOption(Option.builder().longOpt("input-exclude-file")
336                     .argName("File").hasArg().type(File.class)
337                     .converter(Converters.FILE_CONVERTER)
338                     .desc("Reads <Expression> entries from a file. Entries will be excluded from processing.")
339                     .build()),
340             (context, selected) -> {
341                 try {
342                     DocumentName excludeFileName = context.getCommandLine().getParsedOptionValue(selected);
343                     if (excludeFileName != null) {
344                         context.getConfiguration().addExcludedPatterns(ExclusionUtils.asIterable(excludeFileName.asFile(), "#"));
345                     }
346                 } catch (Exception e) {
347                     throw ConfigurationException.from(e);
348                 }
349             }),
350     /**
351      * Excludes files based on standard groupings.
352      */
353     EXCLUDE_STD(new OptionGroup()
354             .addOption(Option.builder().longOpt("input-exclude-std").argName("StandardCollection")
355                     .hasArgs().converter(s -> StandardCollection.valueOf(s.toUpperCase()))
356                     .desc("Excludes files defined in standard collections based on commonly occurring groups. " +
357                             "Excludes any path matcher actions but DOES NOT exclude any file processor actions.")
358                     .type(StandardCollection.class)
359                     .build()),
360             (context, selected) -> {
361                 for (String s : context.getCommandLine().getOptionValues(selected)) {
362                     context.getConfiguration().addExcludedCollection(StandardCollection.valueOf(s));
363                 }
364             }),
365 
366     /**
367      * Excludes files if they are smaller than the given threshold.
368      */
369     EXCLUDE_SIZE(new OptionGroup()
370             .addOption(Option.builder().longOpt("input-exclude-size").argName("Integer")
371                     .hasArg().type(Integer.class)
372                     .desc("Excludes files with sizes less than the number of bytes specified.")
373                     .build()),
374             (context, selected) -> {
375                 try {
376                     final int maxSize = context.getCommandLine().getParsedOptionValue(selected);
377                     DocumentNameMatcher matcher = new DocumentNameMatcher(String.format("File size < %s bytes", maxSize),
378                             (Predicate<DocumentName>) documentName -> {
379                                 File f = new File(documentName.getName());
380                                 return f.isFile() && f.length() < maxSize;
381                             });
382                     context.getConfiguration().addExcludedMatcher(matcher);
383                 } catch (Exception e) {
384                     throw ConfigurationException.from(e);
385                 }
386             }),
387     /**
388      * Excludes files by expression.
389      */
390     INCLUDE(new OptionGroup()
391             .addOption(Option.builder().longOpt("input-include").hasArgs().argName("Expression")
392                     .desc("Includes files matching <Expression>. Will override excluded files.")
393                     .build())
394             .addOption(Option.builder().longOpt("include").hasArgs().argName("Expression")
395                     .desc("Includes files matching <Expression>. Will override excluded files.")
396                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
397                             .setDescription(StdMsgs.useMsg("--input-include")).get())
398                     .build()),
399             (context, selected) -> {
400                 String[] includes = context.getCommandLine().getOptionValues(selected);
401                 if (includes != null) {
402                     context.getConfiguration().addIncludedPatterns(Arrays.asList(includes));
403                 }
404             }),
405 
406     /**
407      * Includes files based on the contents of a file.
408      */
409     INCLUDE_FILE(new OptionGroup()
410             .addOption(Option.builder().longOpt("input-include-file")
411                     .argName("File").hasArg().type(File.class)
412                     .converter(Converters.FILE_CONVERTER)
413                     .desc("Reads <Expression> entries from a file. Entries will override excluded files.")
414                     .build())
415             .addOption(Option.builder().longOpt("includes-file")
416                     .argName("File").hasArg().type(File.class)
417                     .converter(Converters.FILE_CONVERTER)
418                     .desc("Reads <Expression> entries from a file. Entries will override excluded files.")
419                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
420                             .setDescription(StdMsgs.useMsg("--input-include-file")).get())
421                     .build()),
422             (context, selected) -> {
423                 try {
424                     DocumentName includeFileName = context.getCommandLine().getParsedOptionValue(selected);
425                     if (includeFileName != null) {
426                         context.getConfiguration().addIncludedPatterns(ExclusionUtils.asIterable(includeFileName.asFile(), "#"));
427                     }
428                 } catch (Exception e) {
429                     throw ConfigurationException.from(e);
430                 }
431             }),
432 
433     /**
434      * Includes files based on standard groups.
435      */
436     INCLUDE_STD(new OptionGroup()
437             .addOption(Option.builder().longOpt("input-include-std").argName("StandardCollection")
438                     .hasArgs().converter(s -> StandardCollection.valueOf(s.toUpperCase()))
439                     .desc("Includes files defined in standard collections based on commonly occurring groups. " +
440                             "Includes any path matcher actions but DOES NOT include any file processor actions.")
441                     .type(StandardCollection.class)
442                     .build())
443             .addOption(Option.builder().longOpt("scan-hidden-directories")
444                     .desc("Scans hidden directories.")
445                     .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
446                             .setDescription(StdMsgs.useMsg("--input-include-std with 'HIDDEN_DIR' argument")).get()).build()
447             ),
448             (context, selected) -> {
449                 // display deprecation log if needed.
450                 if (context.getCommandLine().hasOption("scan-hidden-directories")) {
451                     context.getConfiguration().addIncludedCollection(StandardCollection.HIDDEN_DIR);
452                 } else {
453                     for (String s : context.getCommandLine().getOptionValues(selected)) {
454                         context.getConfiguration().addIncludedCollection(StandardCollection.valueOf(s));
455                     }
456                 }
457             }),
458 
459     /**
460      * Excludes files based on SCM exclusion file processing.
461      */
462     EXCLUDE_PARSE_SCM(new OptionGroup()
463             .addOption(Option.builder().longOpt("input-exclude-parsed-scm")
464                     .argName("StandardCollection")
465                     .hasArgs().converter(s -> StandardCollection.valueOf(s.toUpperCase()))
466                     .desc("Parse SCM based exclusion files to exclude specified files and directories. " +
467                             "This action can apply to any standard collection that implements a file processor.")
468                     .type(StandardCollection.class)
469                     .build()),
470             (context, selected) -> {
471                 StandardCollection[] collections = getParsedOptionValues(selected, context.getCommandLine());
472                 final ReportConfiguration configuration = context.getConfiguration();
473                 for (StandardCollection collection : collections) {
474                     if (collection == StandardCollection.ALL) {
475                         Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedFileProcessor);
476                         Arrays.asList(StandardCollection.values()).forEach(configuration::addExcludedCollection);
477                     } else {
478                         configuration.addExcludedFileProcessor(collection);
479                         configuration.addExcludedCollection(collection);
480                     }
481                 }
482             }),
483 
484     /**
485      * Stop processing an input stream and declare an input file.
486      */
487     DIR(new OptionGroup().addOption(Option.builder().option("d").longOpt("dir").hasArg()
488             .type(File.class)
489             .desc("Used to indicate end of list when using options that take multiple arguments.").argName("DirOrArchive")
490             .deprecated(DeprecatedAttributes.builder().setForRemoval(true).setSince("0.17")
491                     .setDescription("Use the standard '--' to signal the end of arguments.").get()).build()),
492             Arg::doNotExecute
493     ),
494 
495     /////////////// OUTPUT OPTIONS
496     /**
497      * Defines the stylesheet to use.
498      */
499     OUTPUT_STYLE(new OptionGroup()
500             .addOption(Option.builder().longOpt("output-style").hasArg().argName("StyleSheet")
501                     .desc("XSLT stylesheet to use when creating the report. "
502                             + "Either an external xsl file may be specified or one of the internal named sheets.")
503                     .build())
504             .addOption(Option.builder("s").longOpt("stylesheet").hasArg().argName("StyleSheet")
505                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-style")).get())
506                     .desc("XSLT stylesheet to use when creating the report.")
507                     .build())
508             .addOption(Option.builder("x").longOpt("xml")
509                     .deprecated(DeprecatedAttributes.builder()
510                             .setSince("0.17")
511                             .setForRemoval(true)
512                             .setDescription(StdMsgs.useMsg("--output-style with the 'xml' argument")).get())
513                     .desc("forces XML output rather than the textual report.")
514                     .build()),
515             (context, selected) -> {
516                 String key = selected.getKey(); // is not null due to above isSelected()-call
517                 if ("x".equals(key)) {
518                     // display deprecated message.
519                     context.getCommandLine().hasOption("x");
520                     context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet("xml"));
521                 } else {
522                     String[] style = context.getCommandLine().getOptionValues(selected);
523                     if (style.length != 1) {
524                         DefaultLog.getInstance().error("Please specify a single stylesheet");
525                         throw new ConfigurationException("Please specify a single stylesheet");
526                     }
527                     context.getConfiguration().setStyleSheet(StyleSheets.getStyleSheet(style[0]));
528                 }
529             }),
530 
531     /**
532      * Specifies the license definitions that should be included in the output.
533      */
534     OUTPUT_LICENSES(new OptionGroup()
535             .addOption(Option.builder().longOpt("output-licenses").hasArg().argName("LicenseFilter")
536                     .desc("List the defined licenses.")
537                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
538                     .build())
539             .addOption(Option.builder().longOpt("list-licenses").hasArg().argName("LicenseFilter")
540                     .desc("List the defined licenses.")
541                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
542                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-licenses")).get())
543                     .build()),
544             (context, selected) -> {
545                 try {
546                     context.getConfiguration().listLicenses(context.getCommandLine().getParsedOptionValue(selected));
547                 } catch (ParseException e) {
548                     context.logParseException(e, selected, Defaults.LIST_LICENSES);
549                 }
550             }),
551 
552     /**
553      * Specifies the license families that should be included in the output.
554      */
555     OUTPUT_FAMILIES(new OptionGroup()
556             .addOption(Option.builder().longOpt("output-families").hasArg().argName("LicenseFilter")
557                     .desc("List the defined license families.")
558                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
559                     .build())
560             .addOption(Option.builder().longOpt("list-families").hasArg().argName("LicenseFilter")
561                     .desc("List the defined license families.")
562                     .converter(s -> LicenseSetFactory.LicenseFilter.valueOf(s.toUpperCase()))
563                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-families")).get())
564                     .build()),
565             (context, selected) -> {
566                 try {
567                     context.getConfiguration().listFamilies(context.getCommandLine().getParsedOptionValue(selected));
568                 } catch (ParseException e) {
569                     context.logParseException(e, selected, Defaults.LIST_FAMILIES);
570                 }
571             }),
572 
573     /**
574      * Specifies the log level to log messages at.
575      */
576     LOG_LEVEL(new OptionGroup().addOption(Option.builder().longOpt("log-level")
577             .hasArg().argName("LogLevel")
578             .desc("Sets the log level.")
579             .converter(s -> Log.Level.valueOf(s.toUpperCase()))
580             .build()),
581             (context, selected) -> {
582                 Log dLog = DefaultLog.getInstance();
583                 try {
584                     dLog.setLevel(context.getCommandLine().getParsedOptionValue(selected));
585                 } catch (ParseException e) {
586                     logParseException(DefaultLog.getInstance(), e, selected, context.getCommandLine(), dLog.getLevel());
587                 }
588             }),
589 
590     /**
591      * Specifies that the run should not perform any updates to files.
592      */
593     DRY_RUN(new OptionGroup().addOption(Option.builder().longOpt("dry-run")
594             .desc("If set do not update the files but generate the reports.")
595             .build()),
596             (context, selected) ->
597                     context.getConfiguration().setDryRun(true)
598     ),
599 
600     /**
601      * Specifies where the output should be written.
602      */
603     OUTPUT_FILE(new OptionGroup()
604             .addOption(Option.builder().option("o").longOpt("out").hasArg().argName("File")
605                     .desc("Define the output file where to write a report to.")
606                     .deprecated(DeprecatedAttributes.builder().setSince("0.17").setForRemoval(true).setDescription(StdMsgs.useMsg("--output-file")).get())
607                     .type(DocumentName.class)
608                     .converter(Converters.FILE_CONVERTER)
609                     .build())
610             .addOption(Option.builder().longOpt("output-file").hasArg().argName("File")
611                     .desc("Define the output file where to write a report to.")
612                     .type(DocumentName.class)
613                     .converter(Converters.FILE_CONVERTER)
614                     .build()),
615             (context, selected) -> {
616                 try {
617                     DocumentName documentName = context.getCommandLine().getParsedOptionValue(selected);
618                     File document = documentName.asFile();
619                     File parent = document.getParentFile();
620                     if (!parent.mkdirs() && !parent.isDirectory()) {
621                         DefaultLog.getInstance().error("Could not create report parent directory " + documentName);
622                     }
623                     context.getConfiguration().setOut(document);
624                 } catch (ParseException e) {
625                     // we write to system out by default.
626                     context.logParseException(e, selected, "System.out");
627                     context.getConfiguration().setOut(ReportConfiguration.SYSTEM_OUT);
628                 }
629             }),
630 
631     /**
632      * Specifies the level of reporting detail for archive files.
633      */
634     OUTPUT_ARCHIVE(new OptionGroup()
635             .addOption(Option.builder().longOpt("output-archive").hasArg().argName("ProcessingType")
636                     .desc("Specifies the level of detail in ARCHIVE file reporting.")
637                     .converter(s -> ReportConfiguration.Processing.valueOf(s.toUpperCase()))
638                     .build()),
639             (context, selected) -> {
640                 try {
641                     context.getConfiguration().setArchiveProcessing(context.getCommandLine().getParsedOptionValue(selected));
642                 } catch (ParseException e) {
643                     context.logParseException(e, selected, Defaults.ARCHIVE_PROCESSING);
644                 }
645             }
646     ),
647 
648     /**
649      * Specifies the level of reporting detail for standard files.
650      */
651     OUTPUT_STANDARD(new OptionGroup()
652             .addOption(Option.builder().longOpt("output-standard").hasArg().argName("ProcessingType")
653                     .desc("Specifies the level of detail in STANDARD file reporting.")
654                     .converter(s -> ReportConfiguration.Processing.valueOf(s.toUpperCase()))
655                     .build()),
656             (context, selected) -> {
657                 try {
658                     context.getConfiguration().setStandardProcessing(context.getCommandLine().getParsedOptionValue(selected));
659                 } catch (ParseException e) {
660                     context.logParseException(e, selected, Defaults.STANDARD_PROCESSING);
661                 }
662             }),
663 
664     /**
665      * Provide license definition listing of registered licenses.
666      */
667     HELP_LICENSES(new OptionGroup()
668             .addOption(Option.builder().longOpt("help-licenses") //
669                     .desc("Print information about registered licenses.").build()),
670             Arg::doNotExecute
671     );
672 
673     /**
674      * The option group for the argument.
675      */
676     private final OptionGroup group;
677 
678     /**
679      * The BiConsumer to apply the option to update the state of the context.configuration.
680      */
681     private final BiConsumer<ArgumentContext, Option> process;
682 
683     /**
684      * This method is used for an implementation marker. The options use this as the test process should be handled before
685      * the standard processing. For example, EDIT_COPYRIGHT is only valid if EDIT_ADD is specified. The processes that handle EDIT_ADD
686      * and EDIT_COPYRIGHT do not call the execute method as they have to make extra calls to display deprecated messages and otherwise
687      * properly execute. If somehow, a UI attempts to execute them the UI testing should fail. This method ensures that happens when it
688      * is specified as the process for the option.
689      * @param context the current argument context.
690      * @param selected the selected option.
691      */
692     private static void doNotExecute(final ArgumentContext context, final Option selected) {
693         throw new ImplementationException(String.format("'%s' should not be executed directly", selected));
694     }
695 
696     /**
697      * Creates an Arg from an Option group.
698      *
699      * @param group The option group.
700      * @param process The BiConsumer that executes the argument. Generally these processes apply the argument to the configuration or
701      *                other component of the ArgumentContext.
702      */
703     Arg(final OptionGroup group, final BiConsumer<ArgumentContext, Option> process) {
704         this.group = group;
705         this.process = process;
706     }
707 
708     /**
709      * Executes the process associated with this Arg if the collection has an Option from this group selected.
710      * @param context the ArgumentContext that is being processed.
711      * @param optionCollection the OptionCollection that is available.
712      */
713     private void execute(final ArgumentContext context, final UIOptionCollection<?> optionCollection) {
714         optionCollection.getSelected(this)
715                 .ifPresent(selected -> this.process.accept(context, selected));
716     }
717 
718     /**
719      * Determines if all the options have been removed from this argument.
720      *
721      * @return {@code true} if all the options have been removed from this argument.
722      */
723     public boolean isEmpty() {
724         return this.group().getOptions().isEmpty();
725     }
726 
727     /**
728      * Finds the element associated with the key within the element group.
729      *
730      * @param key the key to search for.
731      * @return the matching Option.
732      * @throws IllegalArgumentException if the key can not be found.
733      */
734     public Option find(final String key) {
735         for (Option result : group.getOptions()) {
736             if (key.equals(result.getKey()) || key.equals(result.getLongOpt())) {
737                 return result;
738             }
739         }
740         throw new IllegalArgumentException("Can not find " + key);
741     }
742 
743     /**
744      * Gets the group for this arg.
745      *
746      * @return the option group for this arg.
747      */
748     public OptionGroup group() {
749         return group;
750     }
751 
752     /**
753      * Returns the first non-deprecated option from the group.
754      *
755      * @return the first non-deprecated option or, if no non-deprecated option is available, the first option.
756      */
757     public Option option() {
758         Option first = null;
759         for (Option result : group.getOptions()) {
760             if (first == null) {
761                 first = result;
762             }
763             if (!result.isDeprecated()) {
764                 return result;
765             }
766         }
767         return first;
768     }
769 
770     /**
771      * Gets the full set of options.
772      *
773      * @return the full set of options for this Arg.
774      */
775     public static Options getOptions() {
776         Options options = new Options();
777         for (Arg arg : Arg.values()) {
778             options.addOptionGroup(arg.group);
779         }
780         return options;
781     }
782 
783     /**
784      * Processes the edit arguments.
785      *
786      * @param context the context to work with.
787      */
788     private static void processEditArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) {
789         optionCollection.getSelected(EDIT_ADD).ifPresent(option -> {
790             // prints deprecation
791             context.getCommandLine().hasOption(option);
792             boolean force = optionCollection.isSelected(EDIT_OVERWRITE);
793             if (force) {
794                 // prints deprecation
795                 optionCollection.getSelected(EDIT_OVERWRITE).ifPresent(context.getCommandLine()::hasOption);
796             }
797             context.getConfiguration().setAddLicenseHeaders(force ? AddLicenseHeaders.FORCED : AddLicenseHeaders.TRUE);
798             optionCollection.getSelected(EDIT_COPYRIGHT).
799                     ifPresent(editOption -> context.getConfiguration().setCopyrightMessage(context.getCommandLine().getOptionValue(editOption)));
800         });
801     }
802 
803     /**
804      * Gets the list of Strings that are arguments for the option.
805      * @param context the ArgumentContext containing the command line.
806      * @param selected the selected option.
807      * @return the list of Strings that are arguments.
808      */
809     private static List<String> processArrayArg(final ArgumentContext context, final Option selected) {
810         try {
811             return Arrays.asList(context.getCommandLine().getParsedOptionValue(selected));
812         } catch (ParseException e) {
813             throw new ConfigurationException(e);
814         }
815     }
816 
817     /**
818      * Parses the option as a file.
819      * @param context the Argument context that provides the command line.
820      * @param selected the selected option.
821      * @return Option as a file.
822      */
823     private static File commandLineFile(final ArgumentContext context, final Option selected) throws ParseException {
824         DocumentName documentName = context.getCommandLine().getParsedOptionValue(selected);
825         return documentName.asFile();
826     }
827 
828     /**
829      * Parses lines with comma separated tokens from a file and returns the entire collection of tokens as a list of strings.
830      * @param context the Argument context that provides the command line.
831      * @param selected the selected option.
832      * @return the list of strings parsed from the file.
833      */
834     private static List<String> processArrayFile(final ArgumentContext context, final Option selected) {
835         List<String> result = new ArrayList<>();
836         try (InputStream in = Files.newInputStream(commandLineFile(context, selected)
837                 .toPath())) {
838             for (String line : IOUtils.readLines(in, StandardCharsets.UTF_8)) {
839                 String[] ids = Converters.TEXT_LIST_CONVERTER.apply(line);
840                 result.addAll(Arrays.asList(ids));
841             }
842             return result;
843         } catch (IOException e) {
844             throw new ConfigurationException(e);
845 
846         } catch (ParseException e) {
847             throw ConfigurationException.from(e);
848         }
849     }
850 
851     /**
852      * Processes the configuration options.
853      *
854      * @param context the context to process.
855      * @throws ConfigurationException if configuration files can not be read.
856      */
857     private static void processConfigurationArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException {
858 
859         Defaults.Builder defaultBuilder = Defaults.builder();
860 
861         optionCollection.getSelected(CONFIGURATION).ifPresent(
862                 selected -> {
863                     DocumentName[] documentNames = getParsedOptionValues(selected, context.getCommandLine());
864                     for (DocumentName documentName : documentNames) {
865                         defaultBuilder.add(documentName.asFile());
866                     }
867                 });
868         optionCollection.getSelected(CONFIGURATION_NO_DEFAULTS).ifPresent(selected -> {
869             // display deprecation log if needed.
870             context.getCommandLine().hasOption(selected);
871             defaultBuilder.noDefault();
872         });
873         context.getConfiguration().setFrom(defaultBuilder.build());
874 
875         for (Arg arg : List.of(FAMILIES_APPROVED, FAMILIES_APPROVED_FILE, FAMILIES_DENIED, FAMILIES_DENIED_FILE,
876                 LICENSES_APPROVED, LICENSES_APPROVED_FILE, LICENSES_DENIED, LICENSES_DENIED_FILE,
877                 COUNTER_MAX, COUNTER_MIN)) {
878             arg.execute(context, optionCollection);
879         }
880     }
881 
882     /**
883      * Process the input setup.
884      *
885      * @param context the context to work in.
886      * @throws ConfigurationException if an exclude file can not be read.
887      */
888     private static void processInputArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException {
889         for (Arg arg : List.of(SOURCE, EXCLUDE, EXCLUDE_FILE, EXCLUDE_STD, EXCLUDE_PARSE_SCM, EXCLUDE_SIZE,
890                 INCLUDE, INCLUDE_FILE, INCLUDE_STD)) {
891             arg.execute(context, optionCollection);
892         }
893     }
894 
895     /**
896      * Logs a ParseException as a warning.
897      *
898      * @param log the Log to write to
899      * @param exception the parse exception to log
900      * @param opt the option being processed
901      * @param cl the command line being processed
902      * @param defaultValue The default value the option is being set to.
903      */
904     private static void logParseException(final Log log, final ParseException exception, final Option opt, final CommandLine cl, final Object defaultValue) {
905         log.warn(format("Invalid %s specified: %s ", opt.getOpt(), cl.getOptionValue(opt)));
906         log.warn(format("%s set to: %s", opt.getOpt(), defaultValue));
907         log.debug(exception);
908     }
909 
910     /**
911      * Process the log level setting.
912      *
913      * @param context The argument context
914      */
915     public static void processLogLevel(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException {
916         LOG_LEVEL.execute(context, optionCollection);
917     }
918 
919     /**
920      * Process the arguments.
921      *
922      * @param context the context in which to process the args.
923      * @throws ConfigurationException on error
924      */
925     public static void processArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException {
926         Converters.FILE_CONVERTER.setWorkingDirectory(context.getWorkingDirectory());
927         processOutputArgs(context, optionCollection);
928         processEditArgs(context, optionCollection);
929         processInputArgs(context, optionCollection);
930         processConfigurationArgs(context, optionCollection);
931     }
932 
933     /**
934      * Process the arguments that can be processed together.
935      *
936      * @param context the context in which to process the args.
937      */
938     private static void processOutputArgs(final ArgumentContext context, final UIOptionCollection<?> optionCollection) throws ConfigurationException {
939         for (Arg arg : List.of(DRY_RUN, OUTPUT_FAMILIES, OUTPUT_LICENSES, OUTPUT_ARCHIVE, OUTPUT_STANDARD, OUTPUT_FILE, OUTPUT_STYLE)) {
940             arg.execute(context, optionCollection);
941         }
942     }
943 
944     /**
945      * Resets the groups in the Args so that they are unused and ready to detect the next set of arguments.
946      */
947     public static void reset() {
948         for (Arg a : Arg.values()) {
949             try {
950                 a.group.setSelected(null);
951             } catch (AlreadySelectedException e) {
952                 throw new RuntimeException("Should not happen", e);
953             }
954         }
955     }
956 
957     /**
958      * Finds the Arg that contains an Option with the specified key.
959      *
960      * @param key the key for the Option to locate.
961      * @return The Arg or {@code null} if no Arg is found.
962      */
963     public static Arg findArg(final String key) {
964         if (key != null) {
965             for (Arg arg : Arg.values()) {
966                 for (Option candidate : arg.group.getOptions()) {
967                     if (key.equals(candidate.getKey()) || key.equals(candidate.getLongOpt())) {
968                         return arg;
969                     }
970                 }
971             }
972         }
973         return null;
974     }
975 
976     private static <T> T[] getParsedOptionValues(final Option selected, final CommandLine commandLine) {
977         try {
978             Class<? extends T> clazz = (Class<? extends T>) selected.getType();
979             String[] values = commandLine.getOptionValues(selected);
980             T[] result = (T[]) Array.newInstance(clazz, values.length);
981             for (int i = 0; i < values.length; i++) {
982                 result[i] = clazz.cast(selected.getConverter().apply(values[i]));
983             }
984             return result;
985         } catch (Throwable t) {
986             throw new ConfigurationException(format("'%s' converter for %s '%s' does not produce a class of type %s", selected,
987                     selected.getKey(), selected.getConverter().getClass().getName(), selected.getType()), t);
988         }
989     }
990 
991     /**
992      * Standard messages used in descriptions.
993      */
994     public static final class StdMsgs {
995         private StdMsgs() {
996             // do not instantiate
997         }
998 
999         /**
1000          * Gets the standard "use instead" message for the specific name.
1001          *
1002          * @param name the name of the option to use instead.
1003          * @return combined "use instead" message.
1004          */
1005         public static String useMsg(final String name) {
1006             return format("Use %s instead.", name);
1007         }
1008     }
1009 }