/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.commons.cli; import java.io.BufferedReader; import java.io.IOException; import java.io.PrintWriter; import java.io.Serializable; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; /** * A formatter of help messages for command line options. * * <p>Example:</p> * * <pre> * Options options = new Options(); * options.addOption(OptionBuilder.withLongOpt("file") * .withDescription("The file to be processed") * .hasArg() * .withArgName("FILE") * .isRequired() * .create('f')); * options.addOption(OptionBuilder.withLongOpt("version") * .withDescription("Print the version of the application") * .create('v')); * options.addOption(OptionBuilder.withLongOpt("help").create('h')); * * String header = "Do something useful with an input file\n\n"; * String footer = "\nPlease report issues at http://example.com/issues"; * * HelpFormatter formatter = new HelpFormatter(); * formatter.printHelp("myapp", header, options, footer, true); * </pre> * * This produces the following output: * * <pre> * usage: myapp -f <FILE> [-h] [-v] * Do something useful with an input file * * -f,--file <FILE> The file to be processed * -h,--help * -v,--version Print the version of the application * * Please report issues at http://example.com/issues * </pre> * * @version $Id: HelpFormatter.java 1677407 2015-05-03 14:31:12Z britter $ */ public class HelpFormatter { // --------------------------------------------------------------- Constants /** default number of characters per line */ public static final int DEFAULT_WIDTH = 74; /** default padding to the left of each line */ public static final int DEFAULT_LEFT_PAD = 1; /** number of space characters to be prefixed to each description line */ public static final int DEFAULT_DESC_PAD = 3; /** the string to display at the beginning of the usage statement */ public static final String DEFAULT_SYNTAX_PREFIX = "usage: "; /** default prefix for shortOpts */ public static final String DEFAULT_OPT_PREFIX = "-"; /** default prefix for long Option */ public static final String DEFAULT_LONG_OPT_PREFIX = "--"; /** * default separator displayed between a long Option and its value * * @since 1.3 **/ public static final String DEFAULT_LONG_OPT_SEPARATOR = " "; /** default name for an argument */ public static final String DEFAULT_ARG_NAME = "arg"; // -------------------------------------------------------------- Attributes /** * number of characters per line * * @deprecated Scope will be made private for next major version * - use get/setWidth methods instead. */ @Deprecated public int defaultWidth = DEFAULT_WIDTH; /** * amount of padding to the left of each line * * @deprecated Scope will be made private for next major version * - use get/setLeftPadding methods instead. */ @Deprecated public int defaultLeftPad = DEFAULT_LEFT_PAD; /** * the number of characters of padding to be prefixed * to each description line * * @deprecated Scope will be made private for next major version * - use get/setDescPadding methods instead. */ @Deprecated public int defaultDescPad = DEFAULT_DESC_PAD; /** * the string to display at the beginning of the usage statement * * @deprecated Scope will be made private for next major version * - use get/setSyntaxPrefix methods instead. */ @Deprecated public String defaultSyntaxPrefix = DEFAULT_SYNTAX_PREFIX; /** * the new line string * * @deprecated Scope will be made private for next major version * - use get/setNewLine methods instead. */ @Deprecated public String defaultNewLine = System.getProperty("line.separator"); /** * the shortOpt prefix * * @deprecated Scope will be made private for next major version * - use get/setOptPrefix methods instead. */ @Deprecated public String defaultOptPrefix = DEFAULT_OPT_PREFIX; /** * the long Opt prefix * * @deprecated Scope will be made private for next major version * - use get/setLongOptPrefix methods instead. */ @Deprecated public String defaultLongOptPrefix = DEFAULT_LONG_OPT_PREFIX; /** * the name of the argument * * @deprecated Scope will be made private for next major version * - use get/setArgName methods instead. */ @Deprecated public String defaultArgName = DEFAULT_ARG_NAME; /** * Comparator used to sort the options when they output in help text * * Defaults to case-insensitive alphabetical sorting by option key */ protected Comparator<Option> optionComparator = new OptionComparator(); /** The separator displayed between the long option and its value. */ private String longOptSeparator = DEFAULT_LONG_OPT_SEPARATOR; /** * Sets the 'width'. * * @param width the new value of 'width' */ public void setWidth(int width) { this.defaultWidth = width; } /** * Returns the 'width'. * * @return the 'width' */ public int getWidth() { return defaultWidth; } /** * Sets the 'leftPadding'. * * @param padding the new value of 'leftPadding' */ public void setLeftPadding(int padding) { this.defaultLeftPad = padding; } /** * Returns the 'leftPadding'. * * @return the 'leftPadding' */ public int getLeftPadding() { return defaultLeftPad; } /** * Sets the 'descPadding'. * * @param padding the new value of 'descPadding' */ public void setDescPadding(int padding) { this.defaultDescPad = padding; } /** * Returns the 'descPadding'. * * @return the 'descPadding' */ public int getDescPadding() { return defaultDescPad; } /** * Sets the 'syntaxPrefix'. * * @param prefix the new value of 'syntaxPrefix' */ public void setSyntaxPrefix(String prefix) { this.defaultSyntaxPrefix = prefix; } /** * Returns the 'syntaxPrefix'. * * @return the 'syntaxPrefix' */ public String getSyntaxPrefix() { return defaultSyntaxPrefix; } /** * Sets the 'newLine'. * * @param newline the new value of 'newLine' */ public void setNewLine(String newline) { this.defaultNewLine = newline; } /** * Returns the 'newLine'. * * @return the 'newLine' */ public String getNewLine() { return defaultNewLine; } /** * Sets the 'optPrefix'. * * @param prefix the new value of 'optPrefix' */ public void setOptPrefix(String prefix) { this.defaultOptPrefix = prefix; } /** * Returns the 'optPrefix'. * * @return the 'optPrefix' */ public String getOptPrefix() { return defaultOptPrefix; } /** * Sets the 'longOptPrefix'. * * @param prefix the new value of 'longOptPrefix' */ public void setLongOptPrefix(String prefix) { this.defaultLongOptPrefix = prefix; } /** * Returns the 'longOptPrefix'. * * @return the 'longOptPrefix' */ public String getLongOptPrefix() { return defaultLongOptPrefix; } /** * Set the separator displayed between a long option and its value. * Ensure that the separator specified is supported by the parser used, * typically ' ' or '='. * * @param longOptSeparator the separator, typically ' ' or '='. * @since 1.3 */ public void setLongOptSeparator(String longOptSeparator) { this.longOptSeparator = longOptSeparator; } /** * Returns the separator displayed between a long option and its value. * * @return the separator * @since 1.3 */ public String getLongOptSeparator() { return longOptSeparator; } /** * Sets the 'argName'. * * @param name the new value of 'argName' */ public void setArgName(String name) { this.defaultArgName = name; } /** * Returns the 'argName'. * * @return the 'argName' */ public String getArgName() { return defaultArgName; } /** * Comparator used to sort the options when they output in help text. * Defaults to case-insensitive alphabetical sorting by option key. * * @return the {@link Comparator} currently in use to sort the options * @since 1.2 */ public Comparator<Option> getOptionComparator() { return optionComparator; } /** * Set the comparator used to sort the options when they output in help text. * Passing in a null comparator will keep the options in the order they were declared. * * @param comparator the {@link Comparator} to use for sorting the options * @since 1.2 */ public void setOptionComparator(Comparator<Option> comparator) { this.optionComparator = comparator; } /** * Print the help for <code>options</code> with the specified * command line syntax. This method prints help information to * System.out. * * @param cmdLineSyntax the syntax for this application * @param options the Options instance */ public void printHelp(String cmdLineSyntax, Options options) { printHelp(getWidth(), cmdLineSyntax, null, options, null, false); } /** * Print the help for <code>options</code> with the specified * command line syntax. This method prints help information to * System.out. * * @param cmdLineSyntax the syntax for this application * @param options the Options instance * @param autoUsage whether to print an automatically generated * usage statement */ public void printHelp(String cmdLineSyntax, Options options, boolean autoUsage) { printHelp(getWidth(), cmdLineSyntax, null, options, null, autoUsage); } /** * Print the help for <code>options</code> with the specified * command line syntax. This method prints help information to * System.out. * * @param cmdLineSyntax the syntax for this application * @param header the banner to display at the beginning of the help * @param options the Options instance * @param footer the banner to display at the end of the help */ public void printHelp(String cmdLineSyntax, String header, Options options, String footer) { printHelp(cmdLineSyntax, header, options, footer, false); } /** * Print the help for <code>options</code> with the specified * command line syntax. This method prints help information to * System.out. * * @param cmdLineSyntax the syntax for this application * @param header the banner to display at the beginning of the help * @param options the Options instance * @param footer the banner to display at the end of the help * @param autoUsage whether to print an automatically generated * usage statement */ public void printHelp(String cmdLineSyntax, String header, Options options, String footer, boolean autoUsage) { printHelp(getWidth(), cmdLineSyntax, header, options, footer, autoUsage); } /** * Print the help for <code>options</code> with the specified * command line syntax. This method prints help information to * System.out. * * @param width the number of characters to be displayed on each line * @param cmdLineSyntax the syntax for this application * @param header the banner to display at the beginning of the help * @param options the Options instance * @param footer the banner to display at the end of the help */ public void printHelp(int width, String cmdLineSyntax, String header, Options options, String footer) { printHelp(width, cmdLineSyntax, header, options, footer, false); } /** * Print the help for <code>options</code> with the specified * command line syntax. This method prints help information to * System.out. * * @param width the number of characters to be displayed on each line * @param cmdLineSyntax the syntax for this application * @param header the banner to display at the beginning of the help * @param options the Options instance * @param footer the banner to display at the end of the help * @param autoUsage whether to print an automatically generated * usage statement */ public void printHelp(int width, String cmdLineSyntax, String header, Options options, String footer, boolean autoUsage) { PrintWriter pw = new PrintWriter(System.out); printHelp(pw, width, cmdLineSyntax, header, options, getLeftPadding(), getDescPadding(), footer, autoUsage); pw.flush(); } /** * Print the help for <code>options</code> with the specified * command line syntax. * * @param pw the writer to which the help will be written * @param width the number of characters to be displayed on each line * @param cmdLineSyntax the syntax for this application * @param header the banner to display at the beginning of the help * @param options the Options instance * @param leftPad the number of characters of padding to be prefixed * to each line * @param descPad the number of characters of padding to be prefixed * to each description line * @param footer the banner to display at the end of the help * * @throws IllegalStateException if there is no room to print a line */ public void printHelp(PrintWriter pw, int width, String cmdLineSyntax, String header, Options options, int leftPad, int descPad, String footer) { printHelp(pw, width, cmdLineSyntax, header, options, leftPad, descPad, footer, false); } /** * Print the help for <code>options</code> with the specified * command line syntax. * * @param pw the writer to which the help will be written * @param width the number of characters to be displayed on each line * @param cmdLineSyntax the syntax for this application * @param header the banner to display at the beginning of the help * @param options the Options instance * @param leftPad the number of characters of padding to be prefixed * to each line * @param descPad the number of characters of padding to be prefixed * to each description line * @param footer the banner to display at the end of the help * @param autoUsage whether to print an automatically generated * usage statement * * @throws IllegalStateException if there is no room to print a line */ public void printHelp(PrintWriter pw, int width, String cmdLineSyntax, String header, Options options, int leftPad, int descPad, String footer, boolean autoUsage) { if (cmdLineSyntax == null || cmdLineSyntax.length() == 0) { throw new IllegalArgumentException("cmdLineSyntax not provided"); } if (autoUsage) { printUsage(pw, width, cmdLineSyntax, options); } else { printUsage(pw, width, cmdLineSyntax); } if (header != null && header.trim().length() > 0) { printWrapped(pw, width, header); } printOptions(pw, width, options, leftPad, descPad); if (footer != null && footer.trim().length() > 0) { printWrapped(pw, width, footer); } } /** * Prints the usage statement for the specified application. * * @param pw The PrintWriter to print the usage statement * @param width The number of characters to display per line * @param app The application name * @param options The command line Options */ public void printUsage(PrintWriter pw, int width, String app, Options options) { // initialise the string buffer StringBuffer buff = new StringBuffer(getSyntaxPrefix()).append(app).append(" "); // create a list for processed option groups Collection<OptionGroup> processedGroups = new ArrayList<OptionGroup>(); List<Option> optList = new ArrayList<Option>(options.getOptions()); if (getOptionComparator() != null) { Collections.sort(optList, getOptionComparator()); } // iterate over the options for (Iterator<Option> it = optList.iterator(); it.hasNext();) { // get the next Option Option option = it.next(); // check if the option is part of an OptionGroup OptionGroup group = options.getOptionGroup(option); // if the option is part of a group if (group != null) { // and if the group has not already been processed if (!processedGroups.contains(group)) { // add the group to the processed list processedGroups.add(group); // add the usage clause appendOptionGroup(buff, group); } // otherwise the option was displayed in the group // previously so ignore it. } // if the Option is not part of an OptionGroup else { appendOption(buff, option, option.isRequired()); } if (it.hasNext()) { buff.append(" "); } } // call printWrapped printWrapped(pw, width, buff.toString().indexOf(' ') + 1, buff.toString()); } /** * Appends the usage clause for an OptionGroup to a StringBuffer. * The clause is wrapped in square brackets if the group is required. * The display of the options is handled by appendOption * @param buff the StringBuffer to append to * @param group the group to append * @see #appendOption(StringBuffer,Option,boolean) */ private void appendOptionGroup(StringBuffer buff, OptionGroup group) { if (!group.isRequired()) { buff.append("["); } List<Option> optList = new ArrayList<Option>(group.getOptions()); if (getOptionComparator() != null) { Collections.sort(optList, getOptionComparator()); } // for each option in the OptionGroup for (Iterator<Option> it = optList.iterator(); it.hasNext();) { // whether the option is required or not is handled at group level appendOption(buff, it.next(), true); if (it.hasNext()) { buff.append(" | "); } } if (!group.isRequired()) { buff.append("]"); } } /** * Appends the usage clause for an Option to a StringBuffer. * * @param buff the StringBuffer to append to * @param option the Option to append * @param required whether the Option is required or not */ private void appendOption(StringBuffer buff, Option option, boolean required) { if (!required) { buff.append("["); } if (option.getOpt() != null) { buff.append("-").append(option.getOpt()); } else { buff.append("--").append(option.getLongOpt()); } // if the Option has a value and a non blank argname if (option.hasArg() && (option.getArgName() == null || option.getArgName().length() != 0)) { buff.append(option.getOpt() == null ? longOptSeparator : " "); buff.append("<").append(option.getArgName() != null ? option.getArgName() : getArgName()).append(">"); } // if the Option is not a required option if (!required) { buff.append("]"); } } /** * Print the cmdLineSyntax to the specified writer, using the * specified width. * * @param pw The printWriter to write the help to * @param width The number of characters per line for the usage statement. * @param cmdLineSyntax The usage statement. */ public void printUsage(PrintWriter pw, int width, String cmdLineSyntax) { int argPos = cmdLineSyntax.indexOf(' ') + 1; printWrapped(pw, width, getSyntaxPrefix().length() + argPos, getSyntaxPrefix() + cmdLineSyntax); } /** * Print the help for the specified Options to the specified writer, * using the specified width, left padding and description padding. * * @param pw The printWriter to write the help to * @param width The number of characters to display per line * @param options The command line Options * @param leftPad the number of characters of padding to be prefixed * to each line * @param descPad the number of characters of padding to be prefixed * to each description line */ public void printOptions(PrintWriter pw, int width, Options options, int leftPad, int descPad) { StringBuffer sb = new StringBuffer(); renderOptions(sb, width, options, leftPad, descPad); pw.println(sb.toString()); } /** * Print the specified text to the specified PrintWriter. * * @param pw The printWriter to write the help to * @param width The number of characters to display per line * @param text The text to be written to the PrintWriter */ public void printWrapped(PrintWriter pw, int width, String text) { printWrapped(pw, width, 0, text); } /** * Print the specified text to the specified PrintWriter. * * @param pw The printWriter to write the help to * @param width The number of characters to display per line * @param nextLineTabStop The position on the next line for the first tab. * @param text The text to be written to the PrintWriter */ public void printWrapped(PrintWriter pw, int width, int nextLineTabStop, String text) { StringBuffer sb = new StringBuffer(text.length()); renderWrappedTextBlock(sb, width, nextLineTabStop, text); pw.println(sb.toString()); } // --------------------------------------------------------------- Protected /** * Render the specified Options and return the rendered Options * in a StringBuffer. * * @param sb The StringBuffer to place the rendered Options into. * @param width The number of characters to display per line * @param options The command line Options * @param leftPad the number of characters of padding to be prefixed * to each line * @param descPad the number of characters of padding to be prefixed * to each description line * * @return the StringBuffer with the rendered Options contents. */ protected StringBuffer renderOptions(StringBuffer sb, int width, Options options, int leftPad, int descPad) { final String lpad = createPadding(leftPad); final String dpad = createPadding(descPad); // first create list containing only <lpad>-a,--aaa where // -a is opt and --aaa is long opt; in parallel look for // the longest opt string this list will be then used to // sort options ascending int max = 0; List<StringBuffer> prefixList = new ArrayList<StringBuffer>(); List<Option> optList = options.helpOptions(); if (getOptionComparator() != null) { Collections.sort(optList, getOptionComparator()); } for (Option option : optList) { StringBuffer optBuf = new StringBuffer(); if (option.getOpt() == null) { optBuf.append(lpad).append(" ").append(getLongOptPrefix()).append(option.getLongOpt()); } else { optBuf.append(lpad).append(getOptPrefix()).append(option.getOpt()); if (option.hasLongOpt()) { optBuf.append(',').append(getLongOptPrefix()).append(option.getLongOpt()); } } if (option.hasArg()) { String argName = option.getArgName(); if (argName != null && argName.length() == 0) { // if the option has a blank argname optBuf.append(' '); } else { optBuf.append(option.hasLongOpt() ? longOptSeparator : " "); optBuf.append("<").append(argName != null ? option.getArgName() : getArgName()).append(">"); } } prefixList.add(optBuf); max = optBuf.length() > max ? optBuf.length() : max; } int x = 0; for (Iterator<Option> it = optList.iterator(); it.hasNext();) { Option option = it.next(); StringBuilder optBuf = new StringBuilder(prefixList.get(x++).toString()); if (optBuf.length() < max) { optBuf.append(createPadding(max - optBuf.length())); } optBuf.append(dpad); int nextLineTabStop = max + descPad; if (option.getDescription() != null) { optBuf.append(option.getDescription()); } renderWrappedText(sb, width, nextLineTabStop, optBuf.toString()); if (it.hasNext()) { sb.append(getNewLine()); } } return sb; } /** * Render the specified text and return the rendered Options * in a StringBuffer. * * @param sb The StringBuffer to place the rendered text into. * @param width The number of characters to display per line * @param nextLineTabStop The position on the next line for the first tab. * @param text The text to be rendered. * * @return the StringBuffer with the rendered Options contents. */ protected StringBuffer renderWrappedText(StringBuffer sb, int width, int nextLineTabStop, String text) { int pos = findWrapPos(text, width, 0); if (pos == -1) { sb.append(rtrim(text)); return sb; } sb.append(rtrim(text.substring(0, pos))).append(getNewLine()); if (nextLineTabStop >= width) { // stops infinite loop happening nextLineTabStop = 1; } // all following lines must be padded with nextLineTabStop space characters final String padding = createPadding(nextLineTabStop); while (true) { text = padding + text.substring(pos).trim(); pos = findWrapPos(text, width, 0); if (pos == -1) { sb.append(text); return sb; } if (text.length() > width && pos == nextLineTabStop - 1) { pos = width; } sb.append(rtrim(text.substring(0, pos))).append(getNewLine()); } } /** * Render the specified text width a maximum width. This method differs * from renderWrappedText by not removing leading spaces after a new line. * * @param sb The StringBuffer to place the rendered text into. * @param width The number of characters to display per line * @param nextLineTabStop The position on the next line for the first tab. * @param text The text to be rendered. */ private Appendable renderWrappedTextBlock(StringBuffer sb, int width, int nextLineTabStop, String text) { try { BufferedReader in = new BufferedReader(new StringReader(text)); String line; boolean firstLine = true; while ((line = in.readLine()) != null) { if (!firstLine) { sb.append(getNewLine()); } else { firstLine = false; } renderWrappedText(sb, width, nextLineTabStop, line); } } catch (IOException e) //NOPMD { // cannot happen } return sb; } /** * Finds the next text wrap position after <code>startPos</code> for the * text in <code>text</code> with the column width <code>width</code>. * The wrap point is the last position before startPos+width having a * whitespace character (space, \n, \r). If there is no whitespace character * before startPos+width, it will return startPos+width. * * @param text The text being searched for the wrap position * @param width width of the wrapped text * @param startPos position from which to start the lookup whitespace * character * @return position on which the text must be wrapped or -1 if the wrap * position is at the end of the text */ protected int findWrapPos(String text, int width, int startPos) { // the line ends before the max wrap pos or a new line char found int pos = text.indexOf('\n', startPos); if (pos != -1 && pos <= width) { return pos + 1; } pos = text.indexOf('\t', startPos); if (pos != -1 && pos <= width) { return pos + 1; } if (startPos + width >= text.length()) { return -1; } // look for the last whitespace character before startPos+width for (pos = startPos + width; pos >= startPos; --pos) { final char c = text.charAt(pos); if (c == ' ' || c == '\n' || c == '\r') { break; } } // if we found it - just return if (pos > startPos) { return pos; } // if we didn't find one, simply chop at startPos+width pos = startPos + width; return pos == text.length() ? -1 : pos; } /** * Return a String of padding of length <code>len</code>. * * @param len The length of the String of padding to create. * * @return The String of padding */ protected String createPadding(int len) { char[] padding = new char[len]; Arrays.fill(padding, ' '); return new String(padding); } /** * Remove the trailing whitespace from the specified String. * * @param s The String to remove the trailing padding from. * * @return The String of without the trailing padding */ protected String rtrim(String s) { if (s == null || s.length() == 0) { return s; } int pos = s.length(); while (pos > 0 && Character.isWhitespace(s.charAt(pos - 1))) { --pos; } return s.substring(0, pos); } // ------------------------------------------------------ Package protected // ---------------------------------------------------------------- Private // ---------------------------------------------------------- Inner classes /** * This class implements the <code>Comparator</code> interface * for comparing Options. */ private static class OptionComparator implements Comparator<Option>, Serializable { /** The serial version UID. */ private static final long serialVersionUID = 5305467873966684014L; /** * Compares its two arguments for order. Returns a negative * integer, zero, or a positive integer as the first argument * is less than, equal to, or greater than the second. * * @param opt1 The first Option to be compared. * @param opt2 The second Option to be compared. * @return a negative integer, zero, or a positive integer as * the first argument is less than, equal to, or greater than the * second. */ public int compare(Option opt1, Option opt2) { return opt1.getKey().compareToIgnoreCase(opt2.getKey()); } } }