Damien Martin-Guillerez | f88f4d8 | 2015-09-25 13:56:55 +0000 | [diff] [blame] | 1 | // Copyright 2014 The Bazel Authors. All rights reserved. |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 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 | |
Damien Martin-Guillerez | 29728d4 | 2015-04-09 20:48:04 +0000 | [diff] [blame] | 16 | import com.google.common.base.Joiner; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 17 | import com.google.common.base.Splitter; |
| 18 | import com.google.common.base.Strings; |
| 19 | import com.google.common.collect.Lists; |
Ulf Adams | 352211d | 2016-06-22 09:24:28 +0000 | [diff] [blame] | 20 | import com.google.common.escape.Escaper; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 21 | import java.lang.reflect.Field; |
| 22 | import java.text.BreakIterator; |
| 23 | import java.util.Collections; |
| 24 | import java.util.Comparator; |
| 25 | import java.util.List; |
| 26 | |
| 27 | /** |
| 28 | * A renderer for usage messages. For now this is very simple. |
| 29 | */ |
| 30 | class OptionsUsage { |
| 31 | |
| 32 | private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n'); |
Damien Martin-Guillerez | 29728d4 | 2015-04-09 20:48:04 +0000 | [diff] [blame] | 33 | private static final Joiner COMMA_JOINER = Joiner.on(","); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 34 | |
| 35 | /** |
| 36 | * Given an options class, render the usage string into the usage, |
| 37 | * which is passed in as an argument. |
| 38 | */ |
| 39 | static void getUsage(Class<? extends OptionsBase> optionsClass, StringBuilder usage) { |
| 40 | List<Field> optionFields = |
| 41 | Lists.newArrayList(OptionsParser.getAllAnnotatedFields(optionsClass)); |
| 42 | Collections.sort(optionFields, BY_NAME); |
| 43 | for (Field optionField : optionFields) { |
| 44 | getUsage(optionField, usage, OptionsParser.HelpVerbosity.LONG); |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | /** |
| 49 | * Paragraph-fill the specified input text, indenting lines to 'indent' and |
| 50 | * wrapping lines at 'width'. Returns the formatted result. |
| 51 | */ |
| 52 | static String paragraphFill(String in, int indent, int width) { |
| 53 | String indentString = Strings.repeat(" ", indent); |
| 54 | StringBuilder out = new StringBuilder(); |
| 55 | String sep = ""; |
| 56 | for (String paragraph : NEWLINE_SPLITTER.split(in)) { |
| 57 | BreakIterator boundary = BreakIterator.getLineInstance(); // (factory) |
| 58 | boundary.setText(paragraph); |
| 59 | out.append(sep).append(indentString); |
| 60 | int cursor = indent; |
| 61 | for (int start = boundary.first(), end = boundary.next(); |
| 62 | end != BreakIterator.DONE; |
| 63 | start = end, end = boundary.next()) { |
| 64 | String word = |
| 65 | paragraph.substring(start, end); // (may include trailing space) |
| 66 | if (word.length() + cursor > width) { |
| 67 | out.append('\n').append(indentString); |
| 68 | cursor = indent; |
| 69 | } |
| 70 | out.append(word); |
| 71 | cursor += word.length(); |
| 72 | } |
| 73 | sep = "\n"; |
| 74 | } |
| 75 | return out.toString(); |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Append the usage message for a single option-field message to 'usage'. |
| 80 | */ |
| 81 | static void getUsage(Field optionField, StringBuilder usage, |
| 82 | OptionsParser.HelpVerbosity helpVerbosity) { |
| 83 | String flagName = getFlagName(optionField); |
| 84 | String typeDescription = getTypeDescription(optionField); |
| 85 | Option annotation = optionField.getAnnotation(Option.class); |
| 86 | usage.append(" --" + flagName); |
| 87 | if (helpVerbosity == OptionsParser.HelpVerbosity.SHORT) { // just the name |
| 88 | usage.append('\n'); |
| 89 | return; |
| 90 | } |
| 91 | if (annotation.abbrev() != '\0') { |
| 92 | usage.append(" [-").append(annotation.abbrev()).append(']'); |
| 93 | } |
| 94 | if (!typeDescription.equals("")) { |
| 95 | usage.append(" (" + typeDescription + "; "); |
| 96 | if (annotation.allowMultiple()) { |
| 97 | usage.append("may be used multiple times"); |
| 98 | } else { |
| 99 | // Don't call the annotation directly (we must allow overrides to certain defaults) |
| 100 | String defaultValueString = OptionsParserImpl.getDefaultOptionString(optionField); |
| 101 | if (OptionsParserImpl.isSpecialNullDefault(defaultValueString, optionField)) { |
| 102 | usage.append("default: see description"); |
| 103 | } else { |
| 104 | usage.append("default: \"" + defaultValueString + "\""); |
| 105 | } |
| 106 | } |
| 107 | usage.append(")"); |
| 108 | } |
| 109 | usage.append("\n"); |
| 110 | if (helpVerbosity == OptionsParser.HelpVerbosity.MEDIUM) { // just the name and type. |
| 111 | return; |
| 112 | } |
| 113 | if (!annotation.help().equals("")) { |
| 114 | usage.append(paragraphFill(annotation.help(), 4, 80)); // (indent, width) |
| 115 | usage.append('\n'); |
| 116 | } |
| 117 | if (annotation.expansion().length > 0) { |
| 118 | StringBuilder expandsMsg = new StringBuilder("Expands to: "); |
| 119 | for (String exp : annotation.expansion()) { |
| 120 | expandsMsg.append(exp).append(" "); |
| 121 | } |
| 122 | usage.append(paragraphFill(expandsMsg.toString(), 4, 80)); // (indent, width) |
| 123 | usage.append('\n'); |
| 124 | } |
| 125 | } |
| 126 | |
Damien Martin-Guillerez | 29728d4 | 2015-04-09 20:48:04 +0000 | [diff] [blame] | 127 | /** |
Ulf Adams | 352211d | 2016-06-22 09:24:28 +0000 | [diff] [blame] | 128 | * Append the usage message for a single option-field message to 'usage'. |
| 129 | */ |
| 130 | static void getUsageHtml(Field optionField, StringBuilder usage, Escaper escaper) { |
Ulf Adams | dba6223 | 2016-06-22 15:34:25 +0000 | [diff] [blame] | 131 | String plainFlagName = optionField.getAnnotation(Option.class).name(); |
Ulf Adams | 352211d | 2016-06-22 09:24:28 +0000 | [diff] [blame] | 132 | String flagName = getFlagName(optionField); |
Ulf Adams | 6f09666 | 2016-06-27 15:51:23 +0000 | [diff] [blame] | 133 | String valueDescription = optionField.getAnnotation(Option.class).valueHelp(); |
Ulf Adams | 352211d | 2016-06-22 09:24:28 +0000 | [diff] [blame] | 134 | String typeDescription = getTypeDescription(optionField); |
| 135 | Option annotation = optionField.getAnnotation(Option.class); |
Ulf Adams | e7598d1 | 2016-06-23 11:01:20 +0000 | [diff] [blame] | 136 | usage.append("<dt><code><a name=\"flag--").append(plainFlagName).append("\"></a>--"); |
Ulf Adams | 6f09666 | 2016-06-27 15:51:23 +0000 | [diff] [blame] | 137 | usage.append(flagName); |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 138 | if (OptionsData.isBooleanField(optionField) || OptionsData.isVoidField(optionField)) { |
Ulf Adams | 6f09666 | 2016-06-27 15:51:23 +0000 | [diff] [blame] | 139 | // Nothing for boolean, tristate, boolean_or_enum, or void options. |
| 140 | } else if (!valueDescription.isEmpty()) { |
| 141 | usage.append("=").append(escaper.escape(valueDescription)); |
| 142 | } else if (!typeDescription.isEmpty()) { |
| 143 | // Generic fallback, which isn't very good. |
| 144 | usage.append("=<").append(escaper.escape(typeDescription)).append(">"); |
| 145 | } |
| 146 | usage.append("</code>"); |
Ulf Adams | 352211d | 2016-06-22 09:24:28 +0000 | [diff] [blame] | 147 | if (annotation.abbrev() != '\0') { |
| 148 | usage.append(" [<code>-").append(annotation.abbrev()).append("</code>]"); |
| 149 | } |
Ulf Adams | 6f09666 | 2016-06-27 15:51:23 +0000 | [diff] [blame] | 150 | if (annotation.allowMultiple()) { |
| 151 | // Allow-multiple options can't have a default value. |
| 152 | usage.append(" multiple uses are accumulated"); |
| 153 | } else { |
| 154 | // Don't call the annotation directly (we must allow overrides to certain defaults). |
| 155 | String defaultValueString = OptionsParserImpl.getDefaultOptionString(optionField); |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 156 | if (OptionsData.isVoidField(optionField)) { |
Ulf Adams | 6f09666 | 2016-06-27 15:51:23 +0000 | [diff] [blame] | 157 | // Void options don't have a default. |
| 158 | } else if (OptionsParserImpl.isSpecialNullDefault(defaultValueString, optionField)) { |
| 159 | usage.append(" default: see description"); |
Ulf Adams | 352211d | 2016-06-22 09:24:28 +0000 | [diff] [blame] | 160 | } else { |
Ulf Adams | 6f09666 | 2016-06-27 15:51:23 +0000 | [diff] [blame] | 161 | usage.append(" default: \"").append(escaper.escape(defaultValueString)).append("\""); |
Ulf Adams | 352211d | 2016-06-22 09:24:28 +0000 | [diff] [blame] | 162 | } |
Ulf Adams | 352211d | 2016-06-22 09:24:28 +0000 | [diff] [blame] | 163 | } |
| 164 | usage.append("</dt>\n"); |
| 165 | usage.append("<dd>\n"); |
| 166 | if (!annotation.help().isEmpty()) { |
| 167 | usage.append(paragraphFill(escaper.escape(annotation.help()), 0, 80)); // (indent, width) |
| 168 | usage.append('\n'); |
| 169 | } |
| 170 | if (annotation.expansion().length > 0) { |
| 171 | usage.append("<br/>\n"); |
Ulf Adams | 65c3e36 | 2016-06-23 11:42:18 +0000 | [diff] [blame] | 172 | StringBuilder expandsMsg = new StringBuilder("Expands to:<br/>\n"); |
Ulf Adams | 352211d | 2016-06-22 09:24:28 +0000 | [diff] [blame] | 173 | for (String exp : annotation.expansion()) { |
Ulf Adams | dba6223 | 2016-06-22 15:34:25 +0000 | [diff] [blame] | 174 | // TODO(ulfjack): Can we link to the expanded flags here? |
Ulf Adams | 65c3e36 | 2016-06-23 11:42:18 +0000 | [diff] [blame] | 175 | expandsMsg |
| 176 | .append(" <code>") |
| 177 | .append(escaper.escape(exp)) |
| 178 | .append("</code><br/>\n"); |
Ulf Adams | 352211d | 2016-06-22 09:24:28 +0000 | [diff] [blame] | 179 | } |
Ulf Adams | 65c3e36 | 2016-06-23 11:42:18 +0000 | [diff] [blame] | 180 | usage.append(expandsMsg.toString()); // (indent, width) |
Ulf Adams | 352211d | 2016-06-22 09:24:28 +0000 | [diff] [blame] | 181 | usage.append('\n'); |
| 182 | } |
| 183 | usage.append("</dd>\n"); |
| 184 | } |
| 185 | |
| 186 | /** |
Damien Martin-Guillerez | 29728d4 | 2015-04-09 20:48:04 +0000 | [diff] [blame] | 187 | * Returns the available completion for the given option field. The completions are the exact |
| 188 | * command line option (with the prepending '--') that one should pass. It is suitable for |
| 189 | * completion script to use. If the option expect an argument, the kind of argument is given |
| 190 | * after the equals. If the kind is a enum, the various enum values are given inside an accolade |
| 191 | * in a comma separated list. For other special kind, the type is given as a name (e.g., |
| 192 | * <code>label</code>, <code>float</ode>, <code>path</code>...). Example outputs of this |
| 193 | * function are for, respectively, a tristate flag <code>tristate_flag</code>, a enum |
| 194 | * flag <code>enum_flag</code> which can take <code>value1</code>, <code>value2</code> and |
| 195 | * <code>value3</code>, a path fragment flag <code>path_flag</code>, a string flag |
| 196 | * <code>string_flag</code> and a void flag <code>void_flag</code>: |
| 197 | * <pre> |
| 198 | * --tristate_flag={auto,yes,no} |
| 199 | * --notristate_flag |
| 200 | * --enum_flag={value1,value2,value3} |
| 201 | * --path_flag=path |
| 202 | * --string_flag= |
| 203 | * --void_flag |
| 204 | * </pre> |
| 205 | * |
| 206 | * @param field The field to return completion for |
| 207 | * @param builder the string builder to store the completion values |
| 208 | */ |
| 209 | static void getCompletion(Field field, StringBuilder builder) { |
| 210 | // Return the list of possible completions for this option |
| 211 | String flagName = field.getAnnotation(Option.class).name(); |
| 212 | Class<?> fieldType = field.getType(); |
| 213 | builder.append("--").append(flagName); |
| 214 | if (fieldType.equals(boolean.class)) { |
| 215 | builder.append("\n"); |
| 216 | builder.append("--no").append(flagName).append("\n"); |
| 217 | } else if (fieldType.equals(TriState.class)) { |
| 218 | builder.append("={auto,yes,no}\n"); |
| 219 | builder.append("--no").append(flagName).append("\n"); |
| 220 | } else if (fieldType.isEnum()) { |
| 221 | builder.append("={") |
| 222 | .append(COMMA_JOINER.join(fieldType.getEnumConstants()).toLowerCase()).append("}\n"); |
| 223 | } else if (fieldType.getSimpleName().equals("Label")) { |
| 224 | // String comparison so we don't introduce a dependency to com.google.devtools.build.lib. |
| 225 | builder.append("=label\n"); |
| 226 | } else if (fieldType.getSimpleName().equals("PathFragment")) { |
| 227 | builder.append("=path\n"); |
| 228 | } else if (Void.class.isAssignableFrom(fieldType)) { |
| 229 | builder.append("\n"); |
| 230 | } else { |
| 231 | // TODO(bazel-team): add more types. Maybe even move the completion type |
| 232 | // to the @Option annotation? |
| 233 | builder.append("=\n"); |
| 234 | } |
| 235 | } |
| 236 | |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 237 | private static final Comparator<Field> BY_NAME = new Comparator<Field>() { |
| 238 | @Override |
| 239 | public int compare(Field left, Field right) { |
| 240 | return left.getName().compareTo(right.getName()); |
| 241 | } |
| 242 | }; |
| 243 | |
| 244 | /** |
| 245 | * An ordering relation for option-field fields that first groups together |
| 246 | * options of the same category, then sorts by name within the category. |
| 247 | */ |
| 248 | static final Comparator<Field> BY_CATEGORY = new Comparator<Field>() { |
| 249 | @Override |
| 250 | public int compare(Field left, Field right) { |
| 251 | int r = left.getAnnotation(Option.class).category().compareTo( |
| 252 | right.getAnnotation(Option.class).category()); |
| 253 | return r == 0 ? BY_NAME.compare(left, right) : r; |
| 254 | } |
| 255 | }; |
| 256 | |
| 257 | private static String getTypeDescription(Field optionsField) { |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 258 | return OptionsData.findConverter(optionsField).getTypeDescription(); |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 259 | } |
| 260 | |
| 261 | static String getFlagName(Field field) { |
| 262 | String name = field.getAnnotation(Option.class).name(); |
Jon Brandvein | 097e64c | 2017-03-17 19:58:04 +0000 | [diff] [blame] | 263 | return OptionsData.isBooleanField(field) ? "[no]" + name : name; |
Han-Wen Nienhuys | d08b27f | 2015-02-25 16:45:20 +0100 | [diff] [blame] | 264 | } |
| 265 | |
| 266 | } |