1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License (the "License"). 6 * You may not use this file except in compliance with the License. 7 * 8 * See LICENSE.txt included in this distribution for the specific 9 * language governing permissions and limitations under the License. 10 * 11 * When distributing Covered Code, include this CDDL HEADER in each 12 * file and include the License file at LICENSE.txt. 13 * If applicable, add the following below this CDDL HEADER, with the 14 * fields enclosed by brackets "[]" replaced with your own identifying 15 * information: Portions Copyright [yyyy] [name of copyright owner] 16 * 17 * CDDL HEADER END 18 */ 19 20 /* 21 * Portions Copyright (c) 2017, Steven Haehn. 22 * Portions Copyright (c) 2019, Chris Fraire <cfraire@me.com>. 23 */ 24 package org.opengrok.indexer.util; 25 26 import java.io.PrintWriter; 27 import java.io.StringWriter; 28 import java.io.PrintStream; 29 import java.text.ParseException; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.function.Consumer; 33 import java.util.function.Function; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.HashMap; 37 import java.util.regex.Matcher; 38 import java.util.regex.Pattern; 39 40 /** 41 * OptionParser is a class for command-line option analysis. 42 * 43 * Now that Java 8 has the crucial Lambda and Consumer interfaces, we can 44 * implement a more powerful program option parsing mechanism, ala ruby 45 * OptionParser style. 46 * 47 * Features 48 * o An option can have multiple short names (-x) and multiple long 49 * names (--xyz). Thus, an option that displays a programs usage 50 * may be available as -h, -?, --help, --usage, and --about. 51 * 52 * o An option may be specified as having no argument, an optional 53 * argument, or a required argument. Arguments may be validated 54 * against a regular expression pattern or a list of valid values. 55 * 56 * o The argument specification and the code to handle it are 57 * written in the same place. The argument description may consist 58 * of one or more lines to be used when displaying usage summary. 59 * 60 * o The option summary is produced without maintaining strings 61 * in a separate setting. 62 * 63 * o Users are allowed to enter initial substrings for long option 64 * names as long as there is no ambiguity. 65 * 66 * o Supports the ability to coerce command line arguments into objects. 67 * This class readily supports Boolean (yes/no,true/false,on/off), 68 * Float, Double, Integer, and String[] (strings separated by comma) 69 * objects. The programmer may define additional coercions of their own. 70 * 71 * @author Steven Haehn 72 */ 73 public class OptionParser { 74 75 // Used to hold data type converters 76 private static final Map<Class<?>, DataParser> converters = new HashMap<>(); 77 78 static class DataParser { 79 Class<?> dataType; 80 Function<String, Object> converter; 81 DataParser(Class<?> cls, Function<String, Object> converter)82 DataParser(Class<?> cls, Function<String, Object> converter) { 83 this.dataType = cls; 84 this.converter = converter; 85 } 86 } 87 88 // Supported internal data type converters. 89 static { accept(Integer.class, Integer::parseInt)90 accept(Integer.class, Integer::parseInt); accept(Boolean.class, OptionParser::parseVerity)91 accept(Boolean.class, OptionParser::parseVerity); accept(Float.class, Float::parseFloat)92 accept(Float.class, Float::parseFloat); accept(Double.class, Double::parseDouble)93 accept(Double.class, Double::parseDouble); accept(String[].class, s -> s.split(","))94 accept(String[].class, s -> s.split(",")); 95 } 96 97 // Option object referenced by its name(s) 98 private final Map<String, Option> options; 99 100 // List of options in order of declaration 101 private final List<Option> optionList; 102 103 // Keeps track of separator elements placed in option summary 104 private final List<Object> usageSummary; 105 106 private boolean scanning = false; 107 108 private String prologue; // text emitted before option summary 109 private String epilogue; // text emitted after options summary 110 111 public class Option { 112 113 List<String> names; // option names/aliases 114 String argument; // argument name for summary 115 String value; // user entered value for option 116 Class<?> valueType; // eg. Integer.class, other than String 117 Pattern valuePattern; // pattern used to accept value 118 List<String> allowedValues; // list of restricted values 119 Boolean mandatory; // true/false when argument present 120 StringBuilder description; // option description for summary 121 Consumer<Object> action; // code to execute when option encountered 122 Option()123 public Option() { 124 names = new ArrayList<>(); 125 } 126 addOption(String option, String arg)127 void addOption(String option, String arg) throws IllegalArgumentException { 128 addAlias(option); 129 setArgument(arg); 130 } 131 addAlias(String alias)132 void addAlias(String alias) throws IllegalArgumentException { 133 names.add(alias); 134 135 if (options.containsKey(alias)) { 136 throw new IllegalArgumentException("** Programmer error! Option " + alias + " already defined"); 137 } 138 139 options.put(alias, this); 140 } 141 setAllowedValues(String[] allowed)142 void setAllowedValues(String[] allowed) { 143 allowedValues = Arrays.asList(allowed); 144 } 145 setValueType(Class<?> type)146 void setValueType(Class<?> type) { 147 valueType = type; 148 } 149 setArgument(String arg)150 void setArgument(String arg) { 151 argument = arg.trim(); 152 mandatory = !argument.startsWith("["); 153 } 154 setPattern(String pattern)155 void setPattern(String pattern) { 156 valuePattern = Pattern.compile(pattern); 157 } 158 159 public static final int MAX_DESCRIPTION_LINE_LENGTH = 80; 160 addDescription(String description)161 void addDescription(String description) { 162 if (description.length() > MAX_DESCRIPTION_LINE_LENGTH) { 163 throw new IllegalArgumentException(String.format("description line longer than %d characters: '%s'", 164 MAX_DESCRIPTION_LINE_LENGTH, description)); 165 } 166 167 if (this.description == null) { 168 this.description = new StringBuilder(); 169 } 170 this.description.append(description); 171 this.description.append("\n"); 172 } 173 174 /** 175 * Code to be activated when option encountered. 176 * 177 * @param action is the code that will be called when the 178 * parser encounters the associated named option in its 179 * argument list. 180 */ execute(Consumer<Object> action)181 public void execute(Consumer<Object> action) { 182 this.action = action; 183 } 184 getUsage()185 String getUsage() { 186 StringBuilder line = new StringBuilder(); 187 String separator = ""; 188 for (String name : names) { 189 line.append(separator); 190 line.append(name); 191 separator = ", "; 192 } 193 194 if (argument != null) { 195 line.append(' '); 196 line.append(argument); 197 } 198 line.append("\n"); 199 if (description != null) { 200 line.append("\t"); 201 line.append(description.toString().replaceAll("\\n", "\n\t")); 202 } 203 204 return line.toString(); 205 } 206 } 207 208 /** 209 * Instantiate a new option parser 210 * 211 * This allows the programmer to create an empty option parser that can 212 * be added to incrementally elsewhere in the program being built. For 213 * example: 214 * 215 * OptionParser parser = OptionParser(); 216 * . 217 * . 218 * parser.prologue = "Usage: program [options] [file [...]] 219 * 220 * parser.on("-?", "--help", "Display this usage.").execute( v -> { 221 * parser.help(); 222 * }); 223 */ OptionParser()224 public OptionParser() { 225 optionList = new ArrayList<>(); 226 options = new HashMap<>(); 227 usageSummary = new ArrayList<>(); 228 } 229 230 // Allowable text values for Boolean.class, with case insensitivity. 231 private static final Pattern VERITY = Pattern.compile("(?i)(true|yes|on)"); 232 private static final Pattern FALSEHOOD = Pattern.compile("(?i)(false|no|off)"); 233 parseVerity(String text)234 private static Boolean parseVerity(String text) { 235 Matcher m = VERITY.matcher(text); 236 boolean veracity; 237 238 if (m.matches()) { 239 veracity = true; 240 } else { 241 m = FALSEHOOD.matcher(text); 242 if (m.matches()) { 243 veracity = false; 244 } else { 245 throw new IllegalArgumentException(); 246 } 247 } 248 return veracity; 249 } 250 251 /** 252 * Supply parser with data conversion mechanism for option value. 253 * The following is an example usage used internally: 254 * 255 * accept(Integer.class, s -> { return Integer.parseInt(s); }); 256 * 257 * @param type is the internal data class to which an option 258 * value should be converted. 259 * 260 * @param parser is the conversion code that will take the given 261 * option value string and produce the named data type. 262 */ accept(Class<?> type, Function<String, Object> parser)263 public static void accept(Class<?> type, Function<String, Object> parser) { 264 converters.put(type, new DataParser(type, parser)); 265 } 266 267 /** 268 * Instantiate a new options parser and construct option actionable components. 269 * 270 * As an example: 271 * 272 * <code> 273 * OptionParser opts = OptionParser.execute(parser -> { 274 * 275 * parser.prologue = 276 * String.format("\nUsage: %s [options] [subDir1 [...]]\n", program); 277 * 278 * parser.on("-?", "--help", "Display this usage.").execute( v -> { 279 * parser.help(); 280 * }); 281 * 282 * parser.epilogue = "That's all folks!"; 283 * } 284 * </code> 285 * 286 * @param parser consumer 287 * @return OptionParser object 288 */ execute(Consumer<OptionParser> parser)289 public static OptionParser execute(Consumer<OptionParser> parser) { 290 OptionParser me = new OptionParser(); 291 parser.accept(me); 292 return me; 293 } 294 295 /** 296 * Provide a 'scanning' option parser. 297 * 298 * This type of parser only operates on the arguments for which it 299 * is constructed. All other arguments passed to it are ignored. 300 * That is, it won't raise any errors for unrecognizable input as 301 * the normal option parser would. 302 * 303 * @param parser consumer 304 * @return OptionParser object 305 */ scan(Consumer<OptionParser> parser)306 public static OptionParser scan(Consumer<OptionParser> parser) { 307 OptionParser me = new OptionParser(); 308 parser.accept(me); 309 me.scanning = true; 310 return me; 311 } 312 313 /** 314 * Construct option recognition and description object 315 * 316 * This method is used to build the option object which holds 317 * its recognition and validation criteria, description and 318 * ultimately its data handler. 319 * 320 * The 'on' parameters consist of formatted strings which provide the 321 * option names, whether or not the option takes on a value and, 322 * if so, the option value type (mandatory/optional). The data type 323 * of the option value may also be provided to allow the parser to 324 * handle conversion from a string to an internally supported data type. 325 * 326 * Other parameters which may be provided are: 327 * 328 * o String array of legal option values (eg. {"on","off"}) 329 * o Regular expression pattern that option value must match 330 * o Multiple line description for the option. 331 * 332 * There are two forms of option names, short and long. The short 333 * option names are a single character in length and are recognized 334 * with a single "-" character to the left of the name (eg. -o). 335 * The long option names hold more than a single character and are 336 * recognized via "--" to the left of the name (eg. --option). The 337 * syntax for specifying an option, whether it takes on a value or 338 * not, and whether that value is mandatory or optional is as follows. 339 * (Note, the use of OPT is an abbreviation for OPTIONAL.) 340 * 341 * Short name 'x': 342 * -x, -xVALUE, -x=VALUE, -x[OPT], -x[=OPT], -x PLACE 343 * 344 * The option has the short name 'x'. The first form has no value. 345 * the next two require values, the next two indicate that the value 346 * is optional (delineated by the '[' character). The last form 347 * indicates that the option must have a value, but that it follows 348 * the option indicator. 349 * 350 * Long name 'switch': 351 * --switch, --switch=VALUE, --switch=[OPT], --switch PLACE 352 * 353 * The option has the long name 'switch'. The first form indicates 354 * it does not require a value, The second form indicates that the 355 * option requires a value. The third form indicates that option may 356 * or may not have value. The last form indicates that a value is 357 * required, but that it follows the option indicator. 358 * 359 * Since an option may have multiple names (aliases), there is a 360 * short hand for describing those which take on a value. 361 * 362 * Option value shorthand: =VALUE, =[OPT] 363 * 364 * The first form indicates that the option value is required, the 365 * second form indicates that the value is optional. For example 366 * the following code says there is an option known by the aliases 367 * -a, -b, and -c and that it needs a required value shown as N. 368 * 369 * <code> 370 * opt.on( "-a", "-b", "-c", "=N" ) 371 * </code> 372 * 373 * When an option takes on a value, 'on' may accept a regular expression 374 * indicating what kind of values are acceptable. The regular expression 375 * is indicated by surrounding the expression with '/' character. For 376 * example, "/pattern/" indicates that the only value acceptable is the 377 * word 'pattern'. 378 * 379 * Any string that does not start with a '-', '=', or '/' is used as a 380 * description for the option in the summary. Multiple descriptions may 381 * be given; they will be shown on additional lines. 382 * 383 * For programmers: If a switch starts with 3 dashes (---) it will 384 * be hidden from the usage summary and manual generation. It is meant 385 * for unit testing access. 386 * 387 * @param args arguments 388 * @return Option 389 */ on(Object... args)390 public Option on(Object... args) { 391 392 Option opt = new Option(); 393 394 // Once description starts, then no other option settings are eligible. 395 boolean addedDescription = false; 396 397 for (Object arg : args) { 398 if (arg instanceof String) { 399 String argument = (String) arg; 400 if (addedDescription) { 401 opt.addDescription(argument); 402 } else if (argument.startsWith("--")) { 403 // handle --switch --switch=ARG --switch=[OPT] --switch PLACE 404 String[] parts = argument.split("[ =]"); 405 406 if (parts.length == 1) { 407 opt.addAlias(parts[0]); 408 } else { 409 opt.addOption(parts[0], parts[1]); 410 } 411 } else if (argument.startsWith("-")) { 412 // handle -x -xARG -x=ARG -x[OPT] -x[=OPT] -x PLACE 413 String optName = argument.substring(0, 2); 414 String remainder = argument.substring(2); 415 opt.addOption(optName, remainder); 416 417 } else if (argument.startsWith("=")) { 418 opt.setArgument(argument.substring(1)); 419 } else if (argument.startsWith("/")) { 420 // regular expression (sans '/'s) 421 opt.setPattern(argument.substring(1, argument.length() - 1)); 422 } else { 423 // this is description 424 opt.addDescription(argument); 425 addedDescription = true; 426 } 427 // This is indicator for a addOption of specific allowable option values 428 } else if (arg instanceof String[]) { 429 opt.setAllowedValues((String[]) arg); 430 // This is indicator for option value data type 431 // to which the parser will take and convert. 432 } else if (arg instanceof Class) { 433 opt.setValueType((Class<?>) arg); 434 } else if (arg == null) { 435 throw new IllegalArgumentException("arg is null"); 436 } else { 437 throw new IllegalArgumentException("Invalid arg: " + 438 arg.getClass().getSimpleName() + " " + arg); 439 } 440 } 441 442 // options starting with 3 dashes are to be hidden from usage. 443 // (the idea here is to hide any unit test entries from general user) 444 if (!opt.names.get(0).startsWith("---")) { 445 optionList.add(opt); 446 usageSummary.add(opt); 447 } 448 449 return opt; 450 } 451 argValue(String arg, boolean mandatory)452 private String argValue(String arg, boolean mandatory) { 453 // Initially assume that the given argument is going 454 // to be the option's value. Note that if the argument 455 // is actually another option (starts with '-') then 456 // there is no value available. If the option is required 457 // to have a value, null is returned. If the option 458 // does not require a value, an empty string is returned. 459 String value = arg; 460 boolean isOption = value.startsWith("-"); 461 462 if (mandatory) { 463 if (isOption ) { 464 value = null; 465 } 466 } else if (isOption) { 467 value = ""; 468 } 469 return value; 470 } 471 getOption(String arg, int index)472 private String getOption(String arg, int index) throws ParseException { 473 String option = null; 474 475 if ( arg.equals("-")) { 476 throw new ParseException("Stand alone '-' found in arguments, not allowed", index); 477 } 478 479 if (arg.startsWith("-")) { 480 if (arg.startsWith("--")) { 481 option = arg; // long name option (--longOption) 482 } else if (arg.length() > 2) { 483 option = arg.substring(0, 2); // short name option (-xValue) 484 } else { 485 option = arg; // short name option (-x) 486 } 487 } 488 return option; 489 } 490 491 /** 492 * Discover full name of partial option name. 493 * 494 * @param option is the initial substring of a long option name. 495 * @param index into original argument list (only used by ParseException) 496 * @return full name of given option substring, or null when not found. 497 * @throws ParseException when more than one candidate name is found. 498 */ candidate(String option, int index)499 protected String candidate(String option, int index) throws ParseException { 500 boolean found = options.containsKey(option); 501 List<String> candidates = new ArrayList<>(); 502 String candidate = null; 503 504 if (found) { 505 candidate = option; 506 } else { 507 // Now check to see if initial substring was entered. 508 for (String key: options.keySet()) { 509 if (key.startsWith(option)) { 510 candidates.add(key); 511 } 512 } 513 if (candidates.size() == 1 ) { 514 candidate = candidates.get(0); 515 } else if (candidates.size() > 1) { 516 throw new ParseException( 517 "Ambiguous option " + option + " matches " + candidates, index); 518 } 519 } 520 return candidate; 521 } 522 523 /** 524 * Parse given set of arguments and activate handlers 525 * 526 * This code parses the given set of parameters looking for a described 527 * set of options and activates the code segments associated with the 528 * option. 529 * 530 * Parsing is discontinued when a lone "--" is encountered in the list of 531 * arguments. If this is a normal non-scan parser, unrecognized options 532 * will cause a parse exception. If this is a scan parser, unrecognized 533 * options are ignored. 534 * 535 * @param args argument vector 536 * @return non-option parameters, or all arguments after "--" encountered. 537 * @throws ParseException parse exception 538 */ 539 parse(String[] args)540 public String[] parse(String[] args) throws ParseException { 541 int ii = 0; 542 int optind = -1; 543 String option; 544 while (ii < args.length) { 545 option = getOption(args[ii], ii); 546 547 // When scanning for specific options... 548 if (scanning) { 549 if (option == null || (option = candidate(option, ii)) == null) { 550 optind = ++ii; // skip over everything else 551 continue; 552 } 553 } 554 555 if (option == null) { // no more options? we be done. 556 break; 557 } else if (option.equals("--")) { // parsing escape found? we be done. 558 optind = ii + 1; 559 break; 560 } else { 561 562 if ( !scanning ) { 563 String candidate = candidate(option, ii); 564 if (candidate != null) { 565 option = candidate; 566 } else { 567 throw new ParseException("Unknown option: " + option, ii); 568 } 569 } 570 Option opt = options.get(option); 571 opt.value = null; 572 573 if (option.length() == 2 && !option.equals(args[ii])) { // catches -xValue 574 opt.value = args[ii].substring(2); 575 } 576 577 // No argument required? 578 if (opt.argument == null || opt.argument.equals("")) { 579 if (opt.value != null) { 580 throw new ParseException("Option " + option + " does not use value.", ii); 581 } 582 opt.value = ""; 583 584 // Argument specified but value not yet acquired 585 } else if (opt.value == null) { 586 587 ii++; // next argument may hold argument value 588 589 // When option is last in list... 590 if (ii >= args.length) { 591 if (!opt.mandatory) { 592 opt.value = ""; // indicate this option's value was optional 593 } 594 } else { 595 596 // Look at next argument for value 597 opt.value = argValue(args[ii], opt.mandatory); 598 599 if (opt.value != null && opt.value.equals("")) { 600 // encountered another option so this 601 // option's value was not required. Backup 602 // argument list index to handle so loop 603 // can re-examine this option. 604 ii--; 605 } 606 } 607 } 608 609 // If there is no value setting for the 610 // option by now, throw a hissy fit. 611 if (opt.value == null) { 612 throw new ParseException("Option " + option + " requires a value.", ii); 613 } 614 615 // Only specific values allowed? 616 if (opt.allowedValues != null) { 617 if (!opt.allowedValues.contains(opt.value)) { 618 throw new ParseException( 619 "'" + opt.value + 620 "' is unknown value for option " + opt.names + 621 ". Must be one of " + opt.allowedValues, ii); 622 } 623 } 624 625 Object value = opt.value; 626 627 // Should option argument match some pattern? 628 if (opt.valuePattern != null) { 629 Matcher m = opt.valuePattern.matcher(opt.value); 630 if (!m.matches()) { 631 throw new ParseException( 632 "Value '" + opt.value + "' for option " + opt.names + opt.argument + 633 "\n does not match pattern " + opt.valuePattern, ii); 634 } 635 636 // Handle special conversions of input 637 // arguments before sending to action handler. 638 } else if (opt.valueType != null) { 639 640 if (!converters.containsKey(opt.valueType)) { 641 throw new ParseException( 642 "No conversion handler for data type " + opt.valueType, ii); 643 } 644 645 try { 646 DataParser data = converters.get(opt.valueType); 647 value = data.converter.apply(opt.value); 648 649 } catch (Exception e) { 650 System.err.println("** " + e.getMessage()); 651 throw new ParseException("Failed to parse (" + opt.value + ") as value of " + opt.names, ii); 652 } 653 } 654 655 if (opt.action != null) { 656 opt.action.accept(value); // 'do' assigned action 657 } 658 optind = ++ii; 659 } 660 } 661 662 // Prepare to gather any remaining arguments 663 // to send back to calling program. 664 665 String[] remainingArgs = null; 666 667 if (optind == -1) { 668 remainingArgs = args; 669 } else if (optind < args.length) { 670 remainingArgs = Arrays.copyOfRange(args, optind, args.length); 671 } else { 672 remainingArgs = new String[0]; // all args used up, send back empty. 673 } 674 675 return remainingArgs; 676 } 677 getPrologue()678 private String getPrologue() { 679 // Assign default prologue statement when none given. 680 if (prologue == null) { 681 prologue = "Usage: MyProgram [options]"; 682 } 683 684 return prologue; 685 } 686 687 688 /** 689 * Define the prologue to be presented before the options summary. 690 * Example: Usage programName [options] 691 * @param text that makes up the prologue. 692 */ setPrologue(String text)693 public void setPrologue(String text) { 694 prologue = text; 695 } 696 697 /** 698 * Define the epilogue to be presented after the options summary. 699 * @param text that makes up the epilogue. 700 */ setEpilogue(String text)701 public void setEpilogue(String text) { 702 epilogue = text; 703 } 704 705 /** 706 * Place text in option summary. 707 * @param text to be inserted into option summary. 708 * 709 * Example usage: 710 * <code> 711 * OptionParser opts = OptionParser.execute( parser -> { 712 * 713 * parser.prologue = String.format("Usage: %s [options] bubba smith", program); 714 * parser.separator(""); 715 * 716 * parser.on("-y value", "--why me", "This is a description").execute( v -> { 717 * System.out.println("got " + v); 718 * }); 719 * 720 * parser.separator(" ----------------------------------------------"); 721 * parser.separator(" Common Options:"); 722 * ... 723 * 724 * parser.separator(" ----------------------------------------------"); 725 * parser.epilogue = " That's all Folks!"; 726 * </code> 727 */ separator(String text)728 public void separator(String text) { 729 usageSummary.add(text); 730 } 731 732 /** 733 * Obtain option summary. 734 * @param indent a string to be used as the option summary initial indent. 735 * @return usage string 736 */ getUsage(String indent)737 public String getUsage(String indent) { 738 739 StringWriter wrt = new StringWriter(); 740 try (PrintWriter out = new PrintWriter(wrt)) { 741 out.println(getPrologue()); 742 for (Object o : usageSummary) { 743 // Need to be able to handle separator strings 744 if (o instanceof String) { 745 out.println((String) o); 746 } else { 747 out.println(indent + ((Option) o).getUsage()); 748 } 749 } 750 if (epilogue != null) { 751 out.println(epilogue); 752 } 753 out.flush(); 754 } 755 return wrt.toString(); 756 } 757 758 /** 759 * Obtain option summary. 760 * @return option summary 761 */ getUsage()762 public String getUsage() { 763 return getUsage(" "); 764 } 765 766 /** 767 * Print out option summary. 768 */ help()769 public void help() { 770 System.out.println(getUsage()); 771 } 772 773 /** 774 * Print out option summary on provided output stream. 775 * @param out print stream 776 */ help(PrintStream out)777 public void help(PrintStream out) { 778 out.println(getUsage()); 779 } 780 getOptionList()781 protected List<Option> getOptionList() { 782 return optionList; 783 } 784 } 785