• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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("=&lt;").append(escaper.escape(typeDescription)).append("&gt");
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("&nbsp;&nbsp;<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