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