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 + '=' + s); 2839 } 2840 } 2841 } 2842 2843 2844 2845 /** 2846 * Wraps the given string and writes it as a comment to the provided writer. 2847 * 2848 * @param w The writer to use to write the wrapped and commented string. 2849 * @param s The string to be wrapped and written. 2850 */ 2851 private static void wrapComment(@NotNull final PrintWriter w, 2852 @NotNull final String s) 2853 { 2854 for (final String line : StaticUtils.wrapLine(s, 77)) 2855 { 2856 w.println("# " + line); 2857 } 2858 } 2859 2860 2861 2862 /** 2863 * Reads the contents of the specified properties file and updates the 2864 * configured arguments as appropriate. 2865 * 2866 * @param propertiesFile The properties file to process. 2867 * @param skipFinalValidation A flag that indicates whether to skip final 2868 * validation because a qualifying usage argument 2869 * was provided. 2870 * 2871 * @throws ArgumentException If a problem is encountered while examining the 2872 * properties file, or while trying to assign a 2873 * property value to a corresponding argument. 2874 */ 2875 private void handlePropertiesFile(@NotNull final File propertiesFile, 2876 @NotNull final AtomicBoolean skipFinalValidation) 2877 throws ArgumentException 2878 { 2879 final String propertiesFilePath = propertiesFile.getAbsolutePath(); 2880 2881 InputStream inputStream = null; 2882 final BufferedReader reader; 2883 try 2884 { 2885 inputStream = new FileInputStream(propertiesFile); 2886 2887 2888 // Handle the case in which the properties file may be encrypted. 2889 final List<char[]> cachedPasswords; 2890 final PrintStream err; 2891 final PrintStream out; 2892 final CommandLineTool tool = commandLineTool; 2893 if (tool == null) 2894 { 2895 cachedPasswords = Collections.emptyList(); 2896 out = System.out; 2897 err = System.err; 2898 } 2899 else 2900 { 2901 cachedPasswords = 2902 tool.getPasswordFileReader().getCachedEncryptionPasswords(); 2903 out = tool.getOut(); 2904 err = tool.getErr(); 2905 } 2906 2907 final ObjectPair<InputStream,char[]> encryptionData = 2908 ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream, 2909 cachedPasswords, true, 2910 INFO_PARSER_PROMPT_FOR_PROP_FILE_ENC_PW.get( 2911 propertiesFile.getAbsolutePath()), 2912 ERR_PARSER_WRONG_PROP_FILE_ENC_PW.get( 2913 propertiesFile.getAbsolutePath()), 2914 out, err); 2915 2916 inputStream = encryptionData.getFirst(); 2917 if ((tool != null) && (encryptionData.getSecond() != null)) 2918 { 2919 tool.getPasswordFileReader().addToEncryptionPasswordCache( 2920 encryptionData.getSecond()); 2921 } 2922 2923 2924 // Handle the case in which the properties file may be compressed. 2925 inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream); 2926 2927 2928 // The java.util.Properties specification states that properties files 2929 // should be read using the ISO 8859-1 character set, and that characters 2930 // that cannot be encoded in that format should be represented using 2931 // Unicode escapes that start with a backslash, a lowercase letter "u", 2932 // and four hexadecimal digits. To provide compatibility with the Java 2933 // Properties file format (except we also support the same property 2934 // appearing multiple times), we will also use that encoding and will 2935 // support Unicode escape sequences. 2936 reader = new BufferedReader(new InputStreamReader(inputStream, 2937 StandardCharsets.ISO_8859_1)); 2938 } 2939 catch (final Exception e) 2940 { 2941 if (inputStream != null) 2942 { 2943 try 2944 { 2945 inputStream.close(); 2946 } 2947 catch (final Exception e2) 2948 { 2949 Debug.debugException(e2); 2950 } 2951 } 2952 2953 Debug.debugException(e); 2954 throw new ArgumentException( 2955 ERR_PARSER_CANNOT_OPEN_PROP_FILE.get(propertiesFilePath, 2956 StaticUtils.getExceptionMessage(e)), 2957 e); 2958 } 2959 2960 try 2961 { 2962 // Read all of the lines of the file, ignoring comments and unwrapping 2963 // properties that span multiple lines. 2964 boolean lineIsContinued = false; 2965 int lineNumber = 0; 2966 final ArrayList<ObjectPair<Integer,StringBuilder>> propertyLines = 2967 new ArrayList<>(10); 2968 while (true) 2969 { 2970 String line; 2971 try 2972 { 2973 line = reader.readLine(); 2974 lineNumber++; 2975 } 2976 catch (final Exception e) 2977 { 2978 Debug.debugException(e); 2979 throw new ArgumentException( 2980 ERR_PARSER_ERROR_READING_PROP_FILE.get(propertiesFilePath, 2981 StaticUtils.getExceptionMessage(e)), 2982 e); 2983 } 2984 2985 2986 // If the line is null, then we've reached the end of the file. If we 2987 // expect a previous line to have been continued, then this is an error. 2988 if (line == null) 2989 { 2990 if (lineIsContinued) 2991 { 2992 throw new ArgumentException( 2993 ERR_PARSER_PROP_FILE_MISSING_CONTINUATION.get( 2994 (lineNumber-1), propertiesFilePath)); 2995 } 2996 break; 2997 } 2998 2999 3000 // See if the line has any leading whitespace, and if so then trim it 3001 // off. If there is leading whitespace, then make sure that we expect 3002 // the previous line to be continued. 3003 final int initialLength = line.length(); 3004 line = StaticUtils.trimLeading(line); 3005 final boolean hasLeadingWhitespace = (line.length() < initialLength); 3006 if (hasLeadingWhitespace && (! lineIsContinued)) 3007 { 3008 throw new ArgumentException( 3009 ERR_PARSER_PROP_FILE_UNEXPECTED_LEADING_SPACE.get( 3010 propertiesFilePath, lineNumber)); 3011 } 3012 3013 3014 // If the line is empty or starts with "#", then skip it. But make sure 3015 // we didn't expect the previous line to be continued. 3016 if ((line.isEmpty()) || line.startsWith("#")) 3017 { 3018 if (lineIsContinued) 3019 { 3020 throw new ArgumentException( 3021 ERR_PARSER_PROP_FILE_MISSING_CONTINUATION.get( 3022 (lineNumber-1), propertiesFilePath)); 3023 } 3024 continue; 3025 } 3026 3027 3028 // See if the line ends with a backslash and if so then trim it off. 3029 final boolean hasTrailingBackslash = line.endsWith("\\"); 3030 if (line.endsWith("\\")) 3031 { 3032 line = line.substring(0, (line.length() - 1)); 3033 } 3034 3035 3036 // If the previous line needs to be continued, then append the new line 3037 // to it. Otherwise, add it as a new line. 3038 if (lineIsContinued) 3039 { 3040 propertyLines.get(propertyLines.size() - 1).getSecond().append(line); 3041 } 3042 else 3043 { 3044 propertyLines.add( 3045 new ObjectPair<>(lineNumber, new StringBuilder(line))); 3046 } 3047 3048 lineIsContinued = hasTrailingBackslash; 3049 } 3050 3051 3052 // Parse all of the lines into a map of identifiers and their 3053 // corresponding values. 3054 propertiesFileUsed = propertiesFile; 3055 if (propertyLines.isEmpty()) 3056 { 3057 return; 3058 } 3059 3060 final HashMap<String,ArrayList<String>> propertyMap = 3061 new HashMap<>(StaticUtils.computeMapCapacity(propertyLines.size())); 3062 for (final ObjectPair<Integer,StringBuilder> p : propertyLines) 3063 { 3064 lineNumber = p.getFirst(); 3065 final String line = handleUnicodeEscapes(propertiesFilePath, lineNumber, 3066 p.getSecond()); 3067 final int equalPos = line.indexOf('='); 3068 if (equalPos <= 0) 3069 { 3070 throw new ArgumentException(ERR_PARSER_MALFORMED_PROP_LINE.get( 3071 propertiesFilePath, lineNumber, line)); 3072 } 3073 3074 final String propertyName = line.substring(0, equalPos).trim(); 3075 final String propertyValue = line.substring(equalPos+1).trim(); 3076 if (propertyValue.isEmpty()) 3077 { 3078 // The property doesn't have a value, so we can ignore it. 3079 continue; 3080 } 3081 3082 3083 // An argument can have multiple identifiers, and we will allow any of 3084 // them to be used to reference it. To deal with this, we'll map the 3085 // argument identifier to its corresponding argument and then use the 3086 // preferred identifier for that argument in the map. The same applies 3087 // to subcommand names. 3088 boolean prefixedWithToolName = false; 3089 boolean prefixedWithSubCommandName = false; 3090 Argument a = getNamedArgument(propertyName); 3091 if (a == null) 3092 { 3093 // It could be that the argument name was prefixed with the tool name. 3094 // Check to see if that was the case. 3095 if (propertyName.startsWith(commandName + '.')) 3096 { 3097 prefixedWithToolName = true; 3098 3099 String basePropertyName = 3100 propertyName.substring(commandName.length()+1); 3101 a = getNamedArgument(basePropertyName); 3102 3103 if (a == null) 3104 { 3105 final int periodPos = basePropertyName.indexOf('.'); 3106 if (periodPos > 0) 3107 { 3108 final String subCommandName = 3109 basePropertyName.substring(0, periodPos); 3110 if ((selectedSubCommand != null) && 3111 selectedSubCommand.hasName(subCommandName)) 3112 { 3113 prefixedWithSubCommandName = true; 3114 basePropertyName = basePropertyName.substring(periodPos+1); 3115 a = selectedSubCommand.getArgumentParser().getNamedArgument( 3116 basePropertyName); 3117 } 3118 } 3119 else if (selectedSubCommand != null) 3120 { 3121 a = selectedSubCommand.getArgumentParser().getNamedArgument( 3122 basePropertyName); 3123 } 3124 } 3125 } 3126 else if (selectedSubCommand != null) 3127 { 3128 a = selectedSubCommand.getArgumentParser().getNamedArgument( 3129 propertyName); 3130 } 3131 } 3132 3133 if (a == null) 3134 { 3135 // This could mean that there's a typo in the property name, but it's 3136 // more likely the case that the property is for a different tool. In 3137 // either case, we'll ignore it. 3138 continue; 3139 } 3140 3141 final String canonicalPropertyName; 3142 if (prefixedWithToolName) 3143 { 3144 if (prefixedWithSubCommandName) 3145 { 3146 canonicalPropertyName = commandName + '.' + 3147 selectedSubCommand.getPrimaryName() + '.' + 3148 a.getIdentifierString(); 3149 } 3150 else 3151 { 3152 canonicalPropertyName = commandName + '.' + a.getIdentifierString(); 3153 } 3154 } 3155 else 3156 { 3157 canonicalPropertyName = a.getIdentifierString(); 3158 } 3159 3160 ArrayList<String> valueList = propertyMap.get(canonicalPropertyName); 3161 if (valueList == null) 3162 { 3163 valueList = new ArrayList<>(5); 3164 propertyMap.put(canonicalPropertyName, valueList); 3165 } 3166 valueList.add(propertyValue); 3167 } 3168 3169 3170 // Iterate through all of the named arguments for the argument parser and 3171 // see if we should use the properties to assign values to any of the 3172 // arguments that weren't provided on the command line. 3173 setArgsFromPropertiesFile(propertyMap, false, skipFinalValidation); 3174 3175 3176 // If there is a selected subcommand, then iterate through all of its 3177 // arguments. 3178 if (selectedSubCommand != null) 3179 { 3180 setArgsFromPropertiesFile(propertyMap, true, skipFinalValidation); 3181 } 3182 } 3183 finally 3184 { 3185 try 3186 { 3187 reader.close(); 3188 } 3189 catch (final Exception e) 3190 { 3191 Debug.debugException(e); 3192 } 3193 } 3194 } 3195 3196 3197 3198 /** 3199 * Retrieves a string that contains the contents of the provided buffer, but 3200 * with any Unicode escape sequences converted to the appropriate character 3201 * representation, and any other escapes having the initial backslash 3202 * removed. 3203 * 3204 * @param propertiesFilePath The path to the properties file being written. 3205 * It must not be {@code null}. 3206 * @param lineNumber The line number on which the property 3207 * definition starts. 3208 * @param buffer The buffer containing the data to be processed. 3209 * It must not be {@code null} but may be empty. 3210 * 3211 * @return A string that contains the contents of the provided buffer, but 3212 * with any Unicode escape sequences converted to the appropriate 3213 * character representation. 3214 * 3215 * @throws ArgumentException If a malformed Unicode escape sequence is 3216 * encountered. 3217 */ 3218 @NotNull() 3219 static String handleUnicodeEscapes(@NotNull final String propertiesFilePath, 3220 final int lineNumber, 3221 @NotNull final StringBuilder buffer) 3222 throws ArgumentException 3223 { 3224 int pos = 0; 3225 while (pos < buffer.length()) 3226 { 3227 final char c = buffer.charAt(pos); 3228 if (c == '\\') 3229 { 3230 if (pos <= (buffer.length() - 5)) 3231 { 3232 final char nextChar = buffer.charAt(pos+1); 3233 if ((nextChar == 'u') || (nextChar == 'U')) 3234 { 3235 try 3236 { 3237 final String hexDigits = buffer.substring(pos+2, pos+6); 3238 final byte[] bytes = StaticUtils.fromHex(hexDigits); 3239 final int i = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF); 3240 buffer.setCharAt(pos, (char) i); 3241 for (int j=0; j < 5; j++) 3242 { 3243 buffer.deleteCharAt(pos+1); 3244 } 3245 } 3246 catch (final Exception e) 3247 { 3248 Debug.debugException(e); 3249 throw new ArgumentException( 3250 ERR_PARSER_MALFORMED_UNICODE_ESCAPE.get(propertiesFilePath, 3251 lineNumber), 3252 e); 3253 } 3254 } 3255 else 3256 { 3257 buffer.deleteCharAt(pos); 3258 } 3259 } 3260 } 3261 3262 pos++; 3263 } 3264 3265 return buffer.toString(); 3266 } 3267 3268 3269 3270 /** 3271 * Sets the values of any arguments not provided on the command line but 3272 * defined in the properties file. 3273 * 3274 * @param propertyMap A map of properties read from the properties 3275 * file. 3276 * @param useSubCommand Indicates whether to use the argument parser 3277 * associated with the selected subcommand rather 3278 * than the global argument parser. 3279 * @param skipFinalValidation A flag that indicates whether to skip final 3280 * validation because a qualifying usage argument 3281 * was provided. 3282 * 3283 * @throws ArgumentException If a problem is encountered while examining the 3284 * properties file, or while trying to assign a 3285 * property value to a corresponding argument. 3286 */ 3287 private void setArgsFromPropertiesFile( 3288 @NotNull final Map<String,ArrayList<String>> propertyMap, 3289 final boolean useSubCommand, 3290 @NotNull final AtomicBoolean skipFinalValidation) 3291 throws ArgumentException 3292 { 3293 final ArgumentParser p; 3294 if (useSubCommand) 3295 { 3296 p = selectedSubCommand.getArgumentParser(); 3297 } 3298 else 3299 { 3300 p = this; 3301 } 3302 3303 3304 for (final Argument a : p.namedArgs) 3305 { 3306 // If the argument was provided on the command line, then that will always 3307 // override anything that might be in the properties file. 3308 if (a.getNumOccurrences() > 0) 3309 { 3310 continue; 3311 } 3312 3313 3314 // If the argument is part of an exclusive argument set, and if one of 3315 // the other arguments in that set was provided on the command line, then 3316 // don't look in the properties file for a value for the argument. 3317 boolean exclusiveArgumentHasValue = false; 3318exclusiveArgumentLoop: 3319 for (final Set<Argument> exclusiveArgumentSet : exclusiveArgumentSets) 3320 { 3321 if (exclusiveArgumentSet.contains(a)) 3322 { 3323 for (final Argument exclusiveArg : exclusiveArgumentSet) 3324 { 3325 if (exclusiveArg.getNumOccurrences() > 0) 3326 { 3327 exclusiveArgumentHasValue = true; 3328 break exclusiveArgumentLoop; 3329 } 3330 } 3331 } 3332 } 3333 3334 if (exclusiveArgumentHasValue) 3335 { 3336 continue; 3337 } 3338 3339 3340 // If we should use a subcommand, then see if the properties file has a 3341 // property that is specific to the selected subcommand. Then fall back 3342 // to a property that is specific to the tool, and finally fall back to 3343 // checking for a set of values that are generic to any tool that has an 3344 // argument with that name. 3345 List<String> values = null; 3346 if (useSubCommand) 3347 { 3348 values = propertyMap.get(commandName + '.' + 3349 selectedSubCommand.getPrimaryName() + '.' + 3350 a.getIdentifierString()); 3351 } 3352 3353 if (values == null) 3354 { 3355 values = propertyMap.get(commandName + '.' + a.getIdentifierString()); 3356 } 3357 3358 if (values == null) 3359 { 3360 values = propertyMap.get(a.getIdentifierString()); 3361 } 3362 3363 if (values != null) 3364 { 3365 for (final String value : values) 3366 { 3367 if (a instanceof BooleanArgument) 3368 { 3369 // We'll treat this as a BooleanValueArgument. 3370 final BooleanValueArgument bva = new BooleanValueArgument( 3371 a.getShortIdentifier(), a.getLongIdentifier(), false, null, 3372 a.getDescription()); 3373 bva.addValue(value); 3374 if (bva.getValue()) 3375 { 3376 a.incrementOccurrences(); 3377 argumentsSetFromPropertiesFile.add(a.getIdentifierString()); 3378 } 3379 } 3380 else 3381 { 3382 a.addValue(value); 3383 a.incrementOccurrences(); 3384 3385 argumentsSetFromPropertiesFile.add(a.getIdentifierString()); 3386 if (a.isSensitive()) 3387 { 3388 argumentsSetFromPropertiesFile.add("***REDACTED***"); 3389 } 3390 else 3391 { 3392 argumentsSetFromPropertiesFile.add(value); 3393 } 3394 } 3395 } 3396 3397 if (a.isUsageArgument() && skipFinalValidationBecauseOfArgument(a)) 3398 { 3399 skipFinalValidation.set(true); 3400 } 3401 } 3402 } 3403 } 3404 3405 3406 3407 /** 3408 * Retrieves lines that make up the usage information for this program, 3409 * optionally wrapping long lines. 3410 * 3411 * @param maxWidth The maximum line width to use for the output. If this is 3412 * less than or equal to zero, then no wrapping will be 3413 * performed. 3414 * 3415 * @return The lines that make up the usage information for this program. 3416 */ 3417 @NotNull() 3418 public List<String> getUsage(final int maxWidth) 3419 { 3420 // If a subcommand was selected, then provide usage specific to that 3421 // subcommand. 3422 if (selectedSubCommand != null) 3423 { 3424 return getSubCommandUsage(maxWidth); 3425 } 3426 3427 // First is a description of the command. 3428 final ArrayList<String> lines = new ArrayList<>(100); 3429 lines.addAll(StaticUtils.wrapLine(commandDescription, maxWidth)); 3430 lines.add(""); 3431 3432 3433 for (final String additionalDescriptionParagraph : 3434 additionalCommandDescriptionParagraphs) 3435 { 3436 lines.addAll(StaticUtils.wrapLine(additionalDescriptionParagraph, 3437 maxWidth)); 3438 lines.add(""); 3439 } 3440 3441 // If the tool supports subcommands, and if there are fewer than 10 3442 // subcommands, then display them inline. 3443 if ((! subCommands.isEmpty()) && (subCommands.size() < 10)) 3444 { 3445 lines.add(INFO_USAGE_SUBCOMMANDS_HEADER.get()); 3446 lines.add(""); 3447 3448 for (final SubCommand sc : subCommands) 3449 { 3450 final StringBuilder nameBuffer = new StringBuilder(); 3451 nameBuffer.append(" "); 3452 3453 final Iterator<String> nameIterator = sc.getNames(false).iterator(); 3454 while (nameIterator.hasNext()) 3455 { 3456 nameBuffer.append(nameIterator.next()); 3457 if (nameIterator.hasNext()) 3458 { 3459 nameBuffer.append(", "); 3460 } 3461 } 3462 lines.add(nameBuffer.toString()); 3463 3464 for (final String descriptionLine : 3465 StaticUtils.wrapLine(sc.getDescription(), (maxWidth - 4))) 3466 { 3467 lines.add(" " + descriptionLine); 3468 } 3469 lines.add(""); 3470 } 3471 } 3472 3473 3474 // Next comes the usage. It may include neither, either, or both of the 3475 // set of options and trailing arguments. 3476 if (! subCommands.isEmpty()) 3477 { 3478 lines.addAll(StaticUtils.wrapLine( 3479 INFO_USAGE_SUBCOMMAND_USAGE.get(commandName), maxWidth)); 3480 } 3481 else if (namedArgs.isEmpty()) 3482 { 3483 if (maxTrailingArgs == 0) 3484 { 3485 lines.addAll(StaticUtils.wrapLine( 3486 INFO_USAGE_NOOPTIONS_NOTRAILING.get(commandName), maxWidth)); 3487 } 3488 else 3489 { 3490 lines.addAll(StaticUtils.wrapLine(INFO_USAGE_NOOPTIONS_TRAILING.get( 3491 commandName, trailingArgsPlaceholder), maxWidth)); 3492 } 3493 } 3494 else 3495 { 3496 if (maxTrailingArgs == 0) 3497 { 3498 lines.addAll(StaticUtils.wrapLine( 3499 INFO_USAGE_OPTIONS_NOTRAILING.get(commandName), maxWidth)); 3500 } 3501 else 3502 { 3503 lines.addAll(StaticUtils.wrapLine(INFO_USAGE_OPTIONS_TRAILING.get( 3504 commandName, trailingArgsPlaceholder), maxWidth)); 3505 } 3506 } 3507 3508 if (! namedArgs.isEmpty()) 3509 { 3510 lines.add(""); 3511 lines.add(INFO_USAGE_OPTIONS_INCLUDE.get()); 3512 3513 3514 // If there are any argument groups, then collect the arguments in those 3515 // groups. 3516 boolean hasRequired = false; 3517 final LinkedHashMap<String,List<Argument>> argumentsByGroup = 3518 new LinkedHashMap<>(StaticUtils.computeMapCapacity(10)); 3519 final ArrayList<Argument> argumentsWithoutGroup = 3520 new ArrayList<>(namedArgs.size()); 3521 final ArrayList<Argument> usageArguments = 3522 new ArrayList<>(namedArgs.size()); 3523 for (final Argument a : namedArgs) 3524 { 3525 if (a.isHidden()) 3526 { 3527 // This argument shouldn't be included in the usage output. 3528 continue; 3529 } 3530 3531 if (a.isRequired() && (! a.hasDefaultValue())) 3532 { 3533 hasRequired = true; 3534 } 3535 3536 final String argumentGroup = a.getArgumentGroupName(); 3537 if (argumentGroup == null) 3538 { 3539 if (a.isUsageArgument()) 3540 { 3541 usageArguments.add(a); 3542 } 3543 else 3544 { 3545 argumentsWithoutGroup.add(a); 3546 } 3547 } 3548 else 3549 { 3550 List<Argument> groupArgs = argumentsByGroup.get(argumentGroup); 3551 if (groupArgs == null) 3552 { 3553 groupArgs = new ArrayList<>(10); 3554 argumentsByGroup.put(argumentGroup, groupArgs); 3555 } 3556 3557 groupArgs.add(a); 3558 } 3559 } 3560 3561 3562 // Iterate through the defined argument groups and display usage 3563 // information for each of them. 3564 for (final Map.Entry<String,List<Argument>> e : 3565 argumentsByGroup.entrySet()) 3566 { 3567 lines.add(""); 3568 lines.add(" " + e.getKey()); 3569 lines.add(""); 3570 for (final Argument a : e.getValue()) 3571 { 3572 getArgUsage(a, lines, true, maxWidth); 3573 } 3574 } 3575 3576 if (! argumentsWithoutGroup.isEmpty()) 3577 { 3578 if (argumentsByGroup.isEmpty()) 3579 { 3580 for (final Argument a : argumentsWithoutGroup) 3581 { 3582 getArgUsage(a, lines, false, maxWidth); 3583 } 3584 } 3585 else 3586 { 3587 lines.add(""); 3588 lines.add(" " + INFO_USAGE_UNGROUPED_ARGS.get()); 3589 lines.add(""); 3590 for (final Argument a : argumentsWithoutGroup) 3591 { 3592 getArgUsage(a, lines, true, maxWidth); 3593 } 3594 } 3595 } 3596 3597 if (! usageArguments.isEmpty()) 3598 { 3599 if (argumentsByGroup.isEmpty()) 3600 { 3601 for (final Argument a : usageArguments) 3602 { 3603 getArgUsage(a, lines, false, maxWidth); 3604 } 3605 } 3606 else 3607 { 3608 lines.add(""); 3609 lines.add(" " + INFO_USAGE_USAGE_ARGS.get()); 3610 lines.add(""); 3611 for (final Argument a : usageArguments) 3612 { 3613 getArgUsage(a, lines, true, maxWidth); 3614 } 3615 } 3616 } 3617 3618 if (hasRequired) 3619 { 3620 lines.add(""); 3621 if (argumentsByGroup.isEmpty()) 3622 { 3623 lines.add("* " + INFO_USAGE_ARG_IS_REQUIRED.get()); 3624 } 3625 else 3626 { 3627 lines.add(" * " + INFO_USAGE_ARG_IS_REQUIRED.get()); 3628 } 3629 } 3630 } 3631 3632 return lines; 3633 } 3634 3635 3636 3637 /** 3638 * Retrieves lines that make up the usage information for the selected 3639 * subcommand. 3640 * 3641 * @param maxWidth The maximum line width to use for the output. If this is 3642 * less than or equal to zero, then no wrapping will be 3643 * performed. 3644 * 3645 * @return The lines that make up the usage information for the selected 3646 * subcommand. 3647 */ 3648 @NotNull() 3649 private List<String> getSubCommandUsage(final int maxWidth) 3650 { 3651 // First is a description of the subcommand. 3652 final ArrayList<String> lines = new ArrayList<>(100); 3653 lines.addAll( 3654 StaticUtils.wrapLine(selectedSubCommand.getDescription(), maxWidth)); 3655 lines.add(""); 3656 3657 // Next comes the usage. 3658 lines.addAll(StaticUtils.wrapLine(INFO_SUBCOMMAND_USAGE_OPTIONS.get( 3659 commandName, selectedSubCommand.getPrimaryName()), maxWidth)); 3660 3661 3662 final ArgumentParser parser = selectedSubCommand.getArgumentParser(); 3663 if (! parser.namedArgs.isEmpty()) 3664 { 3665 lines.add(""); 3666 lines.add(INFO_USAGE_OPTIONS_INCLUDE.get()); 3667 3668 3669 // If there are any argument groups, then collect the arguments in those 3670 // groups. 3671 boolean hasRequired = false; 3672 final LinkedHashMap<String,List<Argument>> argumentsByGroup = 3673 new LinkedHashMap<>(StaticUtils.computeMapCapacity(10)); 3674 final ArrayList<Argument> argumentsWithoutGroup = 3675 new ArrayList<>(parser.namedArgs.size()); 3676 final ArrayList<Argument> usageArguments = 3677 new ArrayList<>(parser.namedArgs.size()); 3678 for (final Argument a : parser.namedArgs) 3679 { 3680 if (a.isHidden()) 3681 { 3682 // This argument shouldn't be included in the usage output. 3683 continue; 3684 } 3685 3686 if (a.isRequired() && (! a.hasDefaultValue())) 3687 { 3688 hasRequired = true; 3689 } 3690 3691 final String argumentGroup = a.getArgumentGroupName(); 3692 if (argumentGroup == null) 3693 { 3694 if (a.isUsageArgument()) 3695 { 3696 usageArguments.add(a); 3697 } 3698 else 3699 { 3700 argumentsWithoutGroup.add(a); 3701 } 3702 } 3703 else 3704 { 3705 List<Argument> groupArgs = argumentsByGroup.get(argumentGroup); 3706 if (groupArgs == null) 3707 { 3708 groupArgs = new ArrayList<>(10); 3709 argumentsByGroup.put(argumentGroup, groupArgs); 3710 } 3711 3712 groupArgs.add(a); 3713 } 3714 } 3715 3716 3717 // Iterate through the defined argument groups and display usage 3718 // information for each of them. 3719 for (final Map.Entry<String,List<Argument>> e : 3720 argumentsByGroup.entrySet()) 3721 { 3722 lines.add(""); 3723 lines.add(" " + e.getKey()); 3724 lines.add(""); 3725 for (final Argument a : e.getValue()) 3726 { 3727 getArgUsage(a, lines, true, maxWidth); 3728 } 3729 } 3730 3731 if (! argumentsWithoutGroup.isEmpty()) 3732 { 3733 if (argumentsByGroup.isEmpty()) 3734 { 3735 for (final Argument a : argumentsWithoutGroup) 3736 { 3737 getArgUsage(a, lines, false, maxWidth); 3738 } 3739 } 3740 else 3741 { 3742 lines.add(""); 3743 lines.add(" " + INFO_USAGE_UNGROUPED_ARGS.get()); 3744 lines.add(""); 3745 for (final Argument a : argumentsWithoutGroup) 3746 { 3747 getArgUsage(a, lines, true, maxWidth); 3748 } 3749 } 3750 } 3751 3752 if (! usageArguments.isEmpty()) 3753 { 3754 if (argumentsByGroup.isEmpty()) 3755 { 3756 for (final Argument a : usageArguments) 3757 { 3758 getArgUsage(a, lines, false, maxWidth); 3759 } 3760 } 3761 else 3762 { 3763 lines.add(""); 3764 lines.add(" " + INFO_USAGE_USAGE_ARGS.get()); 3765 lines.add(""); 3766 for (final Argument a : usageArguments) 3767 { 3768 getArgUsage(a, lines, true, maxWidth); 3769 } 3770 } 3771 } 3772 3773 if (hasRequired) 3774 { 3775 lines.add(""); 3776 if (argumentsByGroup.isEmpty()) 3777 { 3778 lines.add("* " + INFO_USAGE_ARG_IS_REQUIRED.get()); 3779 } 3780 else 3781 { 3782 lines.add(" * " + INFO_USAGE_ARG_IS_REQUIRED.get()); 3783 } 3784 } 3785 } 3786 3787 return lines; 3788 } 3789 3790 3791 3792 /** 3793 * Adds usage information for the provided argument to the given list. 3794 * 3795 * @param a The argument for which to get the usage information. 3796 * @param lines The list to which the resulting lines should be added. 3797 * @param indent Indicates whether to indent each line. 3798 * @param maxWidth The maximum width of each line, in characters. 3799 */ 3800 private static void getArgUsage(@NotNull final Argument a, 3801 @NotNull final List<String> lines, 3802 final boolean indent, final int maxWidth) 3803 { 3804 final StringBuilder argLine = new StringBuilder(); 3805 if (indent && (maxWidth > 10)) 3806 { 3807 if (a.isRequired() && (! a.hasDefaultValue())) 3808 { 3809 argLine.append(" * "); 3810 } 3811 else 3812 { 3813 argLine.append(" "); 3814 } 3815 } 3816 else if (a.isRequired() && (! a.hasDefaultValue())) 3817 { 3818 argLine.append("* "); 3819 } 3820 3821 boolean first = true; 3822 for (final Character c : a.getShortIdentifiers(false)) 3823 { 3824 if (first) 3825 { 3826 argLine.append('-'); 3827 first = false; 3828 } 3829 else 3830 { 3831 argLine.append(", -"); 3832 } 3833 argLine.append(c); 3834 } 3835 3836 for (final String s : a.getLongIdentifiers(false)) 3837 { 3838 if (first) 3839 { 3840 argLine.append("--"); 3841 first = false; 3842 } 3843 else 3844 { 3845 argLine.append(", --"); 3846 } 3847 argLine.append(s); 3848 } 3849 3850 final String valuePlaceholder = a.getValuePlaceholder(); 3851 if (valuePlaceholder != null) 3852 { 3853 argLine.append(' '); 3854 argLine.append(valuePlaceholder); 3855 } 3856 3857 // If we need to wrap the argument line, then align the dashes on the left 3858 // edge. 3859 int subsequentLineWidth = maxWidth - 4; 3860 if (subsequentLineWidth < 4) 3861 { 3862 subsequentLineWidth = maxWidth; 3863 } 3864 final List<String> identifierLines = 3865 StaticUtils.wrapLine(argLine.toString(), maxWidth, 3866 subsequentLineWidth); 3867 for (int i=0; i < identifierLines.size(); i++) 3868 { 3869 if (i == 0) 3870 { 3871 lines.add(identifierLines.get(0)); 3872 } 3873 else 3874 { 3875 lines.add(" " + identifierLines.get(i)); 3876 } 3877 } 3878 3879 3880 // The description should be wrapped, if necessary. We'll also want to 3881 // indent it (unless someone chose an absurdly small wrap width) to make 3882 // it stand out from the argument lines. 3883 final String description = a.getDescription(); 3884 if (maxWidth > 10) 3885 { 3886 final String indentString; 3887 if (indent) 3888 { 3889 indentString = " "; 3890 } 3891 else 3892 { 3893 indentString = " "; 3894 } 3895 3896 final List<String> descLines = StaticUtils.wrapLine(description, 3897 (maxWidth-indentString.length())); 3898 for (final String s : descLines) 3899 { 3900 lines.add(indentString + s); 3901 } 3902 } 3903 else 3904 { 3905 lines.addAll(StaticUtils.wrapLine(description, maxWidth)); 3906 } 3907 } 3908 3909 3910 3911 /** 3912 * Writes usage information for this program to the provided output stream 3913 * using the UTF-8 encoding, optionally wrapping long lines. 3914 * 3915 * @param outputStream The output stream to which the usage information 3916 * should be written. It must not be {@code null}. 3917 * @param maxWidth The maximum line width to use for the output. If 3918 * this is less than or equal to zero, then no wrapping 3919 * will be performed. 3920 * 3921 * @throws IOException If an error occurs while attempting to write to the 3922 * provided output stream. 3923 */ 3924 public void getUsage(@NotNull final OutputStream outputStream, 3925 final int maxWidth) 3926 throws IOException 3927 { 3928 final List<String> usageLines = getUsage(maxWidth); 3929 for (final String s : usageLines) 3930 { 3931 outputStream.write(StaticUtils.getBytes(s)); 3932 outputStream.write(StaticUtils.EOL_BYTES); 3933 } 3934 } 3935 3936 3937 3938 /** 3939 * Retrieves a string representation of the usage information. 3940 * 3941 * @param maxWidth The maximum line width to use for the output. If this is 3942 * less than or equal to zero, then no wrapping will be 3943 * performed. 3944 * 3945 * @return A string representation of the usage information 3946 */ 3947 @NotNull() 3948 public String getUsageString(final int maxWidth) 3949 { 3950 final StringBuilder buffer = new StringBuilder(); 3951 getUsageString(buffer, maxWidth); 3952 return buffer.toString(); 3953 } 3954 3955 3956 3957 /** 3958 * Appends a string representation of the usage information to the provided 3959 * buffer. 3960 * 3961 * @param buffer The buffer to which the information should be appended. 3962 * @param maxWidth The maximum line width to use for the output. If this is 3963 * less than or equal to zero, then no wrapping will be 3964 * performed. 3965 */ 3966 public void getUsageString(@NotNull final StringBuilder buffer, 3967 final int maxWidth) 3968 { 3969 for (final String line : getUsage(maxWidth)) 3970 { 3971 buffer.append(line); 3972 buffer.append(StaticUtils.EOL); 3973 } 3974 } 3975 3976 3977 3978 /** 3979 * Retrieves a string representation of this argument parser. 3980 * 3981 * @return A string representation of this argument parser. 3982 */ 3983 @Override() 3984 @NotNull() 3985 public String toString() 3986 { 3987 final StringBuilder buffer = new StringBuilder(); 3988 toString(buffer); 3989 return buffer.toString(); 3990 } 3991 3992 3993 3994 /** 3995 * Appends a string representation of this argument parser to the provided 3996 * buffer. 3997 * 3998 * @param buffer The buffer to which the information should be appended. 3999 */ 4000 public void toString(@NotNull final StringBuilder buffer) 4001 { 4002 buffer.append("ArgumentParser(commandName='"); 4003 buffer.append(commandName); 4004 buffer.append("', commandDescription={"); 4005 buffer.append('\''); 4006 buffer.append(commandDescription); 4007 buffer.append('\''); 4008 4009 if (additionalCommandDescriptionParagraphs != null) 4010 { 4011 for (final String additionalParagraph : 4012 additionalCommandDescriptionParagraphs) 4013 { 4014 buffer.append(", '"); 4015 buffer.append(additionalParagraph); 4016 buffer.append('\''); 4017 } 4018 } 4019 4020 buffer.append("}, minTrailingArgs="); 4021 buffer.append(minTrailingArgs); 4022 buffer.append(", maxTrailingArgs="); 4023 buffer.append(maxTrailingArgs); 4024 4025 if (trailingArgsPlaceholder != null) 4026 { 4027 buffer.append(", trailingArgsPlaceholder='"); 4028 buffer.append(trailingArgsPlaceholder); 4029 buffer.append('\''); 4030 } 4031 4032 buffer.append(", namedArgs={"); 4033 4034 final Iterator<Argument> iterator = namedArgs.iterator(); 4035 while (iterator.hasNext()) 4036 { 4037 iterator.next().toString(buffer); 4038 if (iterator.hasNext()) 4039 { 4040 buffer.append(", "); 4041 } 4042 } 4043 4044 buffer.append('}'); 4045 4046 if (! subCommands.isEmpty()) 4047 { 4048 buffer.append(", subCommands={"); 4049 4050 final Iterator<SubCommand> subCommandIterator = subCommands.iterator(); 4051 while (subCommandIterator.hasNext()) 4052 { 4053 subCommandIterator.next().toString(buffer); 4054 if (subCommandIterator.hasNext()) 4055 { 4056 buffer.append(", "); 4057 } 4058 } 4059 4060 buffer.append('}'); 4061 } 4062 4063 buffer.append(')'); 4064 } 4065}