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