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