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 }