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