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