// Copyright 2014 Google Inc. All rights reserved. // // Licensed 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 com.google.devtools.common.options; import static com.google.devtools.common.options.OptionsParserImpl.findConverter; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Lists; import java.lang.reflect.Field; import java.text.BreakIterator; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * A renderer for usage messages. For now this is very simple. */ class OptionsUsage { private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n'); private static final Joiner COMMA_JOINER = Joiner.on(","); /** * Given an options class, render the usage string into the usage, * which is passed in as an argument. */ static void getUsage(Class optionsClass, StringBuilder usage) { List optionFields = Lists.newArrayList(OptionsParser.getAllAnnotatedFields(optionsClass)); Collections.sort(optionFields, BY_NAME); for (Field optionField : optionFields) { getUsage(optionField, usage, OptionsParser.HelpVerbosity.LONG); } } /** * Paragraph-fill the specified input text, indenting lines to 'indent' and * wrapping lines at 'width'. Returns the formatted result. */ static String paragraphFill(String in, int indent, int width) { String indentString = Strings.repeat(" ", indent); StringBuilder out = new StringBuilder(); String sep = ""; for (String paragraph : NEWLINE_SPLITTER.split(in)) { BreakIterator boundary = BreakIterator.getLineInstance(); // (factory) boundary.setText(paragraph); out.append(sep).append(indentString); int cursor = indent; for (int start = boundary.first(), end = boundary.next(); end != BreakIterator.DONE; start = end, end = boundary.next()) { String word = paragraph.substring(start, end); // (may include trailing space) if (word.length() + cursor > width) { out.append('\n').append(indentString); cursor = indent; } out.append(word); cursor += word.length(); } sep = "\n"; } return out.toString(); } /** * Append the usage message for a single option-field message to 'usage'. */ static void getUsage(Field optionField, StringBuilder usage, OptionsParser.HelpVerbosity helpVerbosity) { String flagName = getFlagName(optionField); String typeDescription = getTypeDescription(optionField); Option annotation = optionField.getAnnotation(Option.class); usage.append(" --" + flagName); if (helpVerbosity == OptionsParser.HelpVerbosity.SHORT) { // just the name usage.append('\n'); return; } if (annotation.abbrev() != '\0') { usage.append(" [-").append(annotation.abbrev()).append(']'); } if (!typeDescription.equals("")) { usage.append(" (" + typeDescription + "; "); if (annotation.allowMultiple()) { usage.append("may be used multiple times"); } else { // Don't call the annotation directly (we must allow overrides to certain defaults) String defaultValueString = OptionsParserImpl.getDefaultOptionString(optionField); if (OptionsParserImpl.isSpecialNullDefault(defaultValueString, optionField)) { usage.append("default: see description"); } else { usage.append("default: \"" + defaultValueString + "\""); } } usage.append(")"); } usage.append("\n"); if (helpVerbosity == OptionsParser.HelpVerbosity.MEDIUM) { // just the name and type. return; } if (!annotation.help().equals("")) { usage.append(paragraphFill(annotation.help(), 4, 80)); // (indent, width) usage.append('\n'); } if (annotation.expansion().length > 0) { StringBuilder expandsMsg = new StringBuilder("Expands to: "); for (String exp : annotation.expansion()) { expandsMsg.append(exp).append(" "); } usage.append(paragraphFill(expandsMsg.toString(), 4, 80)); // (indent, width) usage.append('\n'); } } /** * Returns the available completion for the given option field. The completions are the exact * command line option (with the prepending '--') that one should pass. It is suitable for * completion script to use. If the option expect an argument, the kind of argument is given * after the equals. If the kind is a enum, the various enum values are given inside an accolade * in a comma separated list. For other special kind, the type is given as a name (e.g., * label, float, path...). Example outputs of this * function are for, respectively, a tristate flag tristate_flag, a enum * flag enum_flag which can take value1, value2 and * value3, a path fragment flag path_flag, a string flag * string_flag and a void flag void_flag: *
   *   --tristate_flag={auto,yes,no}
   *   --notristate_flag
   *   --enum_flag={value1,value2,value3}
   *   --path_flag=path
   *   --string_flag=
   *   --void_flag
   * 
* * @param field The field to return completion for * @param builder the string builder to store the completion values */ static void getCompletion(Field field, StringBuilder builder) { // Return the list of possible completions for this option String flagName = field.getAnnotation(Option.class).name(); Class fieldType = field.getType(); builder.append("--").append(flagName); if (fieldType.equals(boolean.class)) { builder.append("\n"); builder.append("--no").append(flagName).append("\n"); } else if (fieldType.equals(TriState.class)) { builder.append("={auto,yes,no}\n"); builder.append("--no").append(flagName).append("\n"); } else if (fieldType.isEnum()) { builder.append("={") .append(COMMA_JOINER.join(fieldType.getEnumConstants()).toLowerCase()).append("}\n"); } else if (fieldType.getSimpleName().equals("Label")) { // String comparison so we don't introduce a dependency to com.google.devtools.build.lib. builder.append("=label\n"); } else if (fieldType.getSimpleName().equals("PathFragment")) { builder.append("=path\n"); } else if (Void.class.isAssignableFrom(fieldType)) { builder.append("\n"); } else { // TODO(bazel-team): add more types. Maybe even move the completion type // to the @Option annotation? builder.append("=\n"); } } private static final Comparator BY_NAME = new Comparator() { @Override public int compare(Field left, Field right) { return left.getName().compareTo(right.getName()); } }; /** * An ordering relation for option-field fields that first groups together * options of the same category, then sorts by name within the category. */ static final Comparator BY_CATEGORY = new Comparator() { @Override public int compare(Field left, Field right) { int r = left.getAnnotation(Option.class).category().compareTo( right.getAnnotation(Option.class).category()); return r == 0 ? BY_NAME.compare(left, right) : r; } }; private static String getTypeDescription(Field optionsField) { return findConverter(optionsField).getTypeDescription(); } static String getFlagName(Field field) { String name = field.getAnnotation(Option.class).name(); return OptionsParserImpl.isBooleanField(field) ? "[no]" + name : name; } }