View Javadoc
1   /*
2    *    Copyright 2009-2022 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package jargs.gnu;
17  
18  import java.io.Serializable;
19  import java.text.NumberFormat;
20  import java.text.ParseException;
21  import java.util.ArrayList;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Locale;
25  
26  /**
27   * Largely GNU-compatible command-line options parser. Has short (-v) and long-form (--verbose) option support, and also
28   * allows options with associated values (-d 2, --debug 2, --debug=2). Option processing can be explicitly terminated by
29   * the argument '--'.
30   *
31   * @author Steve Purcell
32   *
33   * @version $Revision$
34   *
35   * @see <a href="https://github.com/purcell/jargs/tree/master/src/examples/java/com/sanityinc/jargs/examples">jargs
36   *      examples</a>
37   */
38  public class CmdLineParser {
39  
40      /** The remaining args. */
41      private List<String> remainingArgs;
42  
43      /** The options. */
44      private HashMap<Object, Option> options = new HashMap<>(10);
45  
46      /** The values. */
47      private HashMap<String, Object> values = new HashMap<>(10);
48  
49      /**
50       * Base class for exceptions that may be thrown when options are parsed.
51       */
52      public abstract static class OptionException extends Exception {
53  
54          /** The Constant serialVersionUID. */
55          private static final long serialVersionUID = 1L;
56  
57          /**
58           * Instantiates a new option exception.
59           *
60           * @param msg
61           *            the msg
62           */
63          OptionException(String msg) {
64              super(msg);
65          }
66      }
67  
68      /**
69       * Thrown when the parsed command-line contains an option that is not recognised. <code>getMessage()</code> returns
70       * an error string suitable for reporting the error to the user (in English).
71       */
72      public static class UnknownOptionException extends OptionException {
73  
74          /** The Constant serialVersionUID. */
75          private static final long serialVersionUID = 1L;
76  
77          /** The option name. */
78          private final String optionName;
79  
80          /**
81           * Instantiates a new unknown option exception.
82           *
83           * @param optionName
84           *            the option name
85           */
86          UnknownOptionException(String optionName) {
87              this(optionName, "Unknown option '" + optionName + "'");
88          }
89  
90          /**
91           * Instantiates a new unknown option exception.
92           *
93           * @param optionName
94           *            the option name
95           * @param msg
96           *            the msg
97           */
98          UnknownOptionException(String optionName, String msg) {
99              super(msg);
100             this.optionName = optionName;
101         }
102 
103         /**
104          * Gets the option name.
105          *
106          * @return the name of the option that was unknown (e.g. "-u")
107          */
108         public String getOptionName() {
109             return this.optionName;
110         }
111     }
112 
113     /**
114      * Thrown when the parsed commandline contains multiple concatenated short options, such as -abcd, where one is
115      * unknown. <code>getMessage()</code> returns an english human-readable error string.
116      *
117      * @author Vidar Holen
118      */
119     public static class UnknownSuboptionException extends UnknownOptionException {
120 
121         /** The Constant serialVersionUID. */
122         private static final long serialVersionUID = 1L;
123 
124         /** The suboption. */
125         private final char suboption;
126 
127         /**
128          * Instantiates a new unknown suboption exception.
129          *
130          * @param option
131          *            the option
132          * @param suboption
133          *            the suboption
134          */
135         UnknownSuboptionException(String option, char suboption) {
136             super(option, "Illegal option: '" + suboption + "' in '" + option + "'");
137             this.suboption = suboption;
138         }
139 
140         /**
141          * Gets the suboption.
142          *
143          * @return the suboption
144          */
145         public char getSuboption() {
146             return suboption;
147         }
148     }
149 
150     /**
151      * Thrown when the parsed commandline contains multiple concatenated short options, such as -abcd, where one or more
152      * requires a value. <code>getMessage()</code> returns an english human-readable error string.
153      *
154      * @author Vidar Holen
155      */
156     public static class NotFlagException extends UnknownOptionException {
157 
158         /** The Constant serialVersionUID. */
159         private static final long serialVersionUID = 1L;
160 
161         /** The notflag. */
162         private final char notflag;
163 
164         /**
165          * Instantiates a new not flag exception.
166          *
167          * @param option
168          *            the option
169          * @param unflaggish
170          *            the unflaggish
171          */
172         NotFlagException(String option, char unflaggish) {
173             super(option, "Illegal option: '" + option + "', '" + unflaggish + "' requires a value");
174             notflag = unflaggish;
175         }
176 
177         /**
178          * Gets the option char.
179          *
180          * @return the first character which wasn't a boolean (e.g 'c')
181          */
182         public char getOptionChar() {
183             return notflag;
184         }
185     }
186 
187     /**
188      * Thrown when an illegal or missing value is given by the user for an option that takes a value.
189      * <code>getMessage()</code> returns an error string suitable for reporting the error to the user (in English).
190      */
191     public static class IllegalOptionValueException extends OptionException {
192 
193         /** The Constant serialVersionUID. */
194         private static final long serialVersionUID = 1L;
195 
196         /** The option. */
197         private final Option option;
198 
199         /** The value. */
200         private final String value;
201 
202         /**
203          * Instantiates a new illegal option value exception.
204          *
205          * @param opt
206          *            the opt
207          * @param value
208          *            the value
209          */
210         public IllegalOptionValueException(Option opt, String value) {
211             super("Illegal value '" + value + "' for option "
212                     + (opt.shortForm() != null ? "-" + opt.shortForm() + "/" : "") + "--" + opt.longForm());
213             this.option = opt;
214             this.value = value;
215         }
216 
217         /**
218          * Gets the option.
219          *
220          * @return the name of the option whose value was illegal (e.g. "-u")
221          */
222         public Option getOption() {
223             return this.option;
224         }
225 
226         /**
227          * Gets the value.
228          *
229          * @return the illegal value
230          */
231         public String getValue() {
232             return this.value;
233         }
234 
235     }
236 
237     /**
238      * Representation of a command-line option.
239      */
240     public abstract static class Option implements Serializable {
241 
242         /** The Constant serialVersionUID. */
243         private static final long serialVersionUID = 1L;
244 
245         /** The short form. */
246         private String shortForm;
247 
248         /** The long form. */
249         private String longForm;
250 
251         /** The wants value. */
252         private boolean wantsValue;
253 
254         /**
255          * Instantiates a new option.
256          *
257          * @param longForm
258          *            the long form
259          * @param wantsValue
260          *            the wants value
261          */
262         protected Option(String longForm, boolean wantsValue) {
263             this(null, longForm, wantsValue);
264         }
265 
266         /**
267          * Instantiates a new option.
268          *
269          * @param shortForm
270          *            the short form
271          * @param longForm
272          *            the long form
273          * @param wantsValue
274          *            the wants value
275          */
276         protected Option(char shortForm, String longForm, boolean wantsValue) {
277             this(new String(new char[] { shortForm }), longForm, wantsValue);
278         }
279 
280         /**
281          * Instantiates a new option.
282          *
283          * @param shortForm
284          *            the short form
285          * @param longForm
286          *            the long form
287          * @param wantsValue
288          *            the wants value
289          */
290         private Option(String shortForm, String longForm, boolean wantsValue) {
291             if (longForm == null)
292                 throw new IllegalArgumentException("Null longForm not allowed");
293             this.shortForm = shortForm;
294             this.longForm = longForm;
295             this.wantsValue = wantsValue;
296         }
297 
298         /**
299          * Short form.
300          *
301          * @return the string
302          */
303         public String shortForm() {
304             return this.shortForm;
305         }
306 
307         /**
308          * Long form.
309          *
310          * @return the string
311          */
312         public String longForm() {
313             return this.longForm;
314         }
315 
316         /**
317          * Tells whether or not this option wants a value.
318          *
319          * @return true, if successful
320          */
321         public boolean wantsValue() {
322             return this.wantsValue;
323         }
324 
325         /**
326          * Gets the value.
327          *
328          * @param arg
329          *            the arg
330          * @param locale
331          *            the locale
332          *
333          * @return the value
334          *
335          * @throws IllegalOptionValueException
336          *             the illegal option value exception
337          */
338         public final Object getValue(String arg, Locale locale) throws IllegalOptionValueException {
339             if (this.wantsValue) {
340                 if (arg == null) {
341                     throw new IllegalOptionValueException(this, "");
342                 }
343                 return this.parseValue(arg, locale);
344             } else {
345                 return Boolean.TRUE;
346             }
347         }
348 
349         /**
350          * Override to extract and convert an option value passed on the command-line.
351          *
352          * @param arg
353          *            the arg
354          * @param locale
355          *            the locale
356          *
357          * @return the object
358          *
359          * @throws IllegalOptionValueException
360          *             the illegal option value exception
361          */
362         protected Object parseValue(String arg, Locale locale) throws IllegalOptionValueException {
363             return null;
364         }
365 
366         /**
367          * The Class BooleanOption.
368          */
369         public static class BooleanOption extends Option {
370 
371             /** The Constant serialVersionUID. */
372             private static final long serialVersionUID = 1L;
373 
374             /**
375              * Instantiates a new boolean option.
376              *
377              * @param shortForm
378              *            the short form
379              * @param longForm
380              *            the long form
381              */
382             public BooleanOption(char shortForm, String longForm) {
383                 super(shortForm, longForm, false);
384             }
385 
386             /**
387              * Instantiates a new boolean option.
388              *
389              * @param longForm
390              *            the long form
391              */
392             public BooleanOption(String longForm) {
393                 super(longForm, false);
394             }
395         }
396 
397         /**
398          * An option that expects an integer value.
399          */
400         public static class IntegerOption extends Option {
401 
402             /** The Constant serialVersionUID. */
403             private static final long serialVersionUID = 1L;
404 
405             /**
406              * Instantiates a new integer option.
407              *
408              * @param shortForm
409              *            the short form
410              * @param longForm
411              *            the long form
412              */
413             public IntegerOption(char shortForm, String longForm) {
414                 super(shortForm, longForm, true);
415             }
416 
417             /**
418              * Instantiates a new integer option.
419              *
420              * @param longForm
421              *            the long form
422              */
423             public IntegerOption(String longForm) {
424                 super(longForm, true);
425             }
426 
427             @Override
428             protected Object parseValue(String arg, Locale locale) throws IllegalOptionValueException {
429                 try {
430                     return Integer.valueOf(arg);
431                 } catch (NumberFormatException e) {
432                     throw new IllegalOptionValueException(this, arg);
433                 }
434             }
435         }
436 
437         /**
438          * An option that expects a long integer value.
439          */
440         public static class LongOption extends Option {
441 
442             /** The Constant serialVersionUID. */
443             private static final long serialVersionUID = 1L;
444 
445             /**
446              * Instantiates a new long option.
447              *
448              * @param shortForm
449              *            the short form
450              * @param longForm
451              *            the long form
452              */
453             public LongOption(char shortForm, String longForm) {
454                 super(shortForm, longForm, true);
455             }
456 
457             /**
458              * Instantiates a new long option.
459              *
460              * @param longForm
461              *            the long form
462              */
463             public LongOption(String longForm) {
464                 super(longForm, true);
465             }
466 
467             @Override
468             protected Object parseValue(String arg, Locale locale) throws IllegalOptionValueException {
469                 try {
470                     return Long.valueOf(arg);
471                 } catch (NumberFormatException e) {
472                     throw new IllegalOptionValueException(this, arg);
473                 }
474             }
475         }
476 
477         /**
478          * An option that expects a floating-point value.
479          */
480         public static class DoubleOption extends Option {
481 
482             /** The Constant serialVersionUID. */
483             private static final long serialVersionUID = 1L;
484 
485             /**
486              * Instantiates a new double option.
487              *
488              * @param shortForm
489              *            the short form
490              * @param longForm
491              *            the long form
492              */
493             public DoubleOption(char shortForm, String longForm) {
494                 super(shortForm, longForm, true);
495             }
496 
497             /**
498              * Instantiates a new double option.
499              *
500              * @param longForm
501              *            the long form
502              */
503             public DoubleOption(String longForm) {
504                 super(longForm, true);
505             }
506 
507             @Override
508             protected Object parseValue(String arg, Locale locale) throws IllegalOptionValueException {
509                 try {
510                     NumberFormat format = NumberFormat.getNumberInstance(locale);
511                     Number num = format.parse(arg);
512                     return num.doubleValue();
513                 } catch (ParseException e) {
514                     throw new IllegalOptionValueException(this, arg);
515                 }
516             }
517         }
518 
519         /**
520          * An option that expects a string value.
521          */
522         public static class StringOption extends Option {
523 
524             /** The Constant serialVersionUID. */
525             private static final long serialVersionUID = 1L;
526 
527             /**
528              * Instantiates a new string option.
529              *
530              * @param shortForm
531              *            the short form
532              * @param longForm
533              *            the long form
534              */
535             public StringOption(char shortForm, String longForm) {
536                 super(shortForm, longForm, true);
537             }
538 
539             /**
540              * Instantiates a new string option.
541              *
542              * @param longForm
543              *            the long form
544              */
545             public StringOption(String longForm) {
546                 super(longForm, true);
547             }
548 
549             @Override
550             protected Object parseValue(String arg, Locale locale) {
551                 return arg;
552             }
553         }
554     }
555 
556     /**
557      * Add the specified Option to the list of accepted options.
558      *
559      * @param opt
560      *            the opt
561      *
562      * @return the option
563      */
564     public final Option addOption(Option opt) {
565         if (opt.shortForm() != null) {
566             this.options.put("-" + opt.shortForm(), opt);
567         }
568         this.options.put("--" + opt.longForm(), opt);
569         return opt;
570     }
571 
572     /**
573      * Convenience method for adding a string option.
574      *
575      * @param shortForm
576      *            the short form
577      * @param longForm
578      *            the long form
579      *
580      * @return the new Option
581      */
582     public final Option addStringOption(char shortForm, String longForm) {
583         return addOption(new Option.StringOption(shortForm, longForm));
584     }
585 
586     /**
587      * Convenience method for adding a string option.
588      *
589      * @param longForm
590      *            the long form
591      *
592      * @return the new Option
593      */
594     public final Option addStringOption(String longForm) {
595         return addOption(new Option.StringOption(longForm));
596     }
597 
598     /**
599      * Convenience method for adding an integer option.
600      *
601      * @param shortForm
602      *            the short form
603      * @param longForm
604      *            the long form
605      *
606      * @return the new Option
607      */
608     public final Option addIntegerOption(char shortForm, String longForm) {
609         return addOption(new Option.IntegerOption(shortForm, longForm));
610     }
611 
612     /**
613      * Convenience method for adding an integer option.
614      *
615      * @param longForm
616      *            the long form
617      *
618      * @return the new Option
619      */
620     public final Option addIntegerOption(String longForm) {
621         return addOption(new Option.IntegerOption(longForm));
622     }
623 
624     /**
625      * Convenience method for adding a long integer option.
626      *
627      * @param shortForm
628      *            the short form
629      * @param longForm
630      *            the long form
631      *
632      * @return the new Option
633      */
634     public final Option addLongOption(char shortForm, String longForm) {
635         return addOption(new Option.LongOption(shortForm, longForm));
636     }
637 
638     /**
639      * Convenience method for adding a long integer option.
640      *
641      * @param longForm
642      *            the long form
643      *
644      * @return the new Option
645      */
646     public final Option addLongOption(String longForm) {
647         return addOption(new Option.LongOption(longForm));
648     }
649 
650     /**
651      * Convenience method for adding a double option.
652      *
653      * @param shortForm
654      *            the short form
655      * @param longForm
656      *            the long form
657      *
658      * @return the new Option
659      */
660     public final Option addDoubleOption(char shortForm, String longForm) {
661         return addOption(new Option.DoubleOption(shortForm, longForm));
662     }
663 
664     /**
665      * Convenience method for adding a double option.
666      *
667      * @param longForm
668      *            the long form
669      *
670      * @return the new Option
671      */
672     public final Option addDoubleOption(String longForm) {
673         return addOption(new Option.DoubleOption(longForm));
674     }
675 
676     /**
677      * Convenience method for adding a boolean option.
678      *
679      * @param shortForm
680      *            the short form
681      * @param longForm
682      *            the long form
683      *
684      * @return the new Option
685      */
686     public final Option addBooleanOption(char shortForm, String longForm) {
687         return addOption(new Option.BooleanOption(shortForm, longForm));
688     }
689 
690     /**
691      * Convenience method for adding a boolean option.
692      *
693      * @param longForm
694      *            the long form
695      *
696      * @return the new Option
697      */
698     public final Option addBooleanOption(String longForm) {
699         return addOption(new Option.BooleanOption(longForm));
700     }
701 
702     /**
703      * Equivalent to {@link #getOptionValue(Option, Object) getOptionValue(o, null)}.
704      *
705      * @param o
706      *            the o
707      *
708      * @return the option value
709      */
710     public final Object getOptionValue(Option o) {
711         return getOptionValue(o, null);
712     }
713 
714     /**
715      * Gets the option value.
716      *
717      * @param o
718      *            the o
719      * @param def
720      *            the def
721      *
722      * @return the parsed value of the given Option, or the given default 'def' if the option was not set
723      */
724     @SuppressWarnings("unchecked")
725     public final Object getOptionValue(Option o, Object def) {
726         List<String> v = (ArrayList<String>) values.get(o.longForm());
727 
728         if (v == null) {
729             return def;
730         } else if (v.isEmpty()) {
731             return null;
732         } else {
733             Object result = v.get(0);
734             v.remove(0);
735             return result;
736         }
737     }
738 
739     /**
740      * Gets the option values.
741      *
742      * @param option
743      *            the option
744      *
745      * @return A Vector giving the parsed values of all the occurrences of the given Option, or an empty Vector if the
746      *         option was not set.
747      */
748     public final List<String> getOptionValues(Option option) {
749         List<String> result = new ArrayList<>();
750 
751         while (true) {
752             Object o = getOptionValue(option, null);
753 
754             if (o == null) {
755                 return result;
756             } else {
757                 result.add((String) o);
758             }
759         }
760     }
761 
762     /**
763      * Gets the remaining args.
764      *
765      * @return the non-option arguments
766      */
767     public final List<String> getRemainingArgs() {
768         return this.remainingArgs;
769     }
770 
771     /**
772      * Extract the options and non-option arguments from the given list of command-line arguments. The default locale is
773      * used for parsing options whose values might be locale-specific.
774      *
775      * @param argv
776      *            the argv
777      *
778      * @throws IllegalOptionValueException
779      *             the illegal option value exception
780      * @throws UnknownOptionException
781      *             the unknown option exception
782      */
783     public final void parse(String[] argv) throws IllegalOptionValueException, UnknownOptionException {
784 
785         // It would be best if this method only threw OptionException, but for
786         // backwards compatibility with old user code we throw the two
787         // exceptions above instead.
788 
789         parse(argv, Locale.getDefault());
790     }
791 
792     /**
793      * Extract the options and non-option arguments from the given list of command-line arguments. The specified locale
794      * is used for parsing options whose values might be locale-specific.
795      *
796      * @param argv
797      *            the argv
798      * @param locale
799      *            the locale
800      *
801      * @throws IllegalOptionValueException
802      *             the illegal option value exception
803      * @throws UnknownOptionException
804      *             the unknown option exception
805      */
806     public final void parse(String[] argv, Locale locale) throws IllegalOptionValueException, UnknownOptionException {
807 
808         // It would be best if this method only threw OptionException, but for
809         // backwards compatibility with old user code we throw the two
810         // exceptions above instead.
811 
812         List<String> otherArgs = new ArrayList<>();
813         int position = 0;
814         this.values = new HashMap<>(10);
815         while (position < argv.length) {
816             String curArg = argv[position];
817             if (curArg.startsWith("-")) {
818                 if ("--".equals(curArg)) {
819                     // end of options
820                     position += 1;
821                     break;
822                 }
823                 String valueArg = null;
824                 if (curArg.startsWith("--")) {
825                     // handle --arg=value
826                     int equalsPos = curArg.indexOf('=');
827                     if (equalsPos != -1) {
828                         valueArg = curArg.substring(equalsPos + 1);
829                         curArg = curArg.substring(0, equalsPos);
830                     }
831                 } else if (curArg.length() > 2) {
832                     // handle -abcd
833                     for (int i = 1; i < curArg.length(); i++) {
834                         Option opt = this.options.get("-" + curArg.charAt(i));
835                         if (opt == null)
836                             throw new UnknownSuboptionException(curArg, curArg.charAt(i));
837                         if (opt.wantsValue())
838                             throw new NotFlagException(curArg, curArg.charAt(i));
839                         addValue(opt, opt.getValue(null, locale));
840 
841                     }
842                     position++;
843                     continue;
844                 }
845 
846                 Option opt = this.options.get(curArg);
847                 if (opt == null) {
848                     throw new UnknownOptionException(curArg);
849                 }
850                 Object value;
851                 if (opt.wantsValue()) {
852                     if (valueArg == null) {
853                         position += 1;
854                         if (position < argv.length) {
855                             valueArg = argv[position];
856                         }
857                     }
858                     value = opt.getValue(valueArg, locale);
859                 } else {
860                     value = opt.getValue(null, locale);
861                 }
862 
863                 addValue(opt, value);
864 
865                 position += 1;
866             } else {
867                 otherArgs.add(curArg);
868                 position += 1;
869             }
870         }
871         for (; position < argv.length; ++position) {
872             otherArgs.add(argv[position]);
873         }
874 
875         this.remainingArgs = new ArrayList<>(otherArgs.size());
876         otherArgs.addAll(remainingArgs);
877     }
878 
879     /**
880      * Adds the value.
881      *
882      * @param opt
883      *            the opt
884      * @param value
885      *            the value
886      */
887     @SuppressWarnings("unchecked")
888     private void addValue(Option opt, Object value) {
889         String lf = opt.longForm();
890 
891         List<Object> v = (ArrayList<Object>) values.get(lf);
892 
893         if (v == null) {
894             v = new ArrayList<>();
895             values.put(lf, v);
896         }
897 
898         v.add(value);
899     }
900 
901 }