1 // Copyright 2014 The Bazel Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package com.google.devtools.common.options; 15 16 import com.google.common.base.Joiner; 17 import com.google.common.base.Splitter; 18 import com.google.common.base.Strings; 19 import com.google.common.escape.Escaper; 20 import java.lang.reflect.Field; 21 import java.text.BreakIterator; 22 import java.util.ArrayList; 23 import java.util.Collections; 24 import java.util.Comparator; 25 import java.util.List; 26 import javax.annotation.Nullable; 27 28 /** 29 * A renderer for usage messages. For now this is very simple. 30 */ 31 class OptionsUsage { 32 33 private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n'); 34 private static final Joiner COMMA_JOINER = Joiner.on(","); 35 36 /** 37 * Given an options class, render the usage string into the usage, which is passed in as an 38 * argument. This will not include information about expansions for options using expansion 39 * functions (it would be unsafe to report this as we cannot know what options from other {@link 40 * OptionsBase} subclasses they depend on until a complete parser is constructed). 41 */ getUsage(Class<? extends OptionsBase> optionsClass, StringBuilder usage)42 static void getUsage(Class<? extends OptionsBase> optionsClass, StringBuilder usage) { 43 OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass); 44 List<Field> optionFields = new ArrayList<>(data.getFieldsForClass(optionsClass)); 45 Collections.sort(optionFields, BY_NAME); 46 for (Field optionField : optionFields) { 47 getUsage(optionField, usage, OptionsParser.HelpVerbosity.LONG, null); 48 } 49 } 50 51 /** 52 * Paragraph-fill the specified input text, indenting lines to 'indent' and 53 * wrapping lines at 'width'. Returns the formatted result. 54 */ paragraphFill(String in, int indent, int width)55 static String paragraphFill(String in, int indent, int width) { 56 String indentString = Strings.repeat(" ", indent); 57 StringBuilder out = new StringBuilder(); 58 String sep = ""; 59 for (String paragraph : NEWLINE_SPLITTER.split(in)) { 60 BreakIterator boundary = BreakIterator.getLineInstance(); // (factory) 61 boundary.setText(paragraph); 62 out.append(sep).append(indentString); 63 int cursor = indent; 64 for (int start = boundary.first(), end = boundary.next(); 65 end != BreakIterator.DONE; 66 start = end, end = boundary.next()) { 67 String word = 68 paragraph.substring(start, end); // (may include trailing space) 69 if (word.length() + cursor > width) { 70 out.append('\n').append(indentString); 71 cursor = indent; 72 } 73 out.append(word); 74 cursor += word.length(); 75 } 76 sep = "\n"; 77 } 78 return out.toString(); 79 } 80 81 /** 82 * Returns the expansion for an option, to the extent known. Precisely, if an {@link OptionsData} 83 * object is supplied, the expansion is read from that. Otherwise, the annotation is inspected: If 84 * the annotation uses {@link Option#expansion} it is returned, and if it uses {@link 85 * Option#expansionFunction} null is returned, indicating a lack of definite information. In all 86 * cases, when the option is not an expansion option, an empty array is returned. 87 */ getExpansionIfKnown( Field optionField, Option annotation, @Nullable OptionsData optionsData)88 private static @Nullable String[] getExpansionIfKnown( 89 Field optionField, Option annotation, @Nullable OptionsData optionsData) { 90 if (optionsData != null) { 91 return optionsData.getEvaluatedExpansion(optionField); 92 } else { 93 if (OptionsData.usesExpansionFunction(annotation)) { 94 return null; 95 } else { 96 // Empty array if it's not an expansion option. 97 return annotation.expansion(); 98 } 99 } 100 } 101 102 /** 103 * Appends the usage message for a single option-field message to 'usage'. If {@code optionsData} 104 * is not supplied, options that use expansion functions won't be fully described. 105 */ getUsage( Field optionField, StringBuilder usage, OptionsParser.HelpVerbosity helpVerbosity, @Nullable OptionsData optionsData)106 static void getUsage( 107 Field optionField, 108 StringBuilder usage, 109 OptionsParser.HelpVerbosity helpVerbosity, 110 @Nullable OptionsData optionsData) { 111 String flagName = getFlagName(optionField); 112 String typeDescription = getTypeDescription(optionField); 113 Option annotation = optionField.getAnnotation(Option.class); 114 usage.append(" --" + flagName); 115 if (helpVerbosity == OptionsParser.HelpVerbosity.SHORT) { // just the name 116 usage.append('\n'); 117 return; 118 } 119 if (annotation.abbrev() != '\0') { 120 usage.append(" [-").append(annotation.abbrev()).append(']'); 121 } 122 if (!typeDescription.equals("")) { 123 usage.append(" (" + typeDescription + "; "); 124 if (annotation.allowMultiple()) { 125 usage.append("may be used multiple times"); 126 } else { 127 // Don't call the annotation directly (we must allow overrides to certain defaults) 128 String defaultValueString = OptionsParserImpl.getDefaultOptionString(optionField); 129 if (OptionsParserImpl.isSpecialNullDefault(defaultValueString, optionField)) { 130 usage.append("default: see description"); 131 } else { 132 usage.append("default: \"" + defaultValueString + "\""); 133 } 134 } 135 usage.append(")"); 136 } 137 usage.append("\n"); 138 if (helpVerbosity == OptionsParser.HelpVerbosity.MEDIUM) { // just the name and type. 139 return; 140 } 141 if (!annotation.help().equals("")) { 142 usage.append(paragraphFill(annotation.help(), 4, 80)); // (indent, width) 143 usage.append('\n'); 144 } 145 String[] expansion = getExpansionIfKnown(optionField, annotation, optionsData); 146 if (expansion == null) { 147 usage.append(" Expands to unknown options.\n"); 148 } else if (expansion.length > 0) { 149 StringBuilder expandsMsg = new StringBuilder("Expands to: "); 150 for (String exp : expansion) { 151 expandsMsg.append(exp).append(" "); 152 } 153 usage.append(paragraphFill(expandsMsg.toString(), 4, 80)); // (indent, width) 154 usage.append('\n'); 155 } 156 } 157 158 /** 159 * Append the usage message for a single option-field message to 'usage'. If {@code optionsData} 160 * is not supplied, options that use expansion functions won't be fully described. 161 */ getUsageHtml( Field optionField, StringBuilder usage, Escaper escaper, @Nullable OptionsData optionsData)162 static void getUsageHtml( 163 Field optionField, StringBuilder usage, Escaper escaper, @Nullable OptionsData optionsData) { 164 String plainFlagName = optionField.getAnnotation(Option.class).name(); 165 String flagName = getFlagName(optionField); 166 String valueDescription = optionField.getAnnotation(Option.class).valueHelp(); 167 String typeDescription = getTypeDescription(optionField); 168 Option annotation = optionField.getAnnotation(Option.class); 169 usage.append("<dt><code><a name=\"flag--").append(plainFlagName).append("\"></a>--"); 170 usage.append(flagName); 171 if (OptionsData.isBooleanField(optionField) || OptionsData.isVoidField(optionField)) { 172 // Nothing for boolean, tristate, boolean_or_enum, or void options. 173 } else if (!valueDescription.isEmpty()) { 174 usage.append("=").append(escaper.escape(valueDescription)); 175 } else if (!typeDescription.isEmpty()) { 176 // Generic fallback, which isn't very good. 177 usage.append("=<").append(escaper.escape(typeDescription)).append(">"); 178 } 179 usage.append("</code>"); 180 if (annotation.abbrev() != '\0') { 181 usage.append(" [<code>-").append(annotation.abbrev()).append("</code>]"); 182 } 183 if (annotation.allowMultiple()) { 184 // Allow-multiple options can't have a default value. 185 usage.append(" multiple uses are accumulated"); 186 } else { 187 // Don't call the annotation directly (we must allow overrides to certain defaults). 188 String defaultValueString = OptionsParserImpl.getDefaultOptionString(optionField); 189 if (OptionsData.isVoidField(optionField)) { 190 // Void options don't have a default. 191 } else if (OptionsParserImpl.isSpecialNullDefault(defaultValueString, optionField)) { 192 usage.append(" default: see description"); 193 } else { 194 usage.append(" default: \"").append(escaper.escape(defaultValueString)).append("\""); 195 } 196 } 197 usage.append("</dt>\n"); 198 usage.append("<dd>\n"); 199 if (!annotation.help().isEmpty()) { 200 usage.append(paragraphFill(escaper.escape(annotation.help()), 0, 80)); // (indent, width) 201 usage.append('\n'); 202 } 203 String[] expansion = getExpansionIfKnown(optionField, annotation, optionsData); 204 if (expansion == null) { 205 usage.append(" Expands to unknown options.<br>\n"); 206 } else if (expansion.length > 0) { 207 usage.append("<br/>\n"); 208 StringBuilder expandsMsg = new StringBuilder("Expands to:<br/>\n"); 209 for (String exp : expansion) { 210 // TODO(ulfjack): Can we link to the expanded flags here? 211 expandsMsg 212 .append(" <code>") 213 .append(escaper.escape(exp)) 214 .append("</code><br/>\n"); 215 } 216 usage.append(expandsMsg.toString()); // (indent, width) 217 usage.append('\n'); 218 } 219 usage.append("</dd>\n"); 220 } 221 222 /** 223 * Returns the available completion for the given option field. The completions are the exact 224 * command line option (with the prepending '--') that one should pass. It is suitable for 225 * completion script to use. If the option expect an argument, the kind of argument is given 226 * after the equals. If the kind is a enum, the various enum values are given inside an accolade 227 * in a comma separated list. For other special kind, the type is given as a name (e.g., 228 * <code>label</code>, <code>float</ode>, <code>path</code>...). Example outputs of this 229 * function are for, respectively, a tristate flag <code>tristate_flag</code>, a enum 230 * flag <code>enum_flag</code> which can take <code>value1</code>, <code>value2</code> and 231 * <code>value3</code>, a path fragment flag <code>path_flag</code>, a string flag 232 * <code>string_flag</code> and a void flag <code>void_flag</code>: 233 * <pre> 234 * --tristate_flag={auto,yes,no} 235 * --notristate_flag 236 * --enum_flag={value1,value2,value3} 237 * --path_flag=path 238 * --string_flag= 239 * --void_flag 240 * </pre> 241 * 242 * @param field The field to return completion for 243 * @param builder the string builder to store the completion values 244 */ getCompletion(Field field, StringBuilder builder)245 static void getCompletion(Field field, StringBuilder builder) { 246 // Return the list of possible completions for this option 247 String flagName = field.getAnnotation(Option.class).name(); 248 Class<?> fieldType = field.getType(); 249 builder.append("--").append(flagName); 250 if (fieldType.equals(boolean.class)) { 251 builder.append("\n"); 252 builder.append("--no").append(flagName).append("\n"); 253 } else if (fieldType.equals(TriState.class)) { 254 builder.append("={auto,yes,no}\n"); 255 builder.append("--no").append(flagName).append("\n"); 256 } else if (fieldType.isEnum()) { 257 builder.append("={") 258 .append(COMMA_JOINER.join(fieldType.getEnumConstants()).toLowerCase()).append("}\n"); 259 } else if (fieldType.getSimpleName().equals("Label")) { 260 // String comparison so we don't introduce a dependency to com.google.devtools.build.lib. 261 builder.append("=label\n"); 262 } else if (fieldType.getSimpleName().equals("PathFragment")) { 263 builder.append("=path\n"); 264 } else if (Void.class.isAssignableFrom(fieldType)) { 265 builder.append("\n"); 266 } else { 267 // TODO(bazel-team): add more types. Maybe even move the completion type 268 // to the @Option annotation? 269 builder.append("=\n"); 270 } 271 } 272 273 // TODO(brandjon): Should this use sorting by option name instead of field name? 274 private static final Comparator<Field> BY_NAME = new Comparator<Field>() { 275 @Override 276 public int compare(Field left, Field right) { 277 return left.getName().compareTo(right.getName()); 278 } 279 }; 280 281 /** 282 * An ordering relation for option-field fields that first groups together 283 * options of the same category, then sorts by name within the category. 284 */ 285 static final Comparator<Field> BY_CATEGORY = new Comparator<Field>() { 286 @Override 287 public int compare(Field left, Field right) { 288 int r = left.getAnnotation(Option.class).category().compareTo( 289 right.getAnnotation(Option.class).category()); 290 return r == 0 ? BY_NAME.compare(left, right) : r; 291 } 292 }; 293 getTypeDescription(Field optionsField)294 private static String getTypeDescription(Field optionsField) { 295 return OptionsData.findConverter(optionsField).getTypeDescription(); 296 } 297 getFlagName(Field field)298 static String getFlagName(Field field) { 299 String name = field.getAnnotation(Option.class).name(); 300 return OptionsData.isBooleanField(field) ? "[no]" + name : name; 301 } 302 303 } 304