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