001/* 002 * Copyright 2008-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2018 Ping Identity Corporation 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.util.args; 022 023 024 025import java.io.BufferedReader; 026import java.io.File; 027import java.io.FileInputStream; 028import java.io.FileOutputStream; 029import java.io.IOException; 030import java.io.InputStreamReader; 031import java.io.OutputStream; 032import java.io.OutputStreamWriter; 033import java.io.PrintWriter; 034import java.io.Serializable; 035import java.nio.charset.StandardCharsets; 036import java.util.ArrayList; 037import java.util.Arrays; 038import java.util.Collection; 039import java.util.Collections; 040import java.util.HashMap; 041import java.util.Iterator; 042import java.util.LinkedHashSet; 043import java.util.LinkedHashMap; 044import java.util.List; 045import java.util.Map; 046import java.util.Set; 047 048import com.unboundid.util.Debug; 049import com.unboundid.util.ObjectPair; 050import com.unboundid.util.StaticUtils; 051import com.unboundid.util.ThreadSafety; 052import com.unboundid.util.ThreadSafetyLevel; 053import com.unboundid.util.Validator; 054 055import static com.unboundid.util.args.ArgsMessages.*; 056 057 058 059/** 060 * This class provides an argument parser, which may be used to process command 061 * line arguments provided to Java applications. See the package-level Javadoc 062 * documentation for details regarding the capabilities of the argument parser. 063 */ 064@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 065public final class ArgumentParser 066 implements Serializable 067{ 068 /** 069 * The name of the system property that can be used to specify the default 070 * properties file that should be used to obtain the default values for 071 * arguments not specified via the command line. 072 */ 073 public static final String PROPERTY_DEFAULT_PROPERTIES_FILE_PATH = 074 ArgumentParser.class.getName() + ".propertiesFilePath"; 075 076 077 078 /** 079 * The name of an environment variable that can be used to specify the default 080 * properties file that should be used to obtain the default values for 081 * arguments not specified via the command line. 082 */ 083 public static final String ENV_DEFAULT_PROPERTIES_FILE_PATH = 084 "UNBOUNDID_TOOL_PROPERTIES_FILE_PATH"; 085 086 087 088 /** 089 * The name of the argument used to specify the path to a file to which all 090 * output should be written. 091 */ 092 private static final String ARG_NAME_OUTPUT_FILE = "outputFile"; 093 094 095 096 /** 097 * The name of the argument used to indicate that output should be written to 098 * both the output file and the console. 099 */ 100 private static final String ARG_NAME_TEE_OUTPUT = "teeOutput"; 101 102 103 104 /** 105 * The name of the argument used to specify the path to a properties file from 106 * which to obtain the default values for arguments not specified via the 107 * command line. 108 */ 109 private static final String ARG_NAME_PROPERTIES_FILE_PATH = 110 "propertiesFilePath"; 111 112 113 114 /** 115 * The name of the argument used to specify the path to a file to be generated 116 * with information about the properties that the tool supports. 117 */ 118 private static final String ARG_NAME_GENERATE_PROPERTIES_FILE = 119 "generatePropertiesFile"; 120 121 122 123 /** 124 * The name of the argument used to indicate that the tool should not use any 125 * properties file to obtain default values for arguments not specified via 126 * the command line. 127 */ 128 private static final String ARG_NAME_NO_PROPERTIES_FILE = "noPropertiesFile"; 129 130 131 132 /** 133 * The name of the argument used to indicate that the tool should suppress the 134 * comment that lists the argument values obtained from a properties file. 135 */ 136 private static final String ARG_NAME_SUPPRESS_PROPERTIES_FILE_COMMENT = 137 "suppressPropertiesFileComment"; 138 139 140 141 /** 142 * The serial version UID for this serializable class. 143 */ 144 private static final long serialVersionUID = 3053102992180360269L; 145 146 147 148 // The properties file used to obtain arguments for this tool. 149 private volatile File propertiesFileUsed; 150 151 // The maximum number of trailing arguments allowed to be provided. 152 private final int maxTrailingArgs; 153 154 // The minimum number of trailing arguments allowed to be provided. 155 private final int minTrailingArgs; 156 157 // The set of named arguments associated with this parser, indexed by short 158 // identifier. 159 private final LinkedHashMap<Character,Argument> namedArgsByShortID; 160 161 // The set of named arguments associated with this parser, indexed by long 162 // identifier. 163 private final LinkedHashMap<String,Argument> namedArgsByLongID; 164 165 // The set of subcommands associated with this parser, indexed by name. 166 private final LinkedHashMap<String,SubCommand> subCommandsByName; 167 168 // The full set of named arguments associated with this parser. 169 private final List<Argument> namedArgs; 170 171 // Sets of arguments in which if the key argument is provided, then at least 172 // one of the value arguments must also be provided. 173 private final List<ObjectPair<Argument,Set<Argument>>> dependentArgumentSets; 174 175 // Sets of arguments in which at most one argument in the list is allowed to 176 // be present. 177 private final List<Set<Argument>> exclusiveArgumentSets; 178 179 // Sets of arguments in which at least one argument in the list is required to 180 // be present. 181 private final List<Set<Argument>> requiredArgumentSets; 182 183 // A list of any arguments set from the properties file rather than explicitly 184 // provided on the command line. 185 private final List<String> argumentsSetFromPropertiesFile; 186 187 // The list of trailing arguments provided on the command line. 188 private final List<String> trailingArgs; 189 190 // The full list of subcommands associated with this argument parser. 191 private final List<SubCommand> subCommands; 192 193 // The description for the associated command. 194 private final String commandDescription; 195 196 // The name for the associated command. 197 private final String commandName; 198 199 // The placeholder string for the trailing arguments. 200 private final String trailingArgsPlaceholder; 201 202 // The subcommand with which this argument parser is associated. 203 private volatile SubCommand parentSubCommand; 204 205 // The subcommand that was included in the set of command-line arguments. 206 private volatile SubCommand selectedSubCommand; 207 208 209 210 /** 211 * Creates a new instance of this argument parser with the provided 212 * information. It will not allow unnamed trailing arguments. 213 * 214 * @param commandName The name of the application or utility with 215 * which this argument parser is associated. It 216 * must not be {@code null}. 217 * @param commandDescription A description of the application or utility 218 * with which this argument parser is associated. 219 * It will be included in generated usage 220 * information. It must not be {@code null}. 221 * 222 * @throws ArgumentException If either the command name or command 223 * description is {@code null}, 224 */ 225 public ArgumentParser(final String commandName, 226 final String commandDescription) 227 throws ArgumentException 228 { 229 this(commandName, commandDescription, 0, null); 230 } 231 232 233 234 /** 235 * Creates a new instance of this argument parser with the provided 236 * information. 237 * 238 * @param commandName The name of the application or utility 239 * with which this argument parser is 240 * associated. It must not be {@code null}. 241 * @param commandDescription A description of the application or 242 * utility with which this argument parser is 243 * associated. It will be included in 244 * generated usage information. It must not 245 * be {@code null}. 246 * @param maxTrailingArgs The maximum number of trailing arguments 247 * that may be provided to this command. A 248 * value of zero indicates that no trailing 249 * arguments will be allowed. A value less 250 * than zero will indicate that there is no 251 * limit on the number of trailing arguments 252 * allowed. 253 * @param trailingArgsPlaceholder A placeholder string that will be included 254 * in usage output to indicate what trailing 255 * arguments may be provided. It must not be 256 * {@code null} if {@code maxTrailingArgs} is 257 * anything other than zero. 258 * 259 * @throws ArgumentException If either the command name or command 260 * description is {@code null}, or if the maximum 261 * number of trailing arguments is non-zero and 262 * the trailing arguments placeholder is 263 * {@code null}. 264 */ 265 public ArgumentParser(final String commandName, 266 final String commandDescription, 267 final int maxTrailingArgs, 268 final String trailingArgsPlaceholder) 269 throws ArgumentException 270 { 271 this(commandName, commandDescription, 0, maxTrailingArgs, 272 trailingArgsPlaceholder); 273 } 274 275 276 277 /** 278 * Creates a new instance of this argument parser with the provided 279 * information. 280 * 281 * @param commandName The name of the application or utility 282 * with which this argument parser is 283 * associated. It must not be {@code null}. 284 * @param commandDescription A description of the application or 285 * utility with which this argument parser is 286 * associated. It will be included in 287 * generated usage information. It must not 288 * be {@code null}. 289 * @param minTrailingArgs The minimum number of trailing arguments 290 * that must be provided for this command. A 291 * value of zero indicates that the command 292 * may be invoked without any trailing 293 * arguments. 294 * @param maxTrailingArgs The maximum number of trailing arguments 295 * that may be provided to this command. A 296 * value of zero indicates that no trailing 297 * arguments will be allowed. A value less 298 * than zero will indicate that there is no 299 * limit on the number of trailing arguments 300 * allowed. 301 * @param trailingArgsPlaceholder A placeholder string that will be included 302 * in usage output to indicate what trailing 303 * arguments may be provided. It must not be 304 * {@code null} if {@code maxTrailingArgs} is 305 * anything other than zero. 306 * 307 * @throws ArgumentException If either the command name or command 308 * description is {@code null}, or if the maximum 309 * number of trailing arguments is non-zero and 310 * the trailing arguments placeholder is 311 * {@code null}. 312 */ 313 public ArgumentParser(final String commandName, 314 final String commandDescription, 315 final int minTrailingArgs, 316 final int maxTrailingArgs, 317 final String trailingArgsPlaceholder) 318 throws ArgumentException 319 { 320 if (commandName == null) 321 { 322 throw new ArgumentException(ERR_PARSER_COMMAND_NAME_NULL.get()); 323 } 324 325 if (commandDescription == null) 326 { 327 throw new ArgumentException(ERR_PARSER_COMMAND_DESCRIPTION_NULL.get()); 328 } 329 330 if ((maxTrailingArgs != 0) && (trailingArgsPlaceholder == null)) 331 { 332 throw new ArgumentException( 333 ERR_PARSER_TRAILING_ARGS_PLACEHOLDER_NULL.get()); 334 } 335 336 this.commandName = commandName; 337 this.commandDescription = commandDescription; 338 this.trailingArgsPlaceholder = trailingArgsPlaceholder; 339 340 if (minTrailingArgs >= 0) 341 { 342 this.minTrailingArgs = minTrailingArgs; 343 } 344 else 345 { 346 this.minTrailingArgs = 0; 347 } 348 349 if (maxTrailingArgs >= 0) 350 { 351 this.maxTrailingArgs = maxTrailingArgs; 352 } 353 else 354 { 355 this.maxTrailingArgs = Integer.MAX_VALUE; 356 } 357 358 if (this.minTrailingArgs > this.maxTrailingArgs) 359 { 360 throw new ArgumentException(ERR_PARSER_TRAILING_ARGS_COUNT_MISMATCH.get( 361 this.minTrailingArgs, this.maxTrailingArgs)); 362 } 363 364 namedArgsByShortID = new LinkedHashMap<>(20); 365 namedArgsByLongID = new LinkedHashMap<>(20); 366 namedArgs = new ArrayList<>(20); 367 trailingArgs = new ArrayList<>(20); 368 dependentArgumentSets = new ArrayList<>(20); 369 exclusiveArgumentSets = new ArrayList<>(20); 370 requiredArgumentSets = new ArrayList<>(20); 371 parentSubCommand = null; 372 selectedSubCommand = null; 373 subCommands = new ArrayList<>(20); 374 subCommandsByName = new LinkedHashMap<>(20); 375 propertiesFileUsed = null; 376 argumentsSetFromPropertiesFile = new ArrayList<>(20); 377 } 378 379 380 381 /** 382 * Creates a new argument parser that is a "clean" copy of the provided source 383 * argument parser. 384 * 385 * @param source The source argument parser to use for this argument 386 * parser. 387 * @param subCommand The subcommand with which this argument parser is to be 388 * associated. 389 */ 390 ArgumentParser(final ArgumentParser source, final SubCommand subCommand) 391 { 392 commandName = source.commandName; 393 commandDescription = source.commandDescription; 394 minTrailingArgs = source.minTrailingArgs; 395 maxTrailingArgs = source.maxTrailingArgs; 396 trailingArgsPlaceholder = source.trailingArgsPlaceholder; 397 398 propertiesFileUsed = null; 399 argumentsSetFromPropertiesFile = new ArrayList<>(20); 400 trailingArgs = new ArrayList<>(20); 401 402 namedArgs = new ArrayList<>(source.namedArgs.size()); 403 namedArgsByLongID = 404 new LinkedHashMap<>(source.namedArgsByLongID.size()); 405 namedArgsByShortID = new LinkedHashMap<>(source.namedArgsByShortID.size()); 406 407 final LinkedHashMap<String,Argument> argsByID = 408 new LinkedHashMap<>(source.namedArgs.size()); 409 for (final Argument sourceArg : source.namedArgs) 410 { 411 final Argument a = sourceArg.getCleanCopy(); 412 413 try 414 { 415 a.setRegistered(); 416 } 417 catch (final ArgumentException ae) 418 { 419 // This should never happen. 420 Debug.debugException(ae); 421 } 422 423 namedArgs.add(a); 424 argsByID.put(a.getIdentifierString(), a); 425 426 for (final Character c : a.getShortIdentifiers(true)) 427 { 428 namedArgsByShortID.put(c, a); 429 } 430 431 for (final String s : a.getLongIdentifiers(true)) 432 { 433 namedArgsByLongID.put(StaticUtils.toLowerCase(s), a); 434 } 435 } 436 437 dependentArgumentSets = 438 new ArrayList<>(source.dependentArgumentSets.size()); 439 for (final ObjectPair<Argument,Set<Argument>> p : 440 source.dependentArgumentSets) 441 { 442 final Set<Argument> sourceSet = p.getSecond(); 443 final LinkedHashSet<Argument> newSet = 444 new LinkedHashSet<>(sourceSet.size()); 445 for (final Argument a : sourceSet) 446 { 447 newSet.add(argsByID.get(a.getIdentifierString())); 448 } 449 450 final Argument sourceFirst = p.getFirst(); 451 final Argument newFirst = argsByID.get(sourceFirst.getIdentifierString()); 452 dependentArgumentSets.add( 453 new ObjectPair<Argument, Set<Argument>>(newFirst, newSet)); 454 } 455 456 exclusiveArgumentSets = 457 new ArrayList<>(source.exclusiveArgumentSets.size()); 458 for (final Set<Argument> sourceSet : source.exclusiveArgumentSets) 459 { 460 final LinkedHashSet<Argument> newSet = 461 new LinkedHashSet<>(sourceSet.size()); 462 for (final Argument a : sourceSet) 463 { 464 newSet.add(argsByID.get(a.getIdentifierString())); 465 } 466 467 exclusiveArgumentSets.add(newSet); 468 } 469 470 requiredArgumentSets = 471 new ArrayList<>(source.requiredArgumentSets.size()); 472 for (final Set<Argument> sourceSet : source.requiredArgumentSets) 473 { 474 final LinkedHashSet<Argument> newSet = 475 new LinkedHashSet<>(sourceSet.size()); 476 for (final Argument a : sourceSet) 477 { 478 newSet.add(argsByID.get(a.getIdentifierString())); 479 } 480 requiredArgumentSets.add(newSet); 481 } 482 483 parentSubCommand = subCommand; 484 selectedSubCommand = null; 485 subCommands = new ArrayList<>(source.subCommands.size()); 486 subCommandsByName = new LinkedHashMap<>(source.subCommandsByName.size()); 487 for (final SubCommand sc : source.subCommands) 488 { 489 subCommands.add(sc.getCleanCopy()); 490 for (final String name : sc.getNames(true)) 491 { 492 subCommandsByName.put(StaticUtils.toLowerCase(name), sc); 493 } 494 } 495 } 496 497 498 499 /** 500 * Retrieves the name of the application or utility with which this command 501 * line argument parser is associated. 502 * 503 * @return The name of the application or utility with which this command 504 * line argument parser is associated. 505 */ 506 public String getCommandName() 507 { 508 return commandName; 509 } 510 511 512 513 /** 514 * Retrieves a description of the application or utility with which this 515 * command line argument parser is associated. 516 * 517 * @return A description of the application or utility with which this 518 * command line argument parser is associated. 519 */ 520 public String getCommandDescription() 521 { 522 return commandDescription; 523 } 524 525 526 527 /** 528 * Indicates whether this argument parser allows any unnamed trailing 529 * arguments to be provided. 530 * 531 * @return {@code true} if at least one unnamed trailing argument may be 532 * provided, or {@code false} if not. 533 */ 534 public boolean allowsTrailingArguments() 535 { 536 return (maxTrailingArgs != 0); 537 } 538 539 540 541 /** 542 * Indicates whether this argument parser requires at least unnamed trailing 543 * argument to be provided. 544 * 545 * @return {@code true} if at least one unnamed trailing argument must be 546 * provided, or {@code false} if the tool may be invoked without any 547 * such arguments. 548 */ 549 public boolean requiresTrailingArguments() 550 { 551 return (minTrailingArgs != 0); 552 } 553 554 555 556 /** 557 * Retrieves the placeholder string that will be provided in usage information 558 * to indicate what may be included in the trailing arguments. 559 * 560 * @return The placeholder string that will be provided in usage information 561 * to indicate what may be included in the trailing arguments, or 562 * {@code null} if unnamed trailing arguments are not allowed. 563 */ 564 public String getTrailingArgumentsPlaceholder() 565 { 566 return trailingArgsPlaceholder; 567 } 568 569 570 571 /** 572 * Retrieves the minimum number of unnamed trailing arguments that must be 573 * provided. 574 * 575 * @return The minimum number of unnamed trailing arguments that must be 576 * provided. 577 */ 578 public int getMinTrailingArguments() 579 { 580 return minTrailingArgs; 581 } 582 583 584 585 /** 586 * Retrieves the maximum number of unnamed trailing arguments that may be 587 * provided. 588 * 589 * @return The maximum number of unnamed trailing arguments that may be 590 * provided. 591 */ 592 public int getMaxTrailingArguments() 593 { 594 return maxTrailingArgs; 595 } 596 597 598 599 /** 600 * Updates this argument parser to enable support for a properties file that 601 * can be used to specify the default values for any properties that were not 602 * supplied via the command line. This method should be invoked after the 603 * argument parser has been configured with all of the other arguments that it 604 * supports and before the {@link #parse} method is invoked. In addition, 605 * after invoking the {@code parse} method, the caller must also invoke the 606 * {@link #getGeneratedPropertiesFile} method to determine if the only 607 * processing performed that should be performed is the generation of a 608 * properties file that will have already been performed. 609 * <BR><BR> 610 * This method will update the argument parser to add the following additional 611 * arguments: 612 * <UL> 613 * <LI> 614 * {@code propertiesFilePath} -- Specifies the path to the properties file 615 * that should be used to obtain default values for any arguments not 616 * provided on the command line. If this is not specified and the 617 * {@code noPropertiesFile} argument is not present, then the argument 618 * parser may use a default properties file path specified using either 619 * the {@code com.unboundid.util.args.ArgumentParser..propertiesFilePath} 620 * system property or the {@code UNBOUNDID_TOOL_PROPERTIES_FILE_PATH} 621 * environment variable. 622 * </LI> 623 * <LI> 624 * {@code generatePropertiesFile} -- Indicates that the tool should 625 * generate a properties file for this argument parser and write it to the 626 * specified location. The generated properties file will not have any 627 * properties set, but will include comments that describe all of the 628 * supported arguments, as well general information about the use of a 629 * properties file. If this argument is specified on the command line, 630 * then no other arguments should be given. 631 * </LI> 632 * <LI> 633 * {@code noPropertiesFile} -- Indicates that the tool should not use a 634 * properties file to obtain default values for any arguments not provided 635 * on the command line. 636 * </LI> 637 * </UL> 638 * 639 * @throws ArgumentException If any of the arguments related to properties 640 * file processing conflicts with an argument that 641 * has already been added to the argument parser. 642 */ 643 public void enablePropertiesFileSupport() 644 throws ArgumentException 645 { 646 final FileArgument propertiesFilePath = new FileArgument(null, 647 ARG_NAME_PROPERTIES_FILE_PATH, false, 1, null, 648 INFO_ARG_DESCRIPTION_PROP_FILE_PATH.get(), true, true, true, false); 649 propertiesFilePath.setUsageArgument(true); 650 propertiesFilePath.addLongIdentifier("properties-file-path", true); 651 addArgument(propertiesFilePath); 652 653 final FileArgument generatePropertiesFile = new FileArgument(null, 654 ARG_NAME_GENERATE_PROPERTIES_FILE, false, 1, null, 655 INFO_ARG_DESCRIPTION_GEN_PROP_FILE.get(), false, true, true, false); 656 generatePropertiesFile.setUsageArgument(true); 657 generatePropertiesFile.addLongIdentifier("generate-properties-file", true); 658 addArgument(generatePropertiesFile); 659 660 final BooleanArgument noPropertiesFile = new BooleanArgument(null, 661 ARG_NAME_NO_PROPERTIES_FILE, INFO_ARG_DESCRIPTION_NO_PROP_FILE.get()); 662 noPropertiesFile.setUsageArgument(true); 663 noPropertiesFile.addLongIdentifier("no-properties-file", true); 664 addArgument(noPropertiesFile); 665 666 final BooleanArgument suppressPropertiesFileComment = new BooleanArgument( 667 null, ARG_NAME_SUPPRESS_PROPERTIES_FILE_COMMENT, 1, 668 INFO_ARG_DESCRIPTION_SUPPRESS_PROP_FILE_COMMENT.get()); 669 suppressPropertiesFileComment.setUsageArgument(true); 670 suppressPropertiesFileComment.addLongIdentifier( 671 "suppress-properties-file-comment", true); 672 addArgument(suppressPropertiesFileComment); 673 674 675 // The propertiesFilePath and noPropertiesFile arguments cannot be used 676 // together. 677 addExclusiveArgumentSet(propertiesFilePath, noPropertiesFile); 678 } 679 680 681 682 /** 683 * Indicates whether this argument parser was used to generate a properties 684 * file. If so, then the tool invoking the parser should return without 685 * performing any further processing. 686 * 687 * @return A {@code File} object that represents the path to the properties 688 * file that was generated, or {@code null} if no properties file was 689 * generated. 690 */ 691 public File getGeneratedPropertiesFile() 692 { 693 final Argument a = getNamedArgument(ARG_NAME_GENERATE_PROPERTIES_FILE); 694 if ((a == null) || (! a.isPresent()) || (! (a instanceof FileArgument))) 695 { 696 return null; 697 } 698 699 return ((FileArgument) a).getValue(); 700 } 701 702 703 704 /** 705 * Retrieves the named argument with the specified short identifier. 706 * 707 * @param shortIdentifier The short identifier of the argument to retrieve. 708 * It must not be {@code null}. 709 * 710 * @return The named argument with the specified short identifier, or 711 * {@code null} if there is no such argument. 712 */ 713 public Argument getNamedArgument(final Character shortIdentifier) 714 { 715 Validator.ensureNotNull(shortIdentifier); 716 return namedArgsByShortID.get(shortIdentifier); 717 } 718 719 720 721 /** 722 * Retrieves the named argument with the specified identifier. 723 * 724 * @param identifier The identifier of the argument to retrieve. It may be 725 * the long identifier without any dashes, the short 726 * identifier character preceded by a single dash, or the 727 * long identifier preceded by two dashes. It must not be 728 * {@code null}. 729 * 730 * @return The named argument with the specified long identifier, or 731 * {@code null} if there is no such argument. 732 */ 733 public Argument getNamedArgument(final String identifier) 734 { 735 Validator.ensureNotNull(identifier); 736 737 if (identifier.startsWith("--") && (identifier.length() > 2)) 738 { 739 return namedArgsByLongID.get( 740 StaticUtils.toLowerCase(identifier.substring(2))); 741 } 742 else if (identifier.startsWith("-") && (identifier.length() == 2)) 743 { 744 return namedArgsByShortID.get(identifier.charAt(1)); 745 } 746 else 747 { 748 return namedArgsByLongID.get(StaticUtils.toLowerCase(identifier)); 749 } 750 } 751 752 753 754 /** 755 * Retrieves the argument list argument with the specified identifier. 756 * 757 * @param identifier The identifier of the argument to retrieve. It may be 758 * the long identifier without any dashes, the short 759 * identifier character preceded by a single dash, or the 760 * long identifier preceded by two dashes. It must not be 761 * {@code null}. 762 * 763 * @return The argument list argument with the specified identifier, or 764 * {@code null} if there is no such argument. 765 */ 766 public ArgumentListArgument getArgumentListArgument(final String identifier) 767 { 768 final Argument a = getNamedArgument(identifier); 769 if (a == null) 770 { 771 return null; 772 } 773 else 774 { 775 return (ArgumentListArgument) a; 776 } 777 } 778 779 780 781 /** 782 * Retrieves the Boolean argument with the specified identifier. 783 * 784 * @param identifier The identifier of the argument to retrieve. It may be 785 * the long identifier without any dashes, the short 786 * identifier character preceded by a single dash, or the 787 * long identifier preceded by two dashes. It must not be 788 * {@code null}. 789 * 790 * @return The Boolean argument with the specified identifier, or 791 * {@code null} if there is no such argument. 792 */ 793 public BooleanArgument getBooleanArgument(final String identifier) 794 { 795 final Argument a = getNamedArgument(identifier); 796 if (a == null) 797 { 798 return null; 799 } 800 else 801 { 802 return (BooleanArgument) a; 803 } 804 } 805 806 807 808 /** 809 * Retrieves the Boolean value argument with the specified identifier. 810 * 811 * @param identifier The identifier of the argument to retrieve. It may be 812 * the long identifier without any dashes, the short 813 * identifier character preceded by a single dash, or the 814 * long identifier preceded by two dashes. It must not be 815 * {@code null}. 816 * 817 * @return The Boolean value argument with the specified identifier, or 818 * {@code null} if there is no such argument. 819 */ 820 public BooleanValueArgument getBooleanValueArgument(final String identifier) 821 { 822 final Argument a = getNamedArgument(identifier); 823 if (a == null) 824 { 825 return null; 826 } 827 else 828 { 829 return (BooleanValueArgument) a; 830 } 831 } 832 833 834 835 /** 836 * Retrieves the control argument with the specified identifier. 837 * 838 * @param identifier The identifier of the argument to retrieve. It may be 839 * the long identifier without any dashes, the short 840 * identifier character preceded by a single dash, or the 841 * long identifier preceded by two dashes. It must not be 842 * {@code null}. 843 * 844 * @return The control argument with the specified identifier, or 845 * {@code null} if there is no such argument. 846 */ 847 public ControlArgument getControlArgument(final String identifier) 848 { 849 final Argument a = getNamedArgument(identifier); 850 if (a == null) 851 { 852 return null; 853 } 854 else 855 { 856 return (ControlArgument) a; 857 } 858 } 859 860 861 862 /** 863 * Retrieves the DN argument with the specified identifier. 864 * 865 * @param identifier The identifier of the argument to retrieve. It may be 866 * the long identifier without any dashes, the short 867 * identifier character preceded by a single dash, or the 868 * long identifier preceded by two dashes. It must not be 869 * {@code null}. 870 * 871 * @return The DN argument with the specified identifier, or 872 * {@code null} if there is no such argument. 873 */ 874 public DNArgument getDNArgument(final String identifier) 875 { 876 final Argument a = getNamedArgument(identifier); 877 if (a == null) 878 { 879 return null; 880 } 881 else 882 { 883 return (DNArgument) a; 884 } 885 } 886 887 888 889 /** 890 * Retrieves the duration argument with the specified identifier. 891 * 892 * @param identifier The identifier of the argument to retrieve. It may be 893 * the long identifier without any dashes, the short 894 * identifier character preceded by a single dash, or the 895 * long identifier preceded by two dashes. It must not be 896 * {@code null}. 897 * 898 * @return The duration argument with the specified identifier, or 899 * {@code null} if there is no such argument. 900 */ 901 public DurationArgument getDurationArgument(final String identifier) 902 { 903 final Argument a = getNamedArgument(identifier); 904 if (a == null) 905 { 906 return null; 907 } 908 else 909 { 910 return (DurationArgument) a; 911 } 912 } 913 914 915 916 /** 917 * Retrieves the file argument with the specified identifier. 918 * 919 * @param identifier The identifier of the argument to retrieve. It may be 920 * the long identifier without any dashes, the short 921 * identifier character preceded by a single dash, or the 922 * long identifier preceded by two dashes. It must not be 923 * {@code null}. 924 * 925 * @return The file argument with the specified identifier, or 926 * {@code null} if there is no such argument. 927 */ 928 public FileArgument getFileArgument(final String identifier) 929 { 930 final Argument a = getNamedArgument(identifier); 931 if (a == null) 932 { 933 return null; 934 } 935 else 936 { 937 return (FileArgument) a; 938 } 939 } 940 941 942 943 /** 944 * Retrieves the filter argument with the specified identifier. 945 * 946 * @param identifier The identifier of the argument to retrieve. It may be 947 * the long identifier without any dashes, the short 948 * identifier character preceded by a single dash, or the 949 * long identifier preceded by two dashes. It must not be 950 * {@code null}. 951 * 952 * @return The filter argument with the specified identifier, or 953 * {@code null} if there is no such argument. 954 */ 955 public FilterArgument getFilterArgument(final String identifier) 956 { 957 final Argument a = getNamedArgument(identifier); 958 if (a == null) 959 { 960 return null; 961 } 962 else 963 { 964 return (FilterArgument) a; 965 } 966 } 967 968 969 970 /** 971 * Retrieves the integer argument with the specified identifier. 972 * 973 * @param identifier The identifier of the argument to retrieve. It may be 974 * the long identifier without any dashes, the short 975 * identifier character preceded by a single dash, or the 976 * long identifier preceded by two dashes. It must not be 977 * {@code null}. 978 * 979 * @return The integer argument with the specified identifier, or 980 * {@code null} if there is no such argument. 981 */ 982 public IntegerArgument getIntegerArgument(final String identifier) 983 { 984 final Argument a = getNamedArgument(identifier); 985 if (a == null) 986 { 987 return null; 988 } 989 else 990 { 991 return (IntegerArgument) a; 992 } 993 } 994 995 996 997 /** 998 * Retrieves the scope argument with the specified identifier. 999 * 1000 * @param identifier The identifier of the argument to retrieve. It may be 1001 * the long identifier without any dashes, the short 1002 * identifier character preceded by a single dash, or the 1003 * long identifier preceded by two dashes. It must not be 1004 * {@code null}. 1005 * 1006 * @return The scope argument with the specified identifier, or 1007 * {@code null} if there is no such argument. 1008 */ 1009 public ScopeArgument getScopeArgument(final String identifier) 1010 { 1011 final Argument a = getNamedArgument(identifier); 1012 if (a == null) 1013 { 1014 return null; 1015 } 1016 else 1017 { 1018 return (ScopeArgument) a; 1019 } 1020 } 1021 1022 1023 1024 /** 1025 * Retrieves the string argument with the specified identifier. 1026 * 1027 * @param identifier The identifier of the argument to retrieve. It may be 1028 * the long identifier without any dashes, the short 1029 * identifier character preceded by a single dash, or the 1030 * long identifier preceded by two dashes. It must not be 1031 * {@code null}. 1032 * 1033 * @return The string argument with the specified identifier, or 1034 * {@code null} if there is no such argument. 1035 */ 1036 public StringArgument getStringArgument(final String identifier) 1037 { 1038 final Argument a = getNamedArgument(identifier); 1039 if (a == null) 1040 { 1041 return null; 1042 } 1043 else 1044 { 1045 return (StringArgument) a; 1046 } 1047 } 1048 1049 1050 1051 /** 1052 * Retrieves the timestamp argument with the specified identifier. 1053 * 1054 * @param identifier The identifier of the argument to retrieve. It may be 1055 * the long identifier without any dashes, the short 1056 * identifier character preceded by a single dash, or the 1057 * long identifier preceded by two dashes. It must not be 1058 * {@code null}. 1059 * 1060 * @return The timestamp argument with the specified identifier, or 1061 * {@code null} if there is no such argument. 1062 */ 1063 public TimestampArgument getTimestampArgument(final String identifier) 1064 { 1065 final Argument a = getNamedArgument(identifier); 1066 if (a == null) 1067 { 1068 return null; 1069 } 1070 else 1071 { 1072 return (TimestampArgument) a; 1073 } 1074 } 1075 1076 1077 1078 /** 1079 * Retrieves the set of named arguments defined for use with this argument 1080 * parser. 1081 * 1082 * @return The set of named arguments defined for use with this argument 1083 * parser. 1084 */ 1085 public List<Argument> getNamedArguments() 1086 { 1087 return Collections.unmodifiableList(namedArgs); 1088 } 1089 1090 1091 1092 /** 1093 * Registers the provided argument with this argument parser. 1094 * 1095 * @param argument The argument to be registered. 1096 * 1097 * @throws ArgumentException If the provided argument conflicts with another 1098 * argument already registered with this parser. 1099 */ 1100 public void addArgument(final Argument argument) 1101 throws ArgumentException 1102 { 1103 argument.setRegistered(); 1104 for (final Character c : argument.getShortIdentifiers(true)) 1105 { 1106 if (namedArgsByShortID.containsKey(c)) 1107 { 1108 throw new ArgumentException(ERR_PARSER_SHORT_ID_CONFLICT.get(c)); 1109 } 1110 1111 if ((parentSubCommand != null) && 1112 (parentSubCommand.getArgumentParser().namedArgsByShortID.containsKey( 1113 c))) 1114 { 1115 throw new ArgumentException(ERR_PARSER_SHORT_ID_CONFLICT.get(c)); 1116 } 1117 } 1118 1119 for (final String s : argument.getLongIdentifiers(true)) 1120 { 1121 if (namedArgsByLongID.containsKey(StaticUtils.toLowerCase(s))) 1122 { 1123 throw new ArgumentException(ERR_PARSER_LONG_ID_CONFLICT.get(s)); 1124 } 1125 1126 if ((parentSubCommand != null) && 1127 (parentSubCommand.getArgumentParser().namedArgsByLongID.containsKey( 1128 StaticUtils.toLowerCase(s)))) 1129 { 1130 throw new ArgumentException(ERR_PARSER_LONG_ID_CONFLICT.get(s)); 1131 } 1132 } 1133 1134 for (final SubCommand sc : subCommands) 1135 { 1136 final ArgumentParser parser = sc.getArgumentParser(); 1137 for (final Character c : argument.getShortIdentifiers(true)) 1138 { 1139 if (parser.namedArgsByShortID.containsKey(c)) 1140 { 1141 throw new ArgumentException( 1142 ERR_PARSER_SHORT_ID_CONFLICT_WITH_SUBCOMMAND.get(c, 1143 sc.getPrimaryName())); 1144 } 1145 } 1146 1147 for (final String s : argument.getLongIdentifiers(true)) 1148 { 1149 if (parser.namedArgsByLongID.containsKey(StaticUtils.toLowerCase(s))) 1150 { 1151 throw new ArgumentException( 1152 ERR_PARSER_LONG_ID_CONFLICT_WITH_SUBCOMMAND.get(s, 1153 sc.getPrimaryName())); 1154 } 1155 } 1156 } 1157 1158 for (final Character c : argument.getShortIdentifiers(true)) 1159 { 1160 namedArgsByShortID.put(c, argument); 1161 } 1162 1163 for (final String s : argument.getLongIdentifiers(true)) 1164 { 1165 namedArgsByLongID.put(StaticUtils.toLowerCase(s), argument); 1166 } 1167 1168 namedArgs.add(argument); 1169 } 1170 1171 1172 1173 /** 1174 * Retrieves the list of dependent argument sets for this argument parser. If 1175 * an argument contained as the first object in the pair in a dependent 1176 * argument set is provided, then at least one of the arguments in the paired 1177 * set must also be provided. 1178 * 1179 * @return The list of dependent argument sets for this argument parser. 1180 */ 1181 public List<ObjectPair<Argument,Set<Argument>>> getDependentArgumentSets() 1182 { 1183 return Collections.unmodifiableList(dependentArgumentSets); 1184 } 1185 1186 1187 1188 /** 1189 * Adds the provided collection of arguments as dependent upon the given 1190 * argument. All of the arguments must have already been registered with this 1191 * argument parser using the {@link #addArgument} method. 1192 * 1193 * @param targetArgument The argument whose presence indicates that at 1194 * least one of the dependent arguments must also 1195 * be present. It must not be {@code null}, and 1196 * it must have already been registered with this 1197 * argument parser. 1198 * @param dependentArguments The set of arguments from which at least one 1199 * argument must be present if the target argument 1200 * is present. It must not be {@code null} or 1201 * empty, and all arguments must have already been 1202 * registered with this argument parser. 1203 */ 1204 public void addDependentArgumentSet(final Argument targetArgument, 1205 final Collection<Argument> dependentArguments) 1206 { 1207 Validator.ensureNotNull(targetArgument, dependentArguments); 1208 1209 Validator.ensureFalse(dependentArguments.isEmpty(), 1210 "The ArgumentParser.addDependentArgumentSet method must not be " + 1211 "called with an empty collection of dependentArguments"); 1212 1213 Validator.ensureTrue(namedArgs.contains(targetArgument), 1214 "The ArgumentParser.addDependentArgumentSet method may only be used " + 1215 "if all of the provided arguments have already been registered " + 1216 "with the argument parser via the ArgumentParser.addArgument " + 1217 "method. The " + targetArgument.getIdentifierString() + 1218 " argument has not been registered with the argument parser."); 1219 for (final Argument a : dependentArguments) 1220 { 1221 Validator.ensureTrue(namedArgs.contains(a), 1222 "The ArgumentParser.addDependentArgumentSet method may only be " + 1223 "used if all of the provided arguments have already been " + 1224 "registered with the argument parser via the " + 1225 "ArgumentParser.addArgument method. The " + 1226 a.getIdentifierString() + " argument has not been registered " + 1227 "with the argument parser."); 1228 } 1229 1230 final LinkedHashSet<Argument> argSet = 1231 new LinkedHashSet<>(dependentArguments); 1232 dependentArgumentSets.add( 1233 new ObjectPair<Argument,Set<Argument>>(targetArgument, argSet)); 1234 } 1235 1236 1237 1238 /** 1239 * Adds the provided collection of arguments as dependent upon the given 1240 * argument. All of the arguments must have already been registered with this 1241 * argument parser using the {@link #addArgument} method. 1242 * 1243 * @param targetArgument The argument whose presence indicates that at least 1244 * one of the dependent arguments must also be 1245 * present. It must not be {@code null}, and it must 1246 * have already been registered with this argument 1247 * parser. 1248 * @param dependentArg1 The first argument in the set of arguments in which 1249 * at least one argument must be present if the target 1250 * argument is present. It must not be {@code null}, 1251 * and it must have already been registered with this 1252 * argument parser. 1253 * @param remaining The remaining arguments in the set of arguments in 1254 * which at least one argument must be present if the 1255 * target argument is present. It may be {@code null} 1256 * or empty if no additional dependent arguments are 1257 * needed, but if it is non-empty then all arguments 1258 * must have already been registered with this 1259 * argument parser. 1260 */ 1261 public void addDependentArgumentSet(final Argument targetArgument, 1262 final Argument dependentArg1, 1263 final Argument... remaining) 1264 { 1265 Validator.ensureNotNull(targetArgument, dependentArg1); 1266 1267 Validator.ensureTrue(namedArgs.contains(targetArgument), 1268 "The ArgumentParser.addDependentArgumentSet method may only be used " + 1269 "if all of the provided arguments have already been registered " + 1270 "with the argument parser via the ArgumentParser.addArgument " + 1271 "method. The " + targetArgument.getIdentifierString() + 1272 " argument has not been registered with the argument parser."); 1273 Validator.ensureTrue(namedArgs.contains(dependentArg1), 1274 "The ArgumentParser.addDependentArgumentSet method may only be used " + 1275 "if all of the provided arguments have already been registered " + 1276 "with the argument parser via the ArgumentParser.addArgument " + 1277 "method. The " + dependentArg1.getIdentifierString() + 1278 " argument has not been registered with the argument parser."); 1279 if (remaining != null) 1280 { 1281 for (final Argument a : remaining) 1282 { 1283 Validator.ensureTrue(namedArgs.contains(a), 1284 "The ArgumentParser.addDependentArgumentSet method may only be " + 1285 "used if all of the provided arguments have already been " + 1286 "registered with the argument parser via the " + 1287 "ArgumentParser.addArgument method. The " + 1288 a.getIdentifierString() + " argument has not been " + 1289 "registered with the argument parser."); 1290 } 1291 } 1292 1293 final LinkedHashSet<Argument> argSet = new LinkedHashSet<>(10); 1294 argSet.add(dependentArg1); 1295 if (remaining != null) 1296 { 1297 argSet.addAll(Arrays.asList(remaining)); 1298 } 1299 1300 dependentArgumentSets.add( 1301 new ObjectPair<Argument,Set<Argument>>(targetArgument, argSet)); 1302 } 1303 1304 1305 1306 /** 1307 * Retrieves the list of exclusive argument sets for this argument parser. 1308 * If an argument contained in an exclusive argument set is provided, then 1309 * none of the other arguments in that set may be provided. It is acceptable 1310 * for none of the arguments in the set to be provided, unless the same set 1311 * of arguments is also defined as a required argument set. 1312 * 1313 * @return The list of exclusive argument sets for this argument parser. 1314 */ 1315 public List<Set<Argument>> getExclusiveArgumentSets() 1316 { 1317 return Collections.unmodifiableList(exclusiveArgumentSets); 1318 } 1319 1320 1321 1322 /** 1323 * Adds the provided collection of arguments as an exclusive argument set, in 1324 * which at most one of the arguments may be provided. All of the arguments 1325 * must have already been registered with this argument parser using the 1326 * {@link #addArgument} method. 1327 * 1328 * @param exclusiveArguments The collection of arguments to form an 1329 * exclusive argument set. It must not be 1330 * {@code null}, and all of the arguments must 1331 * have already been registered with this argument 1332 * parser. 1333 */ 1334 public void addExclusiveArgumentSet( 1335 final Collection<Argument> exclusiveArguments) 1336 { 1337 Validator.ensureNotNull(exclusiveArguments); 1338 1339 for (final Argument a : exclusiveArguments) 1340 { 1341 Validator.ensureTrue(namedArgs.contains(a), 1342 "The ArgumentParser.addExclusiveArgumentSet method may only be " + 1343 "used if all of the provided arguments have already been " + 1344 "registered with the argument parser via the " + 1345 "ArgumentParser.addArgument method. The " + 1346 a.getIdentifierString() + " argument has not been " + 1347 "registered with the argument parser."); 1348 } 1349 1350 final LinkedHashSet<Argument> argSet = 1351 new LinkedHashSet<>(exclusiveArguments); 1352 exclusiveArgumentSets.add(Collections.unmodifiableSet(argSet)); 1353 } 1354 1355 1356 1357 /** 1358 * Adds the provided set of arguments as an exclusive argument set, in 1359 * which at most one of the arguments may be provided. All of the arguments 1360 * must have already been registered with this argument parser using the 1361 * {@link #addArgument} method. 1362 * 1363 * @param arg1 The first argument to include in the exclusive argument 1364 * set. It must not be {@code null}, and it must have 1365 * already been registered with this argument parser. 1366 * @param arg2 The second argument to include in the exclusive argument 1367 * set. It must not be {@code null}, and it must have 1368 * already been registered with this argument parser. 1369 * @param remaining Any additional arguments to include in the exclusive 1370 * argument set. It may be {@code null} or empty if no 1371 * additional exclusive arguments are needed, but if it is 1372 * non-empty then all arguments must have already been 1373 * registered with this argument parser. 1374 */ 1375 public void addExclusiveArgumentSet(final Argument arg1, final Argument arg2, 1376 final Argument... remaining) 1377 { 1378 Validator.ensureNotNull(arg1, arg2); 1379 1380 Validator.ensureTrue(namedArgs.contains(arg1), 1381 "The ArgumentParser.addExclusiveArgumentSet method may only be " + 1382 "used if all of the provided arguments have already been " + 1383 "registered with the argument parser via the " + 1384 "ArgumentParser.addArgument method. The " + 1385 arg1.getIdentifierString() + " argument has not been " + 1386 "registered with the argument parser."); 1387 Validator.ensureTrue(namedArgs.contains(arg2), 1388 "The ArgumentParser.addExclusiveArgumentSet method may only be " + 1389 "used if all of the provided arguments have already been " + 1390 "registered with the argument parser via the " + 1391 "ArgumentParser.addArgument method. The " + 1392 arg2.getIdentifierString() + " argument has not been " + 1393 "registered with the argument parser."); 1394 1395 if (remaining != null) 1396 { 1397 for (final Argument a : remaining) 1398 { 1399 Validator.ensureTrue(namedArgs.contains(a), 1400 "The ArgumentParser.addExclusiveArgumentSet method may only be " + 1401 "used if all of the provided arguments have already been " + 1402 "registered with the argument parser via the " + 1403 "ArgumentParser.addArgument method. The " + 1404 a.getIdentifierString() + " argument has not been " + 1405 "registered with the argument parser."); 1406 } 1407 } 1408 1409 final LinkedHashSet<Argument> argSet = new LinkedHashSet<>(10); 1410 argSet.add(arg1); 1411 argSet.add(arg2); 1412 1413 if (remaining != null) 1414 { 1415 argSet.addAll(Arrays.asList(remaining)); 1416 } 1417 1418 exclusiveArgumentSets.add(Collections.unmodifiableSet(argSet)); 1419 } 1420 1421 1422 1423 /** 1424 * Retrieves the list of required argument sets for this argument parser. At 1425 * least one of the arguments contained in this set must be provided. If this 1426 * same set is also defined as an exclusive argument set, then exactly one 1427 * of those arguments must be provided. 1428 * 1429 * @return The list of required argument sets for this argument parser. 1430 */ 1431 public List<Set<Argument>> getRequiredArgumentSets() 1432 { 1433 return Collections.unmodifiableList(requiredArgumentSets); 1434 } 1435 1436 1437 1438 /** 1439 * Adds the provided collection of arguments as a required argument set, in 1440 * which at least one of the arguments must be provided. All of the arguments 1441 * must have already been registered with this argument parser using the 1442 * {@link #addArgument} method. 1443 * 1444 * @param requiredArguments The collection of arguments to form an 1445 * required argument set. It must not be 1446 * {@code null}, and all of the arguments must have 1447 * already been registered with this argument 1448 * parser. 1449 */ 1450 public void addRequiredArgumentSet( 1451 final Collection<Argument> requiredArguments) 1452 { 1453 Validator.ensureNotNull(requiredArguments); 1454 1455 for (final Argument a : requiredArguments) 1456 { 1457 Validator.ensureTrue(namedArgs.contains(a), 1458 "The ArgumentParser.addRequiredArgumentSet method may only be " + 1459 "used if all of the provided arguments have already been " + 1460 "registered with the argument parser via the " + 1461 "ArgumentParser.addArgument method. The " + 1462 a.getIdentifierString() + " argument has not been " + 1463 "registered with the argument parser."); 1464 } 1465 1466 final LinkedHashSet<Argument> argSet = 1467 new LinkedHashSet<>(requiredArguments); 1468 requiredArgumentSets.add(Collections.unmodifiableSet(argSet)); 1469 } 1470 1471 1472 1473 /** 1474 * Adds the provided set of arguments as a required argument set, in which 1475 * at least one of the arguments must be provided. All of the arguments must 1476 * have already been registered with this argument parser using the 1477 * {@link #addArgument} method. 1478 * 1479 * @param arg1 The first argument to include in the required argument 1480 * set. It must not be {@code null}, and it must have 1481 * already been registered with this argument parser. 1482 * @param arg2 The second argument to include in the required argument 1483 * set. It must not be {@code null}, and it must have 1484 * already been registered with this argument parser. 1485 * @param remaining Any additional arguments to include in the required 1486 * argument set. It may be {@code null} or empty if no 1487 * additional required arguments are needed, but if it is 1488 * non-empty then all arguments must have already been 1489 * registered with this argument parser. 1490 */ 1491 public void addRequiredArgumentSet(final Argument arg1, final Argument arg2, 1492 final Argument... remaining) 1493 { 1494 Validator.ensureNotNull(arg1, arg2); 1495 1496 Validator.ensureTrue(namedArgs.contains(arg1), 1497 "The ArgumentParser.addRequiredArgumentSet method may only be " + 1498 "used if all of the provided arguments have already been " + 1499 "registered with the argument parser via the " + 1500 "ArgumentParser.addArgument method. The " + 1501 arg1.getIdentifierString() + " argument has not been " + 1502 "registered with the argument parser."); 1503 Validator.ensureTrue(namedArgs.contains(arg2), 1504 "The ArgumentParser.addRequiredArgumentSet method may only be " + 1505 "used if all of the provided arguments have already been " + 1506 "registered with the argument parser via the " + 1507 "ArgumentParser.addArgument method. The " + 1508 arg2.getIdentifierString() + " argument has not been " + 1509 "registered with the argument parser."); 1510 1511 if (remaining != null) 1512 { 1513 for (final Argument a : remaining) 1514 { 1515 Validator.ensureTrue(namedArgs.contains(a), 1516 "The ArgumentParser.addRequiredArgumentSet method may only be " + 1517 "used if all of the provided arguments have already been " + 1518 "registered with the argument parser via the " + 1519 "ArgumentParser.addArgument method. The " + 1520 a.getIdentifierString() + " argument has not been " + 1521 "registered with the argument parser."); 1522 } 1523 } 1524 1525 final LinkedHashSet<Argument> argSet = new LinkedHashSet<>(10); 1526 argSet.add(arg1); 1527 argSet.add(arg2); 1528 1529 if (remaining != null) 1530 { 1531 argSet.addAll(Arrays.asList(remaining)); 1532 } 1533 1534 requiredArgumentSets.add(Collections.unmodifiableSet(argSet)); 1535 } 1536 1537 1538 1539 /** 1540 * Indicates whether any subcommands have been registered with this argument 1541 * parser. 1542 * 1543 * @return {@code true} if one or more subcommands have been registered with 1544 * this argument parser, or {@code false} if not. 1545 */ 1546 public boolean hasSubCommands() 1547 { 1548 return (! subCommands.isEmpty()); 1549 } 1550 1551 1552 1553 /** 1554 * Retrieves the subcommand that was provided in the set of command-line 1555 * arguments, if any. 1556 * 1557 * @return The subcommand that was provided in the set of command-line 1558 * arguments, or {@code null} if there is none. 1559 */ 1560 public SubCommand getSelectedSubCommand() 1561 { 1562 return selectedSubCommand; 1563 } 1564 1565 1566 1567 /** 1568 * Specifies the subcommand that was provided in the set of command-line 1569 * arguments. 1570 * 1571 * @param subcommand The subcommand that was provided in the set of 1572 * command-line arguments. It may be {@code null} if no 1573 * subcommand should be used. 1574 */ 1575 void setSelectedSubCommand(final SubCommand subcommand) 1576 { 1577 selectedSubCommand = subcommand; 1578 if (subcommand != null) 1579 { 1580 subcommand.setPresent(); 1581 } 1582 } 1583 1584 1585 1586 /** 1587 * Retrieves a list of all subcommands associated with this argument parser. 1588 * 1589 * @return A list of all subcommands associated with this argument parser, or 1590 * an empty list if there are no associated subcommands. 1591 */ 1592 public List<SubCommand> getSubCommands() 1593 { 1594 return Collections.unmodifiableList(subCommands); 1595 } 1596 1597 1598 1599 /** 1600 * Retrieves the subcommand for the provided name. 1601 * 1602 * @param name The name of the subcommand to retrieve. 1603 * 1604 * @return The subcommand with the provided name, or {@code null} if there is 1605 * no such subcommand. 1606 */ 1607 public SubCommand getSubCommand(final String name) 1608 { 1609 if (name == null) 1610 { 1611 return null; 1612 } 1613 1614 return subCommandsByName.get(StaticUtils.toLowerCase(name)); 1615 } 1616 1617 1618 1619 /** 1620 * Registers the provided subcommand with this argument parser. 1621 * 1622 * @param subCommand The subcommand to register with this argument parser. 1623 * It must not be {@code null}. 1624 * 1625 * @throws ArgumentException If this argument parser does not allow 1626 * subcommands, if there is a conflict between any 1627 * of the names of the provided subcommand and an 1628 * already-registered subcommand, or if there is a 1629 * conflict between any of the subcommand-specific 1630 * arguments and global arguments. 1631 */ 1632 public void addSubCommand(final SubCommand subCommand) 1633 throws ArgumentException 1634 { 1635 // Ensure that the subcommand isn't already registered with an argument 1636 // parser. 1637 if (subCommand.getGlobalArgumentParser() != null) 1638 { 1639 throw new ArgumentException( 1640 ERR_PARSER_SUBCOMMAND_ALREADY_REGISTERED_WITH_PARSER.get()); 1641 } 1642 1643 // Ensure that the caller isn't trying to create a nested subcommand. 1644 if (parentSubCommand != null) 1645 { 1646 throw new ArgumentException( 1647 ERR_PARSER_CANNOT_CREATE_NESTED_SUBCOMMAND.get( 1648 parentSubCommand.getPrimaryName())); 1649 } 1650 1651 // Ensure that this argument parser doesn't allow trailing arguments. 1652 if (allowsTrailingArguments()) 1653 { 1654 throw new ArgumentException( 1655 ERR_PARSER_WITH_TRAILING_ARGS_CANNOT_HAVE_SUBCOMMANDS.get()); 1656 } 1657 1658 // Ensure that the subcommand doesn't have any names that conflict with an 1659 // existing subcommand. 1660 for (final String name : subCommand.getNames(true)) 1661 { 1662 if (subCommandsByName.containsKey(StaticUtils.toLowerCase(name))) 1663 { 1664 throw new ArgumentException( 1665 ERR_SUBCOMMAND_NAME_ALREADY_IN_USE.get(name)); 1666 } 1667 } 1668 1669 // Register the subcommand. 1670 for (final String name : subCommand.getNames(true)) 1671 { 1672 subCommandsByName.put(StaticUtils.toLowerCase(name), subCommand); 1673 } 1674 subCommands.add(subCommand); 1675 subCommand.setGlobalArgumentParser(this); 1676 } 1677 1678 1679 1680 /** 1681 * Registers the provided additional name for this subcommand. 1682 * 1683 * @param name The name to be registered. It must not be 1684 * {@code null} or empty. 1685 * @param subCommand The subcommand with which the name is associated. It 1686 * must not be {@code null}. 1687 * 1688 * @throws ArgumentException If the provided name is already in use. 1689 */ 1690 void addSubCommand(final String name, final SubCommand subCommand) 1691 throws ArgumentException 1692 { 1693 final String lowerName = StaticUtils.toLowerCase(name); 1694 if (subCommandsByName.containsKey(lowerName)) 1695 { 1696 throw new ArgumentException( 1697 ERR_SUBCOMMAND_NAME_ALREADY_IN_USE.get(name)); 1698 } 1699 1700 subCommandsByName.put(lowerName, subCommand); 1701 } 1702 1703 1704 1705 /** 1706 * Retrieves the set of unnamed trailing arguments in the provided command 1707 * line arguments. 1708 * 1709 * @return The set of unnamed trailing arguments in the provided command line 1710 * arguments, or an empty list if there were none. 1711 */ 1712 public List<String> getTrailingArguments() 1713 { 1714 return Collections.unmodifiableList(trailingArgs); 1715 } 1716 1717 1718 1719 /** 1720 * Clears the set of trailing arguments for this argument parser. 1721 */ 1722 void resetTrailingArguments() 1723 { 1724 trailingArgs.clear(); 1725 } 1726 1727 1728 1729 /** 1730 * Adds the provided value to the set of trailing arguments. 1731 * 1732 * @param value The value to add to the set of trailing arguments. 1733 * 1734 * @throws ArgumentException If the parser already has the maximum allowed 1735 * number of trailing arguments. 1736 */ 1737 void addTrailingArgument(final String value) 1738 throws ArgumentException 1739 { 1740 if ((maxTrailingArgs > 0) && (trailingArgs.size() >= maxTrailingArgs)) 1741 { 1742 throw new ArgumentException(ERR_PARSER_TOO_MANY_TRAILING_ARGS.get(value, 1743 commandName, maxTrailingArgs)); 1744 } 1745 1746 trailingArgs.add(value); 1747 } 1748 1749 1750 1751 /** 1752 * Retrieves the properties file that was used to obtain values for arguments 1753 * not set on the command line. 1754 * 1755 * @return The properties file that was used to obtain values for arguments 1756 * not set on the command line, or {@code null} if no properties file 1757 * was used. 1758 */ 1759 public File getPropertiesFileUsed() 1760 { 1761 return propertiesFileUsed; 1762 } 1763 1764 1765 1766 /** 1767 * Retrieves a list of the string representations of any arguments used for 1768 * the associated tool that were set from a properties file rather than 1769 * provided on the command line. The values of any arguments marked as 1770 * sensitive will be obscured. 1771 * 1772 * @return A list of the string representations any arguments used for the 1773 * associated tool that were set from a properties file rather than 1774 * provided on the command line, or an empty list if no arguments 1775 * were set from a properties file. 1776 */ 1777 public List<String> getArgumentsSetFromPropertiesFile() 1778 { 1779 return Collections.unmodifiableList(argumentsSetFromPropertiesFile); 1780 } 1781 1782 1783 1784 /** 1785 * Indicates whether the comment listing arguments obtained from a properties 1786 * file should be suppressed. 1787 * 1788 * @return {@code true} if the comment listing arguments obtained from a 1789 * properties file should be suppressed, or {@code false} if not. 1790 */ 1791 public boolean suppressPropertiesFileComment() 1792 { 1793 final BooleanArgument arg = 1794 getBooleanArgument(ARG_NAME_SUPPRESS_PROPERTIES_FILE_COMMENT); 1795 return ((arg != null) && arg.isPresent()); 1796 } 1797 1798 1799 1800 /** 1801 * Creates a copy of this argument parser that is "clean" and appears as if it 1802 * has not been used to parse an argument set. The new parser will have all 1803 * of the same arguments and constraints as this parser. 1804 * 1805 * @return The "clean" copy of this argument parser. 1806 */ 1807 public ArgumentParser getCleanCopy() 1808 { 1809 return new ArgumentParser(this, null); 1810 } 1811 1812 1813 1814 /** 1815 * Parses the provided set of arguments. 1816 * 1817 * @param args An array containing the argument information to parse. It 1818 * must not be {@code null}. 1819 * 1820 * @throws ArgumentException If a problem occurs while attempting to parse 1821 * the argument information. 1822 */ 1823 public void parse(final String[] args) 1824 throws ArgumentException 1825 { 1826 // Iterate through the provided args strings and process them. 1827 ArgumentParser subCommandParser = null; 1828 boolean inTrailingArgs = false; 1829 boolean skipFinalValidation = false; 1830 String subCommandName = null; 1831 for (int i=0; i < args.length; i++) 1832 { 1833 final String s = args[i]; 1834 1835 if (inTrailingArgs) 1836 { 1837 if (maxTrailingArgs == 0) 1838 { 1839 throw new ArgumentException(ERR_PARSER_TRAILING_ARGS_NOT_ALLOWED.get( 1840 s, commandName)); 1841 } 1842 else if (trailingArgs.size() >= maxTrailingArgs) 1843 { 1844 throw new ArgumentException(ERR_PARSER_TOO_MANY_TRAILING_ARGS.get(s, 1845 commandName, maxTrailingArgs)); 1846 } 1847 else 1848 { 1849 trailingArgs.add(s); 1850 } 1851 } 1852 else if (s.equals("--")) 1853 { 1854 // This signifies the end of the named arguments and the beginning of 1855 // the trailing arguments. 1856 inTrailingArgs = true; 1857 } 1858 else if (s.startsWith("--")) 1859 { 1860 // There may be an equal sign to separate the name from the value. 1861 final String argName; 1862 final int equalPos = s.indexOf('='); 1863 if (equalPos > 0) 1864 { 1865 argName = s.substring(2, equalPos); 1866 } 1867 else 1868 { 1869 argName = s.substring(2); 1870 } 1871 1872 final String lowerName = StaticUtils.toLowerCase(argName); 1873 Argument a = namedArgsByLongID.get(lowerName); 1874 if ((a == null) && (subCommandParser != null)) 1875 { 1876 a = subCommandParser.namedArgsByLongID.get(lowerName); 1877 } 1878 1879 if (a == null) 1880 { 1881 throw new ArgumentException(ERR_PARSER_NO_SUCH_LONG_ID.get(argName)); 1882 } 1883 else if (a.isUsageArgument()) 1884 { 1885 skipFinalValidation |= skipFinalValidationBecauseOfArgument(a); 1886 } 1887 1888 a.incrementOccurrences(); 1889 if (a.takesValue()) 1890 { 1891 if (equalPos > 0) 1892 { 1893 a.addValue(s.substring(equalPos+1)); 1894 } 1895 else 1896 { 1897 i++; 1898 if (i >= args.length) 1899 { 1900 throw new ArgumentException(ERR_PARSER_LONG_ARG_MISSING_VALUE.get( 1901 argName)); 1902 } 1903 else 1904 { 1905 a.addValue(args[i]); 1906 } 1907 } 1908 } 1909 else 1910 { 1911 if (equalPos > 0) 1912 { 1913 throw new ArgumentException( 1914 ERR_PARSER_LONG_ARG_DOESNT_TAKE_VALUE.get(argName)); 1915 } 1916 } 1917 } 1918 else if (s.startsWith("-")) 1919 { 1920 if (s.length() == 1) 1921 { 1922 throw new ArgumentException(ERR_PARSER_UNEXPECTED_DASH.get()); 1923 } 1924 else if (s.length() == 2) 1925 { 1926 final char c = s.charAt(1); 1927 1928 Argument a = namedArgsByShortID.get(c); 1929 if ((a == null) && (subCommandParser != null)) 1930 { 1931 a = subCommandParser.namedArgsByShortID.get(c); 1932 } 1933 1934 if (a == null) 1935 { 1936 throw new ArgumentException(ERR_PARSER_NO_SUCH_SHORT_ID.get(c)); 1937 } 1938 else if (a.isUsageArgument()) 1939 { 1940 skipFinalValidation |= skipFinalValidationBecauseOfArgument(a); 1941 } 1942 1943 a.incrementOccurrences(); 1944 if (a.takesValue()) 1945 { 1946 i++; 1947 if (i >= args.length) 1948 { 1949 throw new ArgumentException( 1950 ERR_PARSER_SHORT_ARG_MISSING_VALUE.get(c)); 1951 } 1952 else 1953 { 1954 a.addValue(args[i]); 1955 } 1956 } 1957 } 1958 else 1959 { 1960 char c = s.charAt(1); 1961 Argument a = namedArgsByShortID.get(c); 1962 if ((a == null) && (subCommandParser != null)) 1963 { 1964 a = subCommandParser.namedArgsByShortID.get(c); 1965 } 1966 1967 if (a == null) 1968 { 1969 throw new ArgumentException(ERR_PARSER_NO_SUCH_SHORT_ID.get(c)); 1970 } 1971 else if (a.isUsageArgument()) 1972 { 1973 skipFinalValidation |= skipFinalValidationBecauseOfArgument(a); 1974 } 1975 1976 a.incrementOccurrences(); 1977 if (a.takesValue()) 1978 { 1979 a.addValue(s.substring(2)); 1980 } 1981 else 1982 { 1983 // The rest of the characters in the string must also resolve to 1984 // arguments that don't take values. 1985 for (int j=2; j < s.length(); j++) 1986 { 1987 c = s.charAt(j); 1988 a = namedArgsByShortID.get(c); 1989 if ((a == null) && (subCommandParser != null)) 1990 { 1991 a = subCommandParser.namedArgsByShortID.get(c); 1992 } 1993 1994 if (a == null) 1995 { 1996 throw new ArgumentException( 1997 ERR_PARSER_NO_SUBSEQUENT_SHORT_ARG.get(c, s)); 1998 } 1999 else if (a.isUsageArgument()) 2000 { 2001 skipFinalValidation |= skipFinalValidationBecauseOfArgument(a); 2002 } 2003 2004 a.incrementOccurrences(); 2005 if (a.takesValue()) 2006 { 2007 throw new ArgumentException( 2008 ERR_PARSER_SUBSEQUENT_SHORT_ARG_TAKES_VALUE.get( 2009 c, s)); 2010 } 2011 } 2012 } 2013 } 2014 } 2015 else if (subCommands.isEmpty()) 2016 { 2017 inTrailingArgs = true; 2018 if (maxTrailingArgs == 0) 2019 { 2020 throw new ArgumentException(ERR_PARSER_TRAILING_ARGS_NOT_ALLOWED.get( 2021 s, commandName)); 2022 } 2023 else 2024 { 2025 trailingArgs.add(s); 2026 } 2027 } 2028 else 2029 { 2030 if (selectedSubCommand == null) 2031 { 2032 subCommandName = s; 2033 selectedSubCommand = 2034 subCommandsByName.get(StaticUtils.toLowerCase(s)); 2035 if (selectedSubCommand == null) 2036 { 2037 throw new ArgumentException(ERR_PARSER_NO_SUCH_SUBCOMMAND.get(s, 2038 commandName)); 2039 } 2040 else 2041 { 2042 selectedSubCommand.setPresent(); 2043 subCommandParser = selectedSubCommand.getArgumentParser(); 2044 } 2045 } 2046 else 2047 { 2048 throw new ArgumentException(ERR_PARSER_CONFLICTING_SUBCOMMANDS.get( 2049 subCommandName, s)); 2050 } 2051 } 2052 } 2053 2054 2055 // Perform any appropriate processing related to the use of a properties 2056 // file. 2057 if (! handlePropertiesFile()) 2058 { 2059 return; 2060 } 2061 2062 2063 // If a usage argument was provided, then no further validation should be 2064 // performed. 2065 if (skipFinalValidation) 2066 { 2067 return; 2068 } 2069 2070 2071 // If any subcommands are defined, then one must have been provided. 2072 if ((! subCommands.isEmpty()) && (selectedSubCommand == null)) 2073 { 2074 throw new ArgumentException( 2075 ERR_PARSER_MISSING_SUBCOMMAND.get(commandName)); 2076 } 2077 2078 2079 doFinalValidation(this); 2080 if (selectedSubCommand != null) 2081 { 2082 doFinalValidation(selectedSubCommand.getArgumentParser()); 2083 } 2084 } 2085 2086 2087 2088 /** 2089 * Performs the final validation for the provided argument parser. 2090 * 2091 * @param parser The argument parser for which to perform the final 2092 * validation. 2093 * 2094 * @throws ArgumentException If a validation problem is encountered. 2095 */ 2096 private static void doFinalValidation(final ArgumentParser parser) 2097 throws ArgumentException 2098 { 2099 // Make sure that all required arguments have values. 2100 for (final Argument a : parser.namedArgs) 2101 { 2102 if (a.isRequired() && (! a.isPresent())) 2103 { 2104 throw new ArgumentException(ERR_PARSER_MISSING_REQUIRED_ARG.get( 2105 a.getIdentifierString())); 2106 } 2107 } 2108 2109 2110 // Make sure that at least the minimum number of trailing arguments were 2111 // provided. 2112 if (parser.trailingArgs.size() < parser.minTrailingArgs) 2113 { 2114 throw new ArgumentException(ERR_PARSER_NOT_ENOUGH_TRAILING_ARGS.get( 2115 parser.commandName, parser.minTrailingArgs, 2116 parser.trailingArgsPlaceholder)); 2117 } 2118 2119 2120 // Make sure that there are no dependent argument set conflicts. 2121 for (final ObjectPair<Argument,Set<Argument>> p : 2122 parser.dependentArgumentSets) 2123 { 2124 final Argument targetArg = p.getFirst(); 2125 if (targetArg.getNumOccurrences() > 0) 2126 { 2127 final Set<Argument> argSet = p.getSecond(); 2128 boolean found = false; 2129 for (final Argument a : argSet) 2130 { 2131 if (a.getNumOccurrences() > 0) 2132 { 2133 found = true; 2134 break; 2135 } 2136 } 2137 2138 if (! found) 2139 { 2140 if (argSet.size() == 1) 2141 { 2142 throw new ArgumentException( 2143 ERR_PARSER_DEPENDENT_CONFLICT_SINGLE.get( 2144 targetArg.getIdentifierString(), 2145 argSet.iterator().next().getIdentifierString())); 2146 } 2147 else 2148 { 2149 boolean first = true; 2150 final StringBuilder buffer = new StringBuilder(); 2151 for (final Argument a : argSet) 2152 { 2153 if (first) 2154 { 2155 first = false; 2156 } 2157 else 2158 { 2159 buffer.append(", "); 2160 } 2161 buffer.append(a.getIdentifierString()); 2162 } 2163 throw new ArgumentException( 2164 ERR_PARSER_DEPENDENT_CONFLICT_MULTIPLE.get( 2165 targetArg.getIdentifierString(), buffer.toString())); 2166 } 2167 } 2168 } 2169 } 2170 2171 2172 // Make sure that there are no exclusive argument set conflicts. 2173 for (final Set<Argument> argSet : parser.exclusiveArgumentSets) 2174 { 2175 Argument setArg = null; 2176 for (final Argument a : argSet) 2177 { 2178 if (a.getNumOccurrences() > 0) 2179 { 2180 if (setArg == null) 2181 { 2182 setArg = a; 2183 } 2184 else 2185 { 2186 throw new ArgumentException(ERR_PARSER_EXCLUSIVE_CONFLICT.get( 2187 setArg.getIdentifierString(), 2188 a.getIdentifierString())); 2189 } 2190 } 2191 } 2192 } 2193 2194 // Make sure that there are no required argument set conflicts. 2195 for (final Set<Argument> argSet : parser.requiredArgumentSets) 2196 { 2197 boolean found = false; 2198 for (final Argument a : argSet) 2199 { 2200 if (a.getNumOccurrences() > 0) 2201 { 2202 found = true; 2203 break; 2204 } 2205 } 2206 2207 if (! found) 2208 { 2209 boolean first = true; 2210 final StringBuilder buffer = new StringBuilder(); 2211 for (final Argument a : argSet) 2212 { 2213 if (first) 2214 { 2215 first = false; 2216 } 2217 else 2218 { 2219 buffer.append(", "); 2220 } 2221 buffer.append(a.getIdentifierString()); 2222 } 2223 throw new ArgumentException(ERR_PARSER_REQUIRED_CONFLICT.get( 2224 buffer.toString())); 2225 } 2226 } 2227 } 2228 2229 2230 2231 /** 2232 * Indicates whether the provided argument is one that indicates that the 2233 * parser should skip all validation except that performed when assigning 2234 * values from command-line arguments. Validation that will be skipped 2235 * includes ensuring that all required arguments have values, ensuring that 2236 * the minimum number of trailing arguments were provided, and ensuring that 2237 * there were no dependent/exclusive/required argument set conflicts. 2238 * 2239 * @param a The argument for which to make the determination. 2240 * 2241 * @return {@code true} if the provided argument is one that indicates that 2242 * final validation should be skipped, or {@code false} if not. 2243 */ 2244 private static boolean skipFinalValidationBecauseOfArgument(final Argument a) 2245 { 2246 // We will skip final validation for all usage arguments except the ones 2247 // used for interacting with properties and output files. 2248 if (ARG_NAME_PROPERTIES_FILE_PATH.equals(a.getLongIdentifier()) || 2249 ARG_NAME_NO_PROPERTIES_FILE.equals(a.getLongIdentifier()) || 2250 ARG_NAME_SUPPRESS_PROPERTIES_FILE_COMMENT.equals( 2251 a.getLongIdentifier()) || 2252 ARG_NAME_OUTPUT_FILE.equals(a.getLongIdentifier()) || 2253 ARG_NAME_TEE_OUTPUT.equals(a.getLongIdentifier())) 2254 { 2255 return false; 2256 } 2257 2258 return a.isUsageArgument(); 2259 } 2260 2261 2262 2263 /** 2264 * Performs any appropriate properties file processing for this argument 2265 * parser. 2266 * 2267 * @return {@code true} if the tool should continue processing, or 2268 * {@code false} if it should return immediately. 2269 * 2270 * @throws ArgumentException If a problem is encountered while attempting 2271 * to parse a properties file or update arguments 2272 * with the values contained in it. 2273 */ 2274 private boolean handlePropertiesFile() 2275 throws ArgumentException 2276 { 2277 final BooleanArgument noPropertiesFile; 2278 final FileArgument generatePropertiesFile; 2279 final FileArgument propertiesFilePath; 2280 try 2281 { 2282 propertiesFilePath = getFileArgument(ARG_NAME_PROPERTIES_FILE_PATH); 2283 generatePropertiesFile = 2284 getFileArgument(ARG_NAME_GENERATE_PROPERTIES_FILE); 2285 noPropertiesFile = getBooleanArgument(ARG_NAME_NO_PROPERTIES_FILE); 2286 } 2287 catch (final Exception e) 2288 { 2289 Debug.debugException(e); 2290 2291 // This should only ever happen if the argument parser has an argument 2292 // with a name that conflicts with one of the properties file arguments 2293 // but isn't of the right type. In this case, we'll assume that no 2294 // properties file will be used. 2295 return true; 2296 } 2297 2298 2299 // If any of the properties file arguments isn't defined, then we'll assume 2300 // that no properties file will be used. 2301 if ((propertiesFilePath == null) || (generatePropertiesFile == null) || 2302 (noPropertiesFile == null)) 2303 { 2304 return true; 2305 } 2306 2307 2308 // If the noPropertiesFile argument is present, then don't do anything but 2309 // make sure that neither of the other arguments was specified. 2310 if (noPropertiesFile.isPresent()) 2311 { 2312 if (propertiesFilePath.isPresent()) 2313 { 2314 throw new ArgumentException(ERR_PARSER_EXCLUSIVE_CONFLICT.get( 2315 noPropertiesFile.getIdentifierString(), 2316 propertiesFilePath.getIdentifierString())); 2317 } 2318 else if (generatePropertiesFile.isPresent()) 2319 { 2320 throw new ArgumentException(ERR_PARSER_EXCLUSIVE_CONFLICT.get( 2321 noPropertiesFile.getIdentifierString(), 2322 generatePropertiesFile.getIdentifierString())); 2323 } 2324 else 2325 { 2326 return true; 2327 } 2328 } 2329 2330 2331 // If the generatePropertiesFile argument is present, then make sure the 2332 // propertiesFilePath argument is not set and generate the output. 2333 if (generatePropertiesFile.isPresent()) 2334 { 2335 if (propertiesFilePath.isPresent()) 2336 { 2337 throw new ArgumentException(ERR_PARSER_EXCLUSIVE_CONFLICT.get( 2338 generatePropertiesFile.getIdentifierString(), 2339 propertiesFilePath.getIdentifierString())); 2340 } 2341 else 2342 { 2343 generatePropertiesFile( 2344 generatePropertiesFile.getValue().getAbsolutePath()); 2345 return false; 2346 } 2347 } 2348 2349 2350 // If the propertiesFilePath argument is present, then try to make use of 2351 // the specified file. 2352 if (propertiesFilePath.isPresent()) 2353 { 2354 final File propertiesFile = propertiesFilePath.getValue(); 2355 if (propertiesFile.exists() && propertiesFile.isFile()) 2356 { 2357 handlePropertiesFile(propertiesFilePath.getValue()); 2358 } 2359 else 2360 { 2361 throw new ArgumentException( 2362 ERR_PARSER_NO_SUCH_PROPERTIES_FILE.get( 2363 propertiesFilePath.getIdentifierString(), 2364 propertiesFile.getAbsolutePath())); 2365 } 2366 return true; 2367 } 2368 2369 2370 // We may still use a properties file if the path was specified in either a 2371 // JVM property or an environment variable. If both are defined, the JVM 2372 // property will take precedence. If a property or environment variable 2373 // specifies an invalid value, then we'll just ignore it. 2374 String path = System.getProperty(PROPERTY_DEFAULT_PROPERTIES_FILE_PATH); 2375 if (path == null) 2376 { 2377 path = System.getenv(ENV_DEFAULT_PROPERTIES_FILE_PATH); 2378 } 2379 2380 if (path != null) 2381 { 2382 final File propertiesFile = new File(path); 2383 if (propertiesFile.exists() && propertiesFile.isFile()) 2384 { 2385 handlePropertiesFile(propertiesFile); 2386 } 2387 } 2388 2389 return true; 2390 } 2391 2392 2393 2394 /** 2395 * Write an empty properties file for this argument parser to the specified 2396 * path. 2397 * 2398 * @param path The path to the properties file to be written. 2399 * 2400 * @throws ArgumentException If a problem is encountered while writing the 2401 * properties file. 2402 */ 2403 private void generatePropertiesFile(final String path) 2404 throws ArgumentException 2405 { 2406 final PrintWriter w; 2407 try 2408 { 2409 // The java.util.Properties specification states that properties files 2410 // should be read using the ISO 8859-1 character set. 2411 w = new PrintWriter(new OutputStreamWriter(new FileOutputStream(path), 2412 StandardCharsets.ISO_8859_1)); 2413 } 2414 catch (final Exception e) 2415 { 2416 Debug.debugException(e); 2417 throw new ArgumentException( 2418 ERR_PARSER_GEN_PROPS_CANNOT_OPEN_FILE.get(path, 2419 StaticUtils.getExceptionMessage(e)), 2420 e); 2421 } 2422 2423 try 2424 { 2425 wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_1.get(commandName)); 2426 w.println('#'); 2427 wrapComment(w, 2428 INFO_PARSER_GEN_PROPS_HEADER_2.get(commandName, 2429 ARG_NAME_PROPERTIES_FILE_PATH, 2430 PROPERTY_DEFAULT_PROPERTIES_FILE_PATH, 2431 ENV_DEFAULT_PROPERTIES_FILE_PATH, ARG_NAME_NO_PROPERTIES_FILE)); 2432 w.println('#'); 2433 wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_3.get()); 2434 w.println('#'); 2435 2436 wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_4.get()); 2437 w.println('#'); 2438 wrapComment(w, INFO_PARSER_GEN_PROPS_HEADER_5.get(commandName)); 2439 2440 for (final Argument a : getNamedArguments()) 2441 { 2442 writeArgumentProperties(w, null, a); 2443 } 2444 2445 for (final SubCommand sc : getSubCommands()) 2446 { 2447 for (final Argument a : sc.getArgumentParser().getNamedArguments()) 2448 { 2449 writeArgumentProperties(w, sc, a); 2450 } 2451 } 2452 } 2453 finally 2454 { 2455 w.close(); 2456 } 2457 } 2458 2459 2460 2461 /** 2462 * Writes information about the provided argument to the given writer. 2463 * 2464 * @param w The writer to which the properties should be written. It must 2465 * not be {@code null}. 2466 * @param sc The subcommand with which the argument is associated. It may 2467 * be {@code null} if the provided argument is a global argument. 2468 * @param a The argument for which to write the properties. It must not be 2469 * {@code null}. 2470 */ 2471 private void writeArgumentProperties(final PrintWriter w, 2472 final SubCommand sc, 2473 final Argument a) 2474 { 2475 if (a.isUsageArgument() || a.isHidden()) 2476 { 2477 return; 2478 } 2479 2480 w.println(); 2481 w.println(); 2482 wrapComment(w, a.getDescription()); 2483 w.println('#'); 2484 2485 final String constraints = a.getValueConstraints(); 2486 if ((constraints != null) && (! constraints.isEmpty()) && 2487 (! (a instanceof BooleanArgument))) 2488 { 2489 wrapComment(w, constraints); 2490 w.println('#'); 2491 } 2492 2493 final String identifier; 2494 if (a.getLongIdentifier() != null) 2495 { 2496 identifier = a.getLongIdentifier(); 2497 } 2498 else 2499 { 2500 identifier = a.getIdentifierString(); 2501 } 2502 2503 String placeholder = a.getValuePlaceholder(); 2504 if (placeholder == null) 2505 { 2506 if (a instanceof BooleanArgument) 2507 { 2508 placeholder = "{true|false}"; 2509 } 2510 else 2511 { 2512 placeholder = ""; 2513 } 2514 } 2515 2516 final String propertyName; 2517 if (sc == null) 2518 { 2519 propertyName = commandName + '.' + identifier; 2520 } 2521 else 2522 { 2523 propertyName = commandName + '.' + sc.getPrimaryName() + '.' + identifier; 2524 } 2525 2526 w.println("# " + propertyName + '=' + placeholder); 2527 2528 if (a.isPresent()) 2529 { 2530 for (final String s : a.getValueStringRepresentations(false)) 2531 { 2532 w.println(propertyName + '=' + s); 2533 } 2534 } 2535 } 2536 2537 2538 2539 /** 2540 * Wraps the given string and writes it as a comment to the provided writer. 2541 * 2542 * @param w The writer to use to write the wrapped and commented string. 2543 * @param s The string to be wrapped and written. 2544 */ 2545 private static void wrapComment(final PrintWriter w, final String s) 2546 { 2547 for (final String line : StaticUtils.wrapLine(s, 77)) 2548 { 2549 w.println("# " + line); 2550 } 2551 } 2552 2553 2554 2555 /** 2556 * Reads the contents of the specified properties file and updates the 2557 * configured arguments as appropriate. 2558 * 2559 * @param propertiesFile The properties file to process. 2560 * 2561 * @throws ArgumentException If a problem is encountered while examining the 2562 * properties file, or while trying to assign a 2563 * property value to a corresponding argument. 2564 */ 2565 private void handlePropertiesFile(final File propertiesFile) 2566 throws ArgumentException 2567 { 2568 final String propertiesFilePath = propertiesFile.getAbsolutePath(); 2569 2570 final BufferedReader reader; 2571 try 2572 { 2573 // The java.util.Properties specification states that properties files 2574 // should be read using the ISO 8859-1 character set, and that characters 2575 // that cannot be encoded in that format should be represented using 2576 // Unicode escapes that start with a backslash, a lowercase letter "u", 2577 // and four hexadecimal digits. To provide compatibility with the Java 2578 // Properties file format (except we also support the same property 2579 // appearing multiple times), we will also use that encoding and will 2580 // support Unicode escape sequences. 2581 reader = new BufferedReader(new InputStreamReader( 2582 new FileInputStream(propertiesFile), StandardCharsets.ISO_8859_1)); 2583 } 2584 catch (final Exception e) 2585 { 2586 Debug.debugException(e); 2587 throw new ArgumentException( 2588 ERR_PARSER_CANNOT_OPEN_PROP_FILE.get(propertiesFilePath, 2589 StaticUtils.getExceptionMessage(e)), 2590 e); 2591 } 2592 2593 try 2594 { 2595 // Read all of the lines of the file, ignoring comments and unwrapping 2596 // properties that span multiple lines. 2597 boolean lineIsContinued = false; 2598 int lineNumber = 0; 2599 final ArrayList<ObjectPair<Integer,StringBuilder>> propertyLines = 2600 new ArrayList<>(10); 2601 while (true) 2602 { 2603 String line; 2604 try 2605 { 2606 line = reader.readLine(); 2607 lineNumber++; 2608 } 2609 catch (final Exception e) 2610 { 2611 Debug.debugException(e); 2612 throw new ArgumentException( 2613 ERR_PARSER_ERROR_READING_PROP_FILE.get(propertiesFilePath, 2614 StaticUtils.getExceptionMessage(e)), 2615 e); 2616 } 2617 2618 2619 // If the line is null, then we've reached the end of the file. If we 2620 // expect a previous line to have been continued, then this is an error. 2621 if (line == null) 2622 { 2623 if (lineIsContinued) 2624 { 2625 throw new ArgumentException( 2626 ERR_PARSER_PROP_FILE_MISSING_CONTINUATION.get( 2627 (lineNumber-1), propertiesFilePath)); 2628 } 2629 break; 2630 } 2631 2632 2633 // See if the line has any leading whitespace, and if so then trim it 2634 // off. If there is leading whitespace, then make sure that we expect 2635 // the previous line to be continued. 2636 final int initialLength = line.length(); 2637 line = StaticUtils.trimLeading(line); 2638 final boolean hasLeadingWhitespace = (line.length() < initialLength); 2639 if (hasLeadingWhitespace && (! lineIsContinued)) 2640 { 2641 throw new ArgumentException( 2642 ERR_PARSER_PROP_FILE_UNEXPECTED_LEADING_SPACE.get( 2643 propertiesFilePath, lineNumber)); 2644 } 2645 2646 2647 // If the line is empty or starts with "#", then skip it. But make sure 2648 // we didn't expect the previous line to be continued. 2649 if ((line.isEmpty()) || line.startsWith("#")) 2650 { 2651 if (lineIsContinued) 2652 { 2653 throw new ArgumentException( 2654 ERR_PARSER_PROP_FILE_MISSING_CONTINUATION.get( 2655 (lineNumber-1), propertiesFilePath)); 2656 } 2657 continue; 2658 } 2659 2660 2661 // See if the line ends with a backslash and if so then trim it off. 2662 final boolean hasTrailingBackslash = line.endsWith("\\"); 2663 if (line.endsWith("\\")) 2664 { 2665 line = line.substring(0, (line.length() - 1)); 2666 } 2667 2668 2669 // If the previous line needs to be continued, then append the new line 2670 // to it. Otherwise, add it as a new line. 2671 if (lineIsContinued) 2672 { 2673 propertyLines.get(propertyLines.size() - 1).getSecond().append(line); 2674 } 2675 else 2676 { 2677 propertyLines.add( 2678 new ObjectPair<>(lineNumber, new StringBuilder(line))); 2679 } 2680 2681 lineIsContinued = hasTrailingBackslash; 2682 } 2683 2684 2685 // Parse all of the lines into a map of identifiers and their 2686 // corresponding values. 2687 propertiesFileUsed = propertiesFile; 2688 if (propertyLines.isEmpty()) 2689 { 2690 return; 2691 } 2692 2693 final HashMap<String,ArrayList<String>> propertyMap = 2694 new HashMap<>(propertyLines.size()); 2695 for (final ObjectPair<Integer,StringBuilder> p : propertyLines) 2696 { 2697 lineNumber = p.getFirst(); 2698 final String line = handleUnicodeEscapes(propertiesFilePath, lineNumber, 2699 p.getSecond()); 2700 final int equalPos = line.indexOf('='); 2701 if (equalPos <= 0) 2702 { 2703 throw new ArgumentException(ERR_PARSER_MALFORMED_PROP_LINE.get( 2704 propertiesFilePath, lineNumber, line)); 2705 } 2706 2707 final String propertyName = line.substring(0, equalPos).trim(); 2708 final String propertyValue = line.substring(equalPos+1).trim(); 2709 if (propertyValue.isEmpty()) 2710 { 2711 // The property doesn't have a value, so we can ignore it. 2712 continue; 2713 } 2714 2715 2716 // An argument can have multiple identifiers, and we will allow any of 2717 // them to be used to reference it. To deal with this, we'll map the 2718 // argument identifier to its corresponding argument and then use the 2719 // preferred identifier for that argument in the map. The same applies 2720 // to subcommand names. 2721 boolean prefixedWithToolName = false; 2722 boolean prefixedWithSubCommandName = false; 2723 Argument a = getNamedArgument(propertyName); 2724 if (a == null) 2725 { 2726 // It could be that the argument name was prefixed with the tool name. 2727 // Check to see if that was the case. 2728 if (propertyName.startsWith(commandName + '.')) 2729 { 2730 prefixedWithToolName = true; 2731 2732 String basePropertyName = 2733 propertyName.substring(commandName.length()+1); 2734 a = getNamedArgument(basePropertyName); 2735 2736 if (a == null) 2737 { 2738 final int periodPos = basePropertyName.indexOf('.'); 2739 if (periodPos > 0) 2740 { 2741 final String subCommandName = 2742 basePropertyName.substring(0, periodPos); 2743 if ((selectedSubCommand != null) && 2744 selectedSubCommand.hasName(subCommandName)) 2745 { 2746 prefixedWithSubCommandName = true; 2747 basePropertyName = basePropertyName.substring(periodPos+1); 2748 a = selectedSubCommand.getArgumentParser().getNamedArgument( 2749 basePropertyName); 2750 } 2751 } 2752 else if (selectedSubCommand != null) 2753 { 2754 a = selectedSubCommand.getArgumentParser().getNamedArgument( 2755 basePropertyName); 2756 } 2757 } 2758 } 2759 else if (selectedSubCommand != null) 2760 { 2761 a = selectedSubCommand.getArgumentParser().getNamedArgument( 2762 propertyName); 2763 } 2764 } 2765 2766 if (a == null) 2767 { 2768 // This could mean that there's a typo in the property name, but it's 2769 // more likely the case that the property is for a different tool. In 2770 // either case, we'll ignore it. 2771 continue; 2772 } 2773 2774 final String canonicalPropertyName; 2775 if (prefixedWithToolName) 2776 { 2777 if (prefixedWithSubCommandName) 2778 { 2779 canonicalPropertyName = commandName + '.' + 2780 selectedSubCommand.getPrimaryName() + '.' + 2781 a.getIdentifierString(); 2782 } 2783 else 2784 { 2785 canonicalPropertyName = commandName + '.' + a.getIdentifierString(); 2786 } 2787 } 2788 else 2789 { 2790 canonicalPropertyName = a.getIdentifierString(); 2791 } 2792 2793 ArrayList<String> valueList = propertyMap.get(canonicalPropertyName); 2794 if (valueList == null) 2795 { 2796 valueList = new ArrayList<>(5); 2797 propertyMap.put(canonicalPropertyName, valueList); 2798 } 2799 valueList.add(propertyValue); 2800 } 2801 2802 2803 // Iterate through all of the named arguments for the argument parser and 2804 // see if we should use the properties to assign values to any of the 2805 // arguments that weren't provided on the command line. 2806 setArgsFromPropertiesFile(propertyMap, false); 2807 2808 2809 // If there is a selected subcommand, then iterate through all of its 2810 // arguments. 2811 if (selectedSubCommand != null) 2812 { 2813 setArgsFromPropertiesFile(propertyMap, true); 2814 } 2815 } 2816 finally 2817 { 2818 try 2819 { 2820 reader.close(); 2821 } 2822 catch (final Exception e) 2823 { 2824 Debug.debugException(e); 2825 } 2826 } 2827 } 2828 2829 2830 2831 /** 2832 * Retrieves a string that contains the contents of the provided buffer, but 2833 * with any Unicode escape sequences converted to the appropriate character 2834 * representation. 2835 * 2836 * @param propertiesFilePath The path to the properties file 2837 * @param lineNumber The line number on which the property definition 2838 * starts. 2839 * @param buffer The buffer containing the data to be processed. It 2840 * must not be {@code null} but may be empty. 2841 * 2842 * @return A string that contains the contents of the provided buffer, but 2843 * with any Unicode escape sequences converted to the appropriate 2844 * character representation. 2845 * 2846 * @throws ArgumentException If a malformed Unicode escape sequence is 2847 * encountered. 2848 */ 2849 static String handleUnicodeEscapes(final String propertiesFilePath, 2850 final int lineNumber, 2851 final StringBuilder buffer) 2852 throws ArgumentException 2853 { 2854 int pos = 0; 2855 while (pos < buffer.length()) 2856 { 2857 final char c = buffer.charAt(pos); 2858 if (c == '\\') 2859 { 2860 if (pos <= (buffer.length() - 5)) 2861 { 2862 final char nextChar = buffer.charAt(pos+1); 2863 if ((nextChar == 'u') || (nextChar == 'U')) 2864 { 2865 try 2866 { 2867 final String hexDigits = buffer.substring(pos+2, pos+6); 2868 final byte[] bytes = StaticUtils.fromHex(hexDigits); 2869 final int i = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF); 2870 buffer.setCharAt(pos, (char) i); 2871 for (int j=0; j < 5; j++) 2872 { 2873 buffer.deleteCharAt(pos+1); 2874 } 2875 } 2876 catch (final Exception e) 2877 { 2878 Debug.debugException(e); 2879 throw new ArgumentException( 2880 ERR_PARSER_MALFORMED_UNICODE_ESCAPE.get(propertiesFilePath, 2881 lineNumber), 2882 e); 2883 } 2884 } 2885 else 2886 { 2887 pos++; 2888 } 2889 } 2890 } 2891 2892 pos++; 2893 } 2894 2895 return buffer.toString(); 2896 } 2897 2898 2899 2900 /** 2901 * Sets the values of any arguments not provided on the command line but 2902 * defined in the properties file. 2903 * 2904 * @param propertyMap A map of properties read from the properties file. 2905 * @param useSubCommand Indicates whether to use the argument parser 2906 * associated with the selected subcommand rather than 2907 * the global argument parser. 2908 * 2909 * @throws ArgumentException If a problem is encountered while examining the 2910 * properties file, or while trying to assign a 2911 * property value to a corresponding argument. 2912 */ 2913 private void setArgsFromPropertiesFile( 2914 final Map<String,ArrayList<String>> propertyMap, 2915 final boolean useSubCommand) 2916 throws ArgumentException 2917 { 2918 final ArgumentParser p; 2919 if (useSubCommand) 2920 { 2921 p = selectedSubCommand.getArgumentParser(); 2922 } 2923 else 2924 { 2925 p = this; 2926 } 2927 2928 2929 for (final Argument a : p.namedArgs) 2930 { 2931 // If the argument was provided on the command line, then that will always 2932 // override anything that might be in the properties file. 2933 if (a.getNumOccurrences() > 0) 2934 { 2935 continue; 2936 } 2937 2938 2939 // If the argument is part of an exclusive argument set, and if one of 2940 // the other arguments in that set was provided on the command line, then 2941 // don't look in the properties file for a value for the argument. 2942 boolean exclusiveArgumentHasValue = false; 2943exclusiveArgumentLoop: 2944 for (final Set<Argument> exclusiveArgumentSet : exclusiveArgumentSets) 2945 { 2946 if (exclusiveArgumentSet.contains(a)) 2947 { 2948 for (final Argument exclusiveArg : exclusiveArgumentSet) 2949 { 2950 if (exclusiveArg.getNumOccurrences() > 0) 2951 { 2952 exclusiveArgumentHasValue = true; 2953 break exclusiveArgumentLoop; 2954 } 2955 } 2956 } 2957 } 2958 2959 if (exclusiveArgumentHasValue) 2960 { 2961 continue; 2962 } 2963 2964 2965 // If we should use a subcommand, then see if the properties file has a 2966 // property that is specific to the selected subcommand. Then fall back 2967 // to a property that is specific to the tool, and finally fall back to 2968 // checking for a set of values that are generic to any tool that has an 2969 // argument with that name. 2970 List<String> values = null; 2971 if (useSubCommand) 2972 { 2973 values = propertyMap.get(commandName + '.' + 2974 selectedSubCommand.getPrimaryName() + '.' + 2975 a.getIdentifierString()); 2976 } 2977 2978 if (values == null) 2979 { 2980 values = propertyMap.get(commandName + '.' + a.getIdentifierString()); 2981 } 2982 2983 if (values == null) 2984 { 2985 values = propertyMap.get(a.getIdentifierString()); 2986 } 2987 2988 if (values != null) 2989 { 2990 for (final String value : values) 2991 { 2992 if (a instanceof BooleanArgument) 2993 { 2994 // We'll treat this as a BooleanValueArgument. 2995 final BooleanValueArgument bva = new BooleanValueArgument( 2996 a.getShortIdentifier(), a.getLongIdentifier(), false, null, 2997 a.getDescription()); 2998 bva.addValue(value); 2999 if (bva.getValue()) 3000 { 3001 a.incrementOccurrences(); 3002 argumentsSetFromPropertiesFile.add(a.getIdentifierString()); 3003 } 3004 } 3005 else 3006 { 3007 a.addValue(value); 3008 a.incrementOccurrences(); 3009 3010 argumentsSetFromPropertiesFile.add(a.getIdentifierString()); 3011 if (a.isSensitive()) 3012 { 3013 argumentsSetFromPropertiesFile.add("***REDACTED***"); 3014 } 3015 else 3016 { 3017 argumentsSetFromPropertiesFile.add(value); 3018 } 3019 } 3020 } 3021 } 3022 } 3023 } 3024 3025 3026 3027 /** 3028 * Retrieves lines that make up the usage information for this program, 3029 * optionally wrapping long lines. 3030 * 3031 * @param maxWidth The maximum line width to use for the output. If this is 3032 * less than or equal to zero, then no wrapping will be 3033 * performed. 3034 * 3035 * @return The lines that make up the usage information for this program. 3036 */ 3037 public List<String> getUsage(final int maxWidth) 3038 { 3039 // If a subcommand was selected, then provide usage specific to that 3040 // subcommand. 3041 if (selectedSubCommand != null) 3042 { 3043 return getSubCommandUsage(maxWidth); 3044 } 3045 3046 // First is a description of the command. 3047 final ArrayList<String> lines = new ArrayList<>(100); 3048 lines.addAll(StaticUtils.wrapLine(commandDescription, maxWidth)); 3049 lines.add(""); 3050 3051 3052 // If the tool supports subcommands, and if there are fewer than 10 3053 // subcommands, then display them inline. 3054 if ((! subCommands.isEmpty()) && (subCommands.size() < 10)) 3055 { 3056 lines.add(INFO_USAGE_SUBCOMMANDS_HEADER.get()); 3057 lines.add(""); 3058 3059 for (final SubCommand sc : subCommands) 3060 { 3061 final StringBuilder nameBuffer = new StringBuilder(); 3062 nameBuffer.append(" "); 3063 3064 final Iterator<String> nameIterator = sc.getNames(false).iterator(); 3065 while (nameIterator.hasNext()) 3066 { 3067 nameBuffer.append(nameIterator.next()); 3068 if (nameIterator.hasNext()) 3069 { 3070 nameBuffer.append(", "); 3071 } 3072 } 3073 lines.add(nameBuffer.toString()); 3074 3075 for (final String descriptionLine : 3076 StaticUtils.wrapLine(sc.getDescription(), (maxWidth - 4))) 3077 { 3078 lines.add(" " + descriptionLine); 3079 } 3080 lines.add(""); 3081 } 3082 } 3083 3084 3085 // Next comes the usage. It may include neither, either, or both of the 3086 // set of options and trailing arguments. 3087 if (! subCommands.isEmpty()) 3088 { 3089 lines.addAll(StaticUtils.wrapLine( 3090 INFO_USAGE_SUBCOMMAND_USAGE.get(commandName), maxWidth)); 3091 } 3092 else if (namedArgs.isEmpty()) 3093 { 3094 if (maxTrailingArgs == 0) 3095 { 3096 lines.addAll(StaticUtils.wrapLine( 3097 INFO_USAGE_NOOPTIONS_NOTRAILING.get(commandName), maxWidth)); 3098 } 3099 else 3100 { 3101 lines.addAll(StaticUtils.wrapLine(INFO_USAGE_NOOPTIONS_TRAILING.get( 3102 commandName, trailingArgsPlaceholder), maxWidth)); 3103 } 3104 } 3105 else 3106 { 3107 if (maxTrailingArgs == 0) 3108 { 3109 lines.addAll(StaticUtils.wrapLine( 3110 INFO_USAGE_OPTIONS_NOTRAILING.get(commandName), maxWidth)); 3111 } 3112 else 3113 { 3114 lines.addAll(StaticUtils.wrapLine(INFO_USAGE_OPTIONS_TRAILING.get( 3115 commandName, trailingArgsPlaceholder), maxWidth)); 3116 } 3117 } 3118 3119 if (! namedArgs.isEmpty()) 3120 { 3121 lines.add(""); 3122 lines.add(INFO_USAGE_OPTIONS_INCLUDE.get()); 3123 3124 3125 // If there are any argument groups, then collect the arguments in those 3126 // groups. 3127 boolean hasRequired = false; 3128 final LinkedHashMap<String,List<Argument>> argumentsByGroup = 3129 new LinkedHashMap<>(10); 3130 final ArrayList<Argument> argumentsWithoutGroup = 3131 new ArrayList<>(namedArgs.size()); 3132 final ArrayList<Argument> usageArguments = 3133 new ArrayList<>(namedArgs.size()); 3134 for (final Argument a : namedArgs) 3135 { 3136 if (a.isHidden()) 3137 { 3138 // This argument shouldn't be included in the usage output. 3139 continue; 3140 } 3141 3142 if (a.isRequired() && (! a.hasDefaultValue())) 3143 { 3144 hasRequired = true; 3145 } 3146 3147 final String argumentGroup = a.getArgumentGroupName(); 3148 if (argumentGroup == null) 3149 { 3150 if (a.isUsageArgument()) 3151 { 3152 usageArguments.add(a); 3153 } 3154 else 3155 { 3156 argumentsWithoutGroup.add(a); 3157 } 3158 } 3159 else 3160 { 3161 List<Argument> groupArgs = argumentsByGroup.get(argumentGroup); 3162 if (groupArgs == null) 3163 { 3164 groupArgs = new ArrayList<>(10); 3165 argumentsByGroup.put(argumentGroup, groupArgs); 3166 } 3167 3168 groupArgs.add(a); 3169 } 3170 } 3171 3172 3173 // Iterate through the defined argument groups and display usage 3174 // information for each of them. 3175 for (final Map.Entry<String,List<Argument>> e : 3176 argumentsByGroup.entrySet()) 3177 { 3178 lines.add(""); 3179 lines.add(" " + e.getKey()); 3180 lines.add(""); 3181 for (final Argument a : e.getValue()) 3182 { 3183 getArgUsage(a, lines, true, maxWidth); 3184 } 3185 } 3186 3187 if (! argumentsWithoutGroup.isEmpty()) 3188 { 3189 if (argumentsByGroup.isEmpty()) 3190 { 3191 for (final Argument a : argumentsWithoutGroup) 3192 { 3193 getArgUsage(a, lines, false, maxWidth); 3194 } 3195 } 3196 else 3197 { 3198 lines.add(""); 3199 lines.add(" " + INFO_USAGE_UNGROUPED_ARGS.get()); 3200 lines.add(""); 3201 for (final Argument a : argumentsWithoutGroup) 3202 { 3203 getArgUsage(a, lines, true, maxWidth); 3204 } 3205 } 3206 } 3207 3208 if (! usageArguments.isEmpty()) 3209 { 3210 if (argumentsByGroup.isEmpty()) 3211 { 3212 for (final Argument a : usageArguments) 3213 { 3214 getArgUsage(a, lines, false, maxWidth); 3215 } 3216 } 3217 else 3218 { 3219 lines.add(""); 3220 lines.add(" " + INFO_USAGE_USAGE_ARGS.get()); 3221 lines.add(""); 3222 for (final Argument a : usageArguments) 3223 { 3224 getArgUsage(a, lines, true, maxWidth); 3225 } 3226 } 3227 } 3228 3229 if (hasRequired) 3230 { 3231 lines.add(""); 3232 if (argumentsByGroup.isEmpty()) 3233 { 3234 lines.add("* " + INFO_USAGE_ARG_IS_REQUIRED.get()); 3235 } 3236 else 3237 { 3238 lines.add(" * " + INFO_USAGE_ARG_IS_REQUIRED.get()); 3239 } 3240 } 3241 } 3242 3243 return lines; 3244 } 3245 3246 3247 3248 /** 3249 * Retrieves lines that make up the usage information for the selected 3250 * subcommand. 3251 * 3252 * @param maxWidth The maximum line width to use for the output. If this is 3253 * less than or equal to zero, then no wrapping will be 3254 * performed. 3255 * 3256 * @return The lines that make up the usage information for the selected 3257 * subcommand. 3258 */ 3259 private List<String> getSubCommandUsage(final int maxWidth) 3260 { 3261 // First is a description of the subcommand. 3262 final ArrayList<String> lines = new ArrayList<>(100); 3263 lines.addAll( 3264 StaticUtils.wrapLine(selectedSubCommand.getDescription(), maxWidth)); 3265 lines.add(""); 3266 3267 // Next comes the usage. 3268 lines.addAll(StaticUtils.wrapLine(INFO_SUBCOMMAND_USAGE_OPTIONS.get( 3269 commandName, selectedSubCommand.getPrimaryName()), maxWidth)); 3270 3271 3272 final ArgumentParser parser = selectedSubCommand.getArgumentParser(); 3273 if (! parser.namedArgs.isEmpty()) 3274 { 3275 lines.add(""); 3276 lines.add(INFO_USAGE_OPTIONS_INCLUDE.get()); 3277 3278 3279 // If there are any argument groups, then collect the arguments in those 3280 // groups. 3281 boolean hasRequired = false; 3282 final LinkedHashMap<String,List<Argument>> argumentsByGroup = 3283 new LinkedHashMap<>(10); 3284 final ArrayList<Argument> argumentsWithoutGroup = 3285 new ArrayList<>(parser.namedArgs.size()); 3286 final ArrayList<Argument> usageArguments = 3287 new ArrayList<>(parser.namedArgs.size()); 3288 for (final Argument a : parser.namedArgs) 3289 { 3290 if (a.isHidden()) 3291 { 3292 // This argument shouldn't be included in the usage output. 3293 continue; 3294 } 3295 3296 if (a.isRequired() && (! a.hasDefaultValue())) 3297 { 3298 hasRequired = true; 3299 } 3300 3301 final String argumentGroup = a.getArgumentGroupName(); 3302 if (argumentGroup == null) 3303 { 3304 if (a.isUsageArgument()) 3305 { 3306 usageArguments.add(a); 3307 } 3308 else 3309 { 3310 argumentsWithoutGroup.add(a); 3311 } 3312 } 3313 else 3314 { 3315 List<Argument> groupArgs = argumentsByGroup.get(argumentGroup); 3316 if (groupArgs == null) 3317 { 3318 groupArgs = new ArrayList<>(10); 3319 argumentsByGroup.put(argumentGroup, groupArgs); 3320 } 3321 3322 groupArgs.add(a); 3323 } 3324 } 3325 3326 3327 // Iterate through the defined argument groups and display usage 3328 // information for each of them. 3329 for (final Map.Entry<String,List<Argument>> e : 3330 argumentsByGroup.entrySet()) 3331 { 3332 lines.add(""); 3333 lines.add(" " + e.getKey()); 3334 lines.add(""); 3335 for (final Argument a : e.getValue()) 3336 { 3337 getArgUsage(a, lines, true, maxWidth); 3338 } 3339 } 3340 3341 if (! argumentsWithoutGroup.isEmpty()) 3342 { 3343 if (argumentsByGroup.isEmpty()) 3344 { 3345 for (final Argument a : argumentsWithoutGroup) 3346 { 3347 getArgUsage(a, lines, false, maxWidth); 3348 } 3349 } 3350 else 3351 { 3352 lines.add(""); 3353 lines.add(" " + INFO_USAGE_UNGROUPED_ARGS.get()); 3354 lines.add(""); 3355 for (final Argument a : argumentsWithoutGroup) 3356 { 3357 getArgUsage(a, lines, true, maxWidth); 3358 } 3359 } 3360 } 3361 3362 if (! usageArguments.isEmpty()) 3363 { 3364 if (argumentsByGroup.isEmpty()) 3365 { 3366 for (final Argument a : usageArguments) 3367 { 3368 getArgUsage(a, lines, false, maxWidth); 3369 } 3370 } 3371 else 3372 { 3373 lines.add(""); 3374 lines.add(" " + INFO_USAGE_USAGE_ARGS.get()); 3375 lines.add(""); 3376 for (final Argument a : usageArguments) 3377 { 3378 getArgUsage(a, lines, true, maxWidth); 3379 } 3380 } 3381 } 3382 3383 if (hasRequired) 3384 { 3385 lines.add(""); 3386 if (argumentsByGroup.isEmpty()) 3387 { 3388 lines.add("* " + INFO_USAGE_ARG_IS_REQUIRED.get()); 3389 } 3390 else 3391 { 3392 lines.add(" * " + INFO_USAGE_ARG_IS_REQUIRED.get()); 3393 } 3394 } 3395 } 3396 3397 return lines; 3398 } 3399 3400 3401 3402 /** 3403 * Adds usage information for the provided argument to the given list. 3404 * 3405 * @param a The argument for which to get the usage information. 3406 * @param lines The list to which the resulting lines should be added. 3407 * @param indent Indicates whether to indent each line. 3408 * @param maxWidth The maximum width of each line, in characters. 3409 */ 3410 private static void getArgUsage(final Argument a, final List<String> lines, 3411 final boolean indent, final int maxWidth) 3412 { 3413 final StringBuilder argLine = new StringBuilder(); 3414 if (indent && (maxWidth > 10)) 3415 { 3416 if (a.isRequired() && (! a.hasDefaultValue())) 3417 { 3418 argLine.append(" * "); 3419 } 3420 else 3421 { 3422 argLine.append(" "); 3423 } 3424 } 3425 else if (a.isRequired() && (! a.hasDefaultValue())) 3426 { 3427 argLine.append("* "); 3428 } 3429 3430 boolean first = true; 3431 for (final Character c : a.getShortIdentifiers(false)) 3432 { 3433 if (first) 3434 { 3435 argLine.append('-'); 3436 first = false; 3437 } 3438 else 3439 { 3440 argLine.append(", -"); 3441 } 3442 argLine.append(c); 3443 } 3444 3445 for (final String s : a.getLongIdentifiers(false)) 3446 { 3447 if (first) 3448 { 3449 argLine.append("--"); 3450 first = false; 3451 } 3452 else 3453 { 3454 argLine.append(", --"); 3455 } 3456 argLine.append(s); 3457 } 3458 3459 final String valuePlaceholder = a.getValuePlaceholder(); 3460 if (valuePlaceholder != null) 3461 { 3462 argLine.append(' '); 3463 argLine.append(valuePlaceholder); 3464 } 3465 3466 // If we need to wrap the argument line, then align the dashes on the left 3467 // edge. 3468 int subsequentLineWidth = maxWidth - 4; 3469 if (subsequentLineWidth < 4) 3470 { 3471 subsequentLineWidth = maxWidth; 3472 } 3473 final List<String> identifierLines = 3474 StaticUtils.wrapLine(argLine.toString(), maxWidth, 3475 subsequentLineWidth); 3476 for (int i=0; i < identifierLines.size(); i++) 3477 { 3478 if (i == 0) 3479 { 3480 lines.add(identifierLines.get(0)); 3481 } 3482 else 3483 { 3484 lines.add(" " + identifierLines.get(i)); 3485 } 3486 } 3487 3488 3489 // The description should be wrapped, if necessary. We'll also want to 3490 // indent it (unless someone chose an absurdly small wrap width) to make 3491 // it stand out from the argument lines. 3492 final String description = a.getDescription(); 3493 if (maxWidth > 10) 3494 { 3495 final String indentString; 3496 if (indent) 3497 { 3498 indentString = " "; 3499 } 3500 else 3501 { 3502 indentString = " "; 3503 } 3504 3505 final List<String> descLines = StaticUtils.wrapLine(description, 3506 (maxWidth-indentString.length())); 3507 for (final String s : descLines) 3508 { 3509 lines.add(indentString + s); 3510 } 3511 } 3512 else 3513 { 3514 lines.addAll(StaticUtils.wrapLine(description, maxWidth)); 3515 } 3516 } 3517 3518 3519 3520 /** 3521 * Writes usage information for this program to the provided output stream 3522 * using the UTF-8 encoding, optionally wrapping long lines. 3523 * 3524 * @param outputStream The output stream to which the usage information 3525 * should be written. It must not be {@code null}. 3526 * @param maxWidth The maximum line width to use for the output. If 3527 * this is less than or equal to zero, then no wrapping 3528 * will be performed. 3529 * 3530 * @throws IOException If an error occurs while attempting to write to the 3531 * provided output stream. 3532 */ 3533 public void getUsage(final OutputStream outputStream, final int maxWidth) 3534 throws IOException 3535 { 3536 final List<String> usageLines = getUsage(maxWidth); 3537 for (final String s : usageLines) 3538 { 3539 outputStream.write(StaticUtils.getBytes(s)); 3540 outputStream.write(StaticUtils.EOL_BYTES); 3541 } 3542 } 3543 3544 3545 3546 /** 3547 * Retrieves a string representation of the usage information. 3548 * 3549 * @param maxWidth The maximum line width to use for the output. If this is 3550 * less than or equal to zero, then no wrapping will be 3551 * performed. 3552 * 3553 * @return A string representation of the usage information 3554 */ 3555 public String getUsageString(final int maxWidth) 3556 { 3557 final StringBuilder buffer = new StringBuilder(); 3558 getUsageString(buffer, maxWidth); 3559 return buffer.toString(); 3560 } 3561 3562 3563 3564 /** 3565 * Appends a string representation of the usage information to the provided 3566 * buffer. 3567 * 3568 * @param buffer The buffer to which the information should be appended. 3569 * @param maxWidth The maximum line width to use for the output. If this is 3570 * less than or equal to zero, then no wrapping will be 3571 * performed. 3572 */ 3573 public void getUsageString(final StringBuilder buffer, final int maxWidth) 3574 { 3575 for (final String line : getUsage(maxWidth)) 3576 { 3577 buffer.append(line); 3578 buffer.append(StaticUtils.EOL); 3579 } 3580 } 3581 3582 3583 3584 /** 3585 * Retrieves a string representation of this argument parser. 3586 * 3587 * @return A string representation of this argument parser. 3588 */ 3589 @Override() 3590 public String toString() 3591 { 3592 final StringBuilder buffer = new StringBuilder(); 3593 toString(buffer); 3594 return buffer.toString(); 3595 } 3596 3597 3598 3599 /** 3600 * Appends a string representation of this argument parser to the provided 3601 * buffer. 3602 * 3603 * @param buffer The buffer to which the information should be appended. 3604 */ 3605 public void toString(final StringBuilder buffer) 3606 { 3607 buffer.append("ArgumentParser(commandName='"); 3608 buffer.append(commandName); 3609 buffer.append("', commandDescription='"); 3610 buffer.append(commandDescription); 3611 buffer.append("', minTrailingArgs="); 3612 buffer.append(minTrailingArgs); 3613 buffer.append("', maxTrailingArgs="); 3614 buffer.append(maxTrailingArgs); 3615 3616 if (trailingArgsPlaceholder != null) 3617 { 3618 buffer.append(", trailingArgsPlaceholder='"); 3619 buffer.append(trailingArgsPlaceholder); 3620 buffer.append('\''); 3621 } 3622 3623 buffer.append("namedArgs={"); 3624 3625 final Iterator<Argument> iterator = namedArgs.iterator(); 3626 while (iterator.hasNext()) 3627 { 3628 iterator.next().toString(buffer); 3629 if (iterator.hasNext()) 3630 { 3631 buffer.append(", "); 3632 } 3633 } 3634 3635 buffer.append('}'); 3636 3637 if (! subCommands.isEmpty()) 3638 { 3639 buffer.append(", subCommands={"); 3640 3641 final Iterator<SubCommand> subCommandIterator = subCommands.iterator(); 3642 while (subCommandIterator.hasNext()) 3643 { 3644 subCommandIterator.next().toString(buffer); 3645 if (subCommandIterator.hasNext()) 3646 { 3647 buffer.append(", "); 3648 } 3649 } 3650 3651 buffer.append('}'); 3652 } 3653 3654 buffer.append(')'); 3655 } 3656}