1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.wrapper.cli;
20
21 import java.io.OutputStreamWriter;
22 import java.io.PrintWriter;
23 import java.io.Writer;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.Comparator;
29 import java.util.Formatter;
30 import java.util.HashMap;
31 import java.util.HashSet;
32 import java.util.LinkedHashMap;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.TreeSet;
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 public class CommandLineParser {
63 private Map<String, CommandLineOption> optionsByString = new HashMap<>();
64
65 private boolean allowMixedOptions;
66
67 private boolean allowUnknownOptions;
68
69 private final PrintWriter deprecationPrinter;
70
71 public CommandLineParser() {
72 this(new OutputStreamWriter(System.out));
73 }
74
75 public CommandLineParser(Writer deprecationPrinter) {
76 this.deprecationPrinter = new PrintWriter(deprecationPrinter);
77 }
78
79
80
81
82
83
84
85
86 public ParsedCommandLine parse(String... commandLine) throws CommandLineArgumentException {
87 return parse(Arrays.asList(commandLine));
88 }
89
90
91
92
93
94
95
96
97 public ParsedCommandLine parse(Iterable<String> commandLine) throws CommandLineArgumentException {
98 ParsedCommandLine parsedCommandLine =
99 new ParsedCommandLine(new HashSet<CommandLineOption>(optionsByString.values()));
100 ParserState parseState = new BeforeFirstSubCommand(parsedCommandLine);
101 for (String arg : commandLine) {
102 if (parseState.maybeStartOption(arg)) {
103 if ("--".equals(arg)) {
104 parseState = new AfterOptions(parsedCommandLine);
105 } else if (arg.matches("--[^=]+")) {
106 OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(2));
107 parseState = parsedOption.onStartNextArg();
108 } else if (arg.matches("--[^=]+=.*")) {
109 int endArg = arg.indexOf('=');
110 OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(2, endArg));
111 parseState = parsedOption.onArgument(arg.substring(endArg + 1));
112 } else if (arg.matches("-[^=]=.*")) {
113 OptionParserState parsedOption = parseState.onStartOption(arg, arg.substring(1, 2));
114 parseState = parsedOption.onArgument(arg.substring(3));
115 } else {
116 assert arg.matches("-[^-].*");
117 String option = arg.substring(1);
118 if (optionsByString.containsKey(option)) {
119 OptionParserState parsedOption = parseState.onStartOption(arg, option);
120 parseState = parsedOption.onStartNextArg();
121 } else {
122 String option1 = arg.substring(1, 2);
123 OptionParserState parsedOption;
124 if (optionsByString.containsKey(option1)) {
125 parsedOption = parseState.onStartOption("-" + option1, option1);
126 if (parsedOption.getHasArgument()) {
127 parseState = parsedOption.onArgument(arg.substring(2));
128 } else {
129 parseState = parsedOption.onComplete();
130 for (int i = 2; i < arg.length(); i++) {
131 String optionStr = arg.substring(i, i + 1);
132 parsedOption = parseState.onStartOption("-" + optionStr, optionStr);
133 parseState = parsedOption.onComplete();
134 }
135 }
136 } else {
137 if (allowUnknownOptions) {
138
139 parsedOption = parseState.onStartOption(arg, option);
140 parseState = parsedOption.onComplete();
141 } else {
142
143
144 parsedOption = parseState.onStartOption("-" + option1, option1);
145 parseState = parsedOption.onComplete();
146 }
147 }
148 }
149 }
150 } else {
151 parseState = parseState.onNonOption(arg);
152 }
153 }
154
155 parseState.onCommandLineEnd();
156 return parsedCommandLine;
157 }
158
159 public CommandLineParser allowMixedSubcommandsAndOptions() {
160 allowMixedOptions = true;
161 return this;
162 }
163
164 public CommandLineParser allowUnknownOptions() {
165 allowUnknownOptions = true;
166 return this;
167 }
168
169
170
171
172
173
174 public void printUsage(Appendable out) {
175 Formatter formatter = new Formatter(out);
176 Set<CommandLineOption> orderedOptions = new TreeSet<>(new OptionComparator());
177 orderedOptions.addAll(optionsByString.values());
178 Map<String, String> lines = new LinkedHashMap<>();
179 for (CommandLineOption option : orderedOptions) {
180 Set<String> orderedOptionStrings = new TreeSet<>(new OptionStringComparator());
181 orderedOptionStrings.addAll(option.getOptions());
182 List<String> prefixedStrings = new ArrayList<>();
183 for (String optionString : orderedOptionStrings) {
184 if (optionString.length() == 1) {
185 prefixedStrings.add("-" + optionString);
186 } else {
187 prefixedStrings.add("--" + optionString);
188 }
189 }
190
191 String key = join(prefixedStrings, ", ");
192 String value = option.getDescription();
193 if (value == null || value.length() == 0) {
194 value = "";
195 }
196
197 lines.put(key, value);
198 }
199 int max = 0;
200 for (String optionStr : lines.keySet()) {
201 max = Math.max(max, optionStr.length());
202 }
203 for (Map.Entry<String, String> entry : lines.entrySet()) {
204 if (entry.getValue().length() == 0) {
205 formatter.format("%s%n", entry.getKey());
206 } else {
207 formatter.format("%-" + max + "s %s%n", entry.getKey(), entry.getValue());
208 }
209 }
210 formatter.flush();
211 }
212
213 private static String join(Collection<?> things, String separator) {
214 StringBuffer buffer = new StringBuffer();
215 boolean first = true;
216
217 if (separator == null) {
218 separator = "";
219 }
220
221 for (Object thing : things) {
222 if (!first) {
223 buffer.append(separator);
224 }
225 buffer.append(thing.toString());
226 first = false;
227 }
228 return buffer.toString();
229 }
230
231
232
233
234
235
236
237 public CommandLineOption option(String... options) {
238 for (String option : options) {
239 if (optionsByString.containsKey(option)) {
240 throw new IllegalArgumentException(String.format("Option '%s' is already defined.", option));
241 }
242 if (option.startsWith("-")) {
243 throw new IllegalArgumentException(
244 String.format("Cannot add option '%s' as an option cannot" + " start with '-'.", option));
245 }
246 }
247 CommandLineOption option = new CommandLineOption(Arrays.asList(options));
248 for (String optionStr : option.getOptions()) {
249 this.optionsByString.put(optionStr, option);
250 }
251 return option;
252 }
253
254 private static final class OptionString {
255 private final String arg;
256
257 private final String option;
258
259 private OptionString(String arg, String option) {
260 this.arg = arg;
261 this.option = option;
262 }
263
264 public String getDisplayName() {
265 return arg.startsWith("--") ? "--" + option : "-" + option;
266 }
267
268 @Override
269 public String toString() {
270 return getDisplayName();
271 }
272 }
273
274 private abstract static class ParserState {
275 public abstract boolean maybeStartOption(String arg);
276
277 boolean isOption(String arg) {
278 return arg.matches("-.+");
279 }
280
281 public abstract OptionParserState onStartOption(String arg, String option);
282
283 public abstract ParserState onNonOption(String arg);
284
285 public void onCommandLineEnd() {}
286 }
287
288 private abstract class OptionAwareParserState extends ParserState {
289 protected final ParsedCommandLine commandLine;
290
291 protected OptionAwareParserState(ParsedCommandLine commandLine) {
292 this.commandLine = commandLine;
293 }
294
295 @Override
296 public boolean maybeStartOption(String arg) {
297 return isOption(arg);
298 }
299
300 @Override
301 public ParserState onNonOption(String arg) {
302 commandLine.addExtraValue(arg);
303 return allowMixedOptions ? new AfterFirstSubCommand(commandLine) : new AfterOptions(commandLine);
304 }
305 }
306
307 private final class BeforeFirstSubCommand extends OptionAwareParserState {
308 private BeforeFirstSubCommand(ParsedCommandLine commandLine) {
309 super(commandLine);
310 }
311
312 @Override
313 public OptionParserState onStartOption(String arg, String option) {
314 OptionString optionString = new OptionString(arg, option);
315 CommandLineOption commandLineOption = optionsByString.get(option);
316 if (commandLineOption == null) {
317 if (allowUnknownOptions) {
318 return new UnknownOptionParserState(arg, commandLine, this);
319 } else {
320 throw new CommandLineArgumentException(
321 String.format("Unknown command-line option '%s'.", optionString));
322 }
323 }
324 return new KnownOptionParserState(optionString, commandLineOption, commandLine, this);
325 }
326 }
327
328 private final class AfterFirstSubCommand extends OptionAwareParserState {
329 private AfterFirstSubCommand(ParsedCommandLine commandLine) {
330 super(commandLine);
331 }
332
333 @Override
334 public OptionParserState onStartOption(String arg, String option) {
335 CommandLineOption commandLineOption = optionsByString.get(option);
336 if (commandLineOption == null) {
337 return new UnknownOptionParserState(arg, commandLine, this);
338 }
339 return new KnownOptionParserState(new OptionString(arg, option), commandLineOption, commandLine, this);
340 }
341 }
342
343 private static final class AfterOptions extends ParserState {
344 private final ParsedCommandLine commandLine;
345
346 private AfterOptions(ParsedCommandLine commandLine) {
347 this.commandLine = commandLine;
348 }
349
350 @Override
351 public boolean maybeStartOption(String arg) {
352 return false;
353 }
354
355 @Override
356 public OptionParserState onStartOption(String arg, String option) {
357 return new UnknownOptionParserState(arg, commandLine, this);
358 }
359
360 @Override
361 public ParserState onNonOption(String arg) {
362 commandLine.addExtraValue(arg);
363 return this;
364 }
365 }
366
367 private static final class MissingOptionArgState extends ParserState {
368 private final OptionParserState option;
369
370 private MissingOptionArgState(OptionParserState option) {
371 this.option = option;
372 }
373
374 @Override
375 public boolean maybeStartOption(String arg) {
376 return isOption(arg);
377 }
378
379 @Override
380 public OptionParserState onStartOption(String arg, String option) {
381 return this.option.onComplete().onStartOption(arg, option);
382 }
383
384 @Override
385 public ParserState onNonOption(String arg) {
386 return option.onArgument(arg);
387 }
388
389 @Override
390 public void onCommandLineEnd() {
391 option.onComplete();
392 }
393 }
394
395 private abstract static class OptionParserState {
396 public abstract ParserState onStartNextArg();
397
398 public abstract ParserState onArgument(String argument);
399
400 public abstract boolean getHasArgument();
401
402 public abstract ParserState onComplete();
403 }
404
405 private final class KnownOptionParserState extends OptionParserState {
406 private final OptionString optionString;
407
408 private final CommandLineOption option;
409
410 private final ParsedCommandLine commandLine;
411
412 private final ParserState state;
413
414 private final List<String> values = new ArrayList<>();
415
416 private KnownOptionParserState(
417 OptionString optionString, CommandLineOption option, ParsedCommandLine commandLine, ParserState state) {
418 this.optionString = optionString;
419 this.option = option;
420 this.commandLine = commandLine;
421 this.state = state;
422 }
423
424 @Override
425 public ParserState onArgument(String argument) {
426 if (!getHasArgument()) {
427 throw new CommandLineArgumentException(
428 String.format("Command-line option '%s' does not" + " take an argument.", optionString));
429 }
430 if (argument.length() == 0) {
431 throw new CommandLineArgumentException(String.format(
432 "An empty argument was provided" + " for command-line option '%s'.", optionString));
433 }
434 values.add(argument);
435 return onComplete();
436 }
437
438 @Override
439 public ParserState onStartNextArg() {
440 if (option.getAllowsArguments() && values.isEmpty()) {
441 return new MissingOptionArgState(this);
442 }
443 return onComplete();
444 }
445
446 @Override
447 public boolean getHasArgument() {
448 return option.getAllowsArguments();
449 }
450
451 @Override
452 public ParserState onComplete() {
453 if (getHasArgument() && values.isEmpty()) {
454 throw new CommandLineArgumentException(
455 String.format("No argument was provided" + " for command-line option '%s'.", optionString));
456 }
457
458 ParsedCommandLineOption parsedOption = commandLine.addOption(optionString.option, option);
459 if (values.size() + parsedOption.getValues().size() > 1 && !option.getAllowsMultipleArguments()) {
460 throw new CommandLineArgumentException(String.format(
461 "Multiple arguments were provided" + " for command-line option '%s'.", optionString));
462 }
463 for (String value : values) {
464 parsedOption.addArgument(value);
465 }
466 if (option.getDeprecationWarning() != null) {
467 deprecationPrinter.println(
468 "The " + optionString + " option is deprecated - " + option.getDeprecationWarning());
469 }
470 if (option.getSubcommand() != null) {
471 return state.onNonOption(option.getSubcommand());
472 }
473
474 return state;
475 }
476 }
477
478 private static final class UnknownOptionParserState extends OptionParserState {
479 private final ParserState state;
480
481 private final String arg;
482
483 private final ParsedCommandLine commandLine;
484
485 private UnknownOptionParserState(String arg, ParsedCommandLine commandLine, ParserState state) {
486 this.arg = arg;
487 this.commandLine = commandLine;
488 this.state = state;
489 }
490
491 @Override
492 public boolean getHasArgument() {
493 return true;
494 }
495
496 @Override
497 public ParserState onStartNextArg() {
498 return onComplete();
499 }
500
501 @Override
502 public ParserState onArgument(String argument) {
503 return onComplete();
504 }
505
506 @Override
507 public ParserState onComplete() {
508 commandLine.addExtraValue(arg);
509 return state;
510 }
511 }
512
513 private static final class OptionComparator implements Comparator<CommandLineOption> {
514 public int compare(CommandLineOption option1, CommandLineOption option2) {
515 String min1 = Collections.min(option1.getOptions(), new OptionStringComparator());
516 String min2 = Collections.min(option2.getOptions(), new OptionStringComparator());
517 return new CaseInsensitiveStringComparator().compare(min1, min2);
518 }
519 }
520
521 private static final class CaseInsensitiveStringComparator implements Comparator<String> {
522 public int compare(String option1, String option2) {
523 int diff = option1.compareToIgnoreCase(option2);
524 if (diff != 0) {
525 return diff;
526 }
527 return option1.compareTo(option2);
528 }
529 }
530
531 private static final class OptionStringComparator implements Comparator<String> {
532 public int compare(String option1, String option2) {
533 boolean short1 = option1.length() == 1;
534 boolean short2 = option2.length() == 1;
535 if (short1 && !short2) {
536 return -1;
537 }
538 if (!short1 && short2) {
539 return 1;
540 }
541 return new CaseInsensitiveStringComparator().compare(option1, option2);
542 }
543 }
544 }