/*
 * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
 * which is available at https://www.apache.org/licenses/LICENSE-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
 */

package io.vertx.core.cli;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.*;
import java.util.stream.Collectors;


Usage message formatter.
Author:Clement Escoffier
/** * Usage message formatter. * * @author Clement Escoffier <clement@apache.org> */
public class UsageMessageFormatter {
default number of characters per line
/** * default number of characters per line */
public static final int DEFAULT_WIDTH = 80;
default padding to the left of each line
/** * 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
/** * 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
/** * the string to display at the beginning of the usage statement */
public static final String DEFAULT_USAGE_PREFIX = "Usage: ";
default prefix for shortOpts
/** * default prefix for shortOpts */
public static final String DEFAULT_OPT_PREFIX = "-";
default prefix for long Option
/** * default prefix for long Option */
public static final String DEFAULT_LONG_OPT_PREFIX = "--";
default separator displayed between a long Option and its value
/** * default separator displayed between a long Option and its value */
public static final String DEFAULT_LONG_OPT_SEPARATOR = " ";
default name for an argument
/** * default name for an argument */
public static final String DEFAULT_ARG_NAME = "arg"; private int width = DEFAULT_WIDTH; private int leftPad = DEFAULT_LEFT_PAD; private int descPad = DEFAULT_DESC_PAD; private String usagePrefix = DEFAULT_USAGE_PREFIX; private String newLine = System.lineSeparator(); private String defaultOptionPrefix = DEFAULT_OPT_PREFIX; private String defaultLongOptPrefix = DEFAULT_LONG_OPT_PREFIX; private String defaultArgName = DEFAULT_ARG_NAME; private String longOptSeparator = DEFAULT_LONG_OPT_SEPARATOR;
Comparator used to sort the options when they output in help text

Defaults to case-insensitive alphabetical sorting by option key.
/** * Comparator used to sort the options when they output in help text * <p/> * Defaults to case-insensitive alphabetical sorting by option key. */
protected Comparator<Option> optionComparator = (opt1, opt2) -> opt1.getName().compareToIgnoreCase(opt2.getName()); public void setWidth(int width) { this.width = width; } public int getWidth() { return width; } public void setLeftPadding(int padding) { this.leftPad = padding; } public int getLeftPadding() { return leftPad; } public void setDescPadding(int padding) { this.descPad = padding; } public int getDescPadding() { return descPad; } public void setUsagePrefix(String prefix) { this.usagePrefix = prefix; } public String getUsagePrefix() { return usagePrefix; } public void setNewLine(String newline) { this.newLine = newline; } public String getNewLine() { return newLine; } public void setOptionPrefix(String prefix) { this.defaultOptionPrefix = prefix; } public String getOptionPrefix() { return defaultOptionPrefix; } public void setLongOptionPrefix(String prefix) { this.defaultLongOptPrefix = prefix; } public String getLongOptionPrefix() { 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 '='.
Params:
  • longOptSeparator – the separator, typically ' ' or '='.
/** * 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 '='. */
public void setLongOptionSeparator(String longOptSeparator) { this.longOptSeparator = longOptSeparator; }
Returns the separator displayed between a long option and its value.
Returns:the separator
/** * Returns the separator displayed between a long option and its value. * * @return the separator */
public String getLongOptionSeparator() { return longOptSeparator; } public void setArgName(String name) { this.defaultArgName = name; } 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.
Returns:the Comparator currently in use to sort the options
/** * 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 */
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.
Params:
  • comparator – the Comparator to use for sorting the options
/** * 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 */
public void setOptionComparator(Comparator<Option> comparator) { this.optionComparator = comparator; }
Appends the usage clause for an Option to a StringBuilder.
Params:
  • buff – the StringBuilder to append to
  • option – the Option to append
/** * Appends the usage clause for an Option to a StringBuilder. * * @param buff the StringBuilder to append to * @param option the Option to append */
protected void appendOption(StringBuilder buff, Option option) { if (option.isHidden()) { return; } if (!option.isRequired()) { buff.append("["); } if (!isNullOrEmpty(option.getShortName())) { buff.append("-").append(option.getShortName()); } else { buff.append("--").append(option.getLongName()); } if (!option.getChoices().isEmpty()) { buff.append(isNullOrEmpty(option.getShortName()) ? getLongOptionSeparator() : " "); buff.append(option.getChoices().stream().collect(Collectors.joining(", ", "{", "}"))); } else { // if the Option accepts values and a non blank argname if (option.acceptValue() && (option.getArgName() == null || option.getArgName().length() != 0)) { buff.append(isNullOrEmpty(option.getShortName()) ? getLongOptionSeparator() : " "); buff.append("<").append(option.getArgName() != null ? option.getArgName() : getArgName()).append(">"); } } // if the Option is not a required option if (!option.isRequired()) { buff.append("]"); } }
Appends the usage clause for an Argument to a StringBuilder.
Params:
  • buff – the StringBuilder to append to
  • argument – the argument to add
  • required – whether the Option is required or not
/** * Appends the usage clause for an Argument to a StringBuilder. * * @param buff the StringBuilder to append to * @param argument the argument to add * @param required whether the Option is required or not */
protected void appendArgument(StringBuilder buff, Argument argument, boolean required) { if (argument.isHidden()) { return; } if (!required) { buff.append("["); } buff.append(argument.getArgName()); if (argument.isMultiValued()) { buff.append("..."); } // if the Option is not a required option if (!required) { buff.append("]"); } }
Computes the usage of the given CLI.
Params:
  • builder – where the usage is going to be written
  • cli – the cli
/** * Computes the usage of the given {@link CLI}. * * @param builder where the usage is going to be written * @param cli the cli */
public void usage(StringBuilder builder, CLI cli) { usage(builder, null, cli); }
Computes the usage of the given CLI.
Params:
  • builder – where the usage is going to be written
  • prefix – a prefix to prepend to the usage line. It will be added between 'Usage: ' and the CLI name.
  • cli – the cli
/** * Computes the usage of the given {@link CLI}. * * @param builder where the usage is going to be written * @param prefix a prefix to prepend to the usage line. It will be added between 'Usage: ' and the CLI name. * @param cli the cli */
public void usage(StringBuilder builder, String prefix, CLI cli) { computeUsageLine(builder, prefix, cli); if (cli.getSummary() != null && cli.getSummary().trim().length() > 0) { buildWrapped(builder, "\n" + cli.getSummary()); } if (cli.getDescription() != null && cli.getDescription().trim().length() > 0) { buildWrapped(builder, "\n" + cli.getDescription()); } builder.append("\n"); if (cli.getOptions().isEmpty() && cli.getArguments().isEmpty()) { // When we have neither options and arguments, just leave. return; } builder.append("Options and Arguments:\n"); computeOptionsAndArguments(builder, cli.getOptions(), cli.getArguments()); } public void computeUsage(StringBuilder buffer, String cmdLineSyntax) { int argPos = cmdLineSyntax.indexOf(' ') + 1; buildWrapped(buffer, getUsagePrefix().length() + argPos, getUsagePrefix() + cmdLineSyntax); } public void computeUsageLine(StringBuilder buffer, String prefix, CLI cli) { // initialise the string buffer StringBuilder buff; if (prefix == null) { buff = new StringBuilder(getUsagePrefix()); } else { buff = new StringBuilder(getUsagePrefix()).append(prefix); if (!prefix.endsWith(" ")) { buff.append(" "); } } buff.append(cli.getName()).append(" "); if (getOptionComparator() != null) { Collections.sort(cli.getOptions(), getOptionComparator()); } // iterate over the options for (Option option : cli.getOptions()) { appendOption(buff, option); buff.append(" "); } // iterate over the arguments for (Argument arg : cli.getArguments()) { appendArgument(buff, arg, arg.isRequired()); buff.append(" "); } buildWrapped(buffer, buff.toString().indexOf(' ') + 1, buff.toString()); }
Computes the help for the specified Options to the specified writer.
Params:
  • buffer – The buffer to write the help to
  • options – The command line Options
  • arguments – the command line Arguments
/** * Computes the help for the specified Options to the specified writer. * * @param buffer The buffer to write the help to * @param options The command line Options * @param arguments the command line Arguments */
public void computeOptionsAndArguments(StringBuilder buffer, List<Option> options, List<Argument> arguments) { renderOptionsAndArguments(buffer, options, arguments); buffer.append(newLine); }
Builds the specified text to the specified buffer.
Params:
  • buffer – The buffer to write the help to
  • text – The text to be written to the buffer
/** * Builds the specified text to the specified buffer. * * @param buffer The buffer to write the help to * @param text The text to be written to the buffer */
public void buildWrapped(StringBuilder buffer, String text) { buildWrapped(buffer, 0, text); }
Builds the specified text to the specified buffer.
Params:
  • buffer – The buffer to write the help to
  • nextLineTabStop – The position on the next line for the first tab.
  • text – The text to be written to the buffer
/** * Builds the specified text to the specified buffer. * * @param buffer The buffer to write the help to * @param nextLineTabStop The position on the next line for the first tab. * @param text The text to be written to the buffer */
public void buildWrapped(StringBuilder buffer, int nextLineTabStop, String text) { renderWrappedTextBlock(buffer, width, nextLineTabStop, text); buffer.append(newLine); } protected StringBuilder renderCommands(StringBuilder sb, Collection<CLI> commands) { final String lpad = createPadding(leftPad); final String dpad = createPadding(descPad); // We need a double loop to compute the longest command name int max = 0; List<StringBuilder> prefixList = new ArrayList<>(); for (CLI command : commands) { if (!command.isHidden()) { StringBuilder buf = new StringBuilder(); buf.append(lpad).append(" ").append(command.getName()); prefixList.add(buf); max = Math.max(buf.length(), max); } } int x = 0; // Use an iterator to detect the last item. for (Iterator<CLI> it = commands.iterator(); it.hasNext(); ) { CLI command = it.next(); if (command.isHidden()) { continue; } StringBuilder buf = new StringBuilder(prefixList.get(x++).toString()); if (buf.length() < max) { buf.append(createPadding(max - buf.length())); } buf.append(dpad); int nextLineTabStop = max + descPad; buf.append(command.getSummary()); renderWrappedText(sb, width, nextLineTabStop, buf.toString()); if (it.hasNext()) { sb.append(getNewLine()); } } return sb; } public static boolean isNullOrEmpty(String s) { return s == null || s.trim().length() == 0; }
Renders the specified Options and Arguments and return the rendered output in a StringBuilder.
Params:
  • sb – The StringBuilder to place the rendered Options and Arguments into.
  • options – The command line Options
  • arguments – The command line Arguments
Returns:the StringBuilder with the rendered content.
/** * Renders the specified Options and Arguments and return the rendered output * in a StringBuilder. * * @param sb The StringBuilder to place the rendered Options and Arguments into. * @param options The command line Options * @param arguments The command line Arguments * @return the StringBuilder with the rendered content. */
protected StringBuilder renderOptionsAndArguments(StringBuilder sb, List<Option> options, List<Argument> arguments) { 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<StringBuilder> prefixList = new ArrayList<>(); if (getOptionComparator() != null) { Collections.sort(options, getOptionComparator()); } for (Option option : options) { StringBuilder buf = new StringBuilder(); if (option.isHidden()) { continue; } if (isNullOrEmpty(option.getShortName())) { buf.append(lpad).append(" ").append(getLongOptionPrefix()).append(option.getLongName()); } else { buf.append(lpad).append(getOptionPrefix()).append(option.getShortName()); if (!isNullOrEmpty(option.getLongName())) { buf.append(',').append(getLongOptionPrefix()).append(option.getLongName()); } } if (!option.getChoices().isEmpty()) { buf.append(!isNullOrEmpty(option.getLongName()) ? longOptSeparator : " "); buf.append(option.getChoices().stream().collect(Collectors.joining(", ", "{", "}"))); } else if (option.acceptValue()) { String argName = option.getArgName(); if (argName != null && argName.length() == 0) { // if the option has a blank argname buf.append(' '); } else { buf.append(!isNullOrEmpty(option.getLongName()) ? longOptSeparator : " "); buf.append("<").append(argName != null ? option.getArgName() : getArgName()).append(">"); } } prefixList.add(buf); max = Math.max(buf.length(), max); } for (Argument argument : arguments) { StringBuilder buf = new StringBuilder(); if (argument.isHidden()) { continue; } buf.append(lpad).append("<").append(argument.getArgName()).append(">"); prefixList.add(buf); max = Math.max(buf.length(), max); } int x = 0; // Append options - Use an iterator to detect the last item. for (Iterator<Option> it = options.iterator(); it.hasNext(); ) { Option option = it.next(); if (option.isHidden()) { continue; } 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()); } } // Append arguments - Use an iterator to detect the last item. if (!options.isEmpty() && !arguments.isEmpty()) { sb.append(getNewLine()); } for (Iterator<Argument> it = arguments.iterator(); it.hasNext(); ) { Argument argument = it.next(); if (argument.isHidden()) { continue; } StringBuilder argBuf = new StringBuilder(prefixList.get(x++).toString()); if (argBuf.length() < max) { argBuf.append(createPadding(max - argBuf.length())); } argBuf.append(dpad); int nextLineTabStop = max + descPad; if (argument.getDescription() != null) { argBuf.append(argument.getDescription()); } renderWrappedText(sb, width, nextLineTabStop, argBuf.toString()); if (it.hasNext()) { sb.append(getNewLine()); } } return sb; }
Render the specified text and return the rendered Options in a StringBuilder.
Params:
  • sb – The StringBuilder to place the rendered text into.
  • width – The number of characters to display per line
  • nextLineTabStop – The position on the next line for the first tab.
  • text – The text to be rendered.
Returns:the StringBuilder with the rendered Options contents.
/** * Render the specified text and return the rendered Options * in a StringBuilder. * * @param sb The StringBuilder 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 StringBuilder with the rendered Options contents. */
protected StringBuilder renderWrappedText(StringBuilder 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()); } }
Renders the specified text width a maximum width. This method differs from renderWrappedText by not removing leading spaces after a new line.
Params:
  • sb – The StringBuilder to place the rendered text into.
  • width – The number of characters to display per line
  • nextLineTabStop – The position on the next line for the first tab.
  • text – The text to be rendered.
/** * Renders the specified text width a maximum width. This method differs * from renderWrappedText by not removing leading spaces after a new line. * * @param sb The StringBuilder 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. */
public Appendable renderWrappedTextBlock(StringBuilder 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 startPos for the text in text with the column width width. 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.
Params:
  • text – The text being searched for the wrap position
  • width – width of the wrapped text
  • startPos – position from which to start the lookup whitespace character
Returns:position on which the text must be wrapped or -1 if the wrap position is at the end of the text
/** * 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 */
public static 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 len.
Params:
  • len – The length of the String of padding to create.
Returns:The String of padding
/** * 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 */
public static String createPadding(int len) { char[] padding = new char[len]; Arrays.fill(padding, ' '); return new String(padding); }
Remove the trailing whitespace from the specified String.
Params:
  • s – The String to remove the trailing padding from.
Returns:The String of without the trailing 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 */
public static 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); } }