blob: f3ee4d3536b096b98ef7c71ecf8ad05292a864e6 [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
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.
14package com.google.devtools.common.options;
15
Damien Martin-Guillerez29728d42015-04-09 20:48:04 +000016import com.google.common.base.Joiner;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010017import com.google.common.base.Splitter;
18import com.google.common.base.Strings;
19import com.google.common.collect.Lists;
Ulf Adams352211d2016-06-22 09:24:28 +000020import com.google.common.escape.Escaper;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010021import java.lang.reflect.Field;
22import java.text.BreakIterator;
23import java.util.Collections;
24import java.util.Comparator;
25import java.util.List;
26
27/**
28 * A renderer for usage messages. For now this is very simple.
29 */
30class OptionsUsage {
31
32 private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n');
Damien Martin-Guillerez29728d42015-04-09 20:48:04 +000033 private static final Joiner COMMA_JOINER = Joiner.on(",");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010034
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-Guillerez29728d42015-04-09 20:48:04 +0000127 /**
Ulf Adams352211d2016-06-22 09:24:28 +0000128 * Append the usage message for a single option-field message to 'usage'.
129 */
130 static void getUsageHtml(Field optionField, StringBuilder usage, Escaper escaper) {
Ulf Adamsdba62232016-06-22 15:34:25 +0000131 String plainFlagName = optionField.getAnnotation(Option.class).name();
Ulf Adams352211d2016-06-22 09:24:28 +0000132 String flagName = getFlagName(optionField);
Ulf Adams6f096662016-06-27 15:51:23 +0000133 String valueDescription = optionField.getAnnotation(Option.class).valueHelp();
Ulf Adams352211d2016-06-22 09:24:28 +0000134 String typeDescription = getTypeDescription(optionField);
135 Option annotation = optionField.getAnnotation(Option.class);
Ulf Adamse7598d12016-06-23 11:01:20 +0000136 usage.append("<dt><code><a name=\"flag--").append(plainFlagName).append("\"></a>--");
Ulf Adams6f096662016-06-27 15:51:23 +0000137 usage.append(flagName);
Jon Brandvein097e64c2017-03-17 19:58:04 +0000138 if (OptionsData.isBooleanField(optionField) || OptionsData.isVoidField(optionField)) {
Ulf Adams6f096662016-06-27 15:51:23 +0000139 // 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("=&lt;").append(escaper.escape(typeDescription)).append("&gt");
145 }
146 usage.append("</code>");
Ulf Adams352211d2016-06-22 09:24:28 +0000147 if (annotation.abbrev() != '\0') {
148 usage.append(" [<code>-").append(annotation.abbrev()).append("</code>]");
149 }
Ulf Adams6f096662016-06-27 15:51:23 +0000150 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 Brandvein097e64c2017-03-17 19:58:04 +0000156 if (OptionsData.isVoidField(optionField)) {
Ulf Adams6f096662016-06-27 15:51:23 +0000157 // Void options don't have a default.
158 } else if (OptionsParserImpl.isSpecialNullDefault(defaultValueString, optionField)) {
159 usage.append(" default: see description");
Ulf Adams352211d2016-06-22 09:24:28 +0000160 } else {
Ulf Adams6f096662016-06-27 15:51:23 +0000161 usage.append(" default: \"").append(escaper.escape(defaultValueString)).append("\"");
Ulf Adams352211d2016-06-22 09:24:28 +0000162 }
Ulf Adams352211d2016-06-22 09:24:28 +0000163 }
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 Adams65c3e362016-06-23 11:42:18 +0000172 StringBuilder expandsMsg = new StringBuilder("Expands to:<br/>\n");
Ulf Adams352211d2016-06-22 09:24:28 +0000173 for (String exp : annotation.expansion()) {
Ulf Adamsdba62232016-06-22 15:34:25 +0000174 // TODO(ulfjack): Can we link to the expanded flags here?
Ulf Adams65c3e362016-06-23 11:42:18 +0000175 expandsMsg
176 .append("&nbsp;&nbsp;<code>")
177 .append(escaper.escape(exp))
178 .append("</code><br/>\n");
Ulf Adams352211d2016-06-22 09:24:28 +0000179 }
Ulf Adams65c3e362016-06-23 11:42:18 +0000180 usage.append(expandsMsg.toString()); // (indent, width)
Ulf Adams352211d2016-06-22 09:24:28 +0000181 usage.append('\n');
182 }
183 usage.append("</dd>\n");
184 }
185
186 /**
Damien Martin-Guillerez29728d42015-04-09 20:48:04 +0000187 * 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 Nienhuysd08b27f2015-02-25 16:45:20 +0100237 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 Brandvein097e64c2017-03-17 19:58:04 +0000258 return OptionsData.findConverter(optionsField).getTypeDescription();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100259 }
260
261 static String getFlagName(Field field) {
262 String name = field.getAnnotation(Option.class).name();
Jon Brandvein097e64c2017-03-17 19:58:04 +0000263 return OptionsData.isBooleanField(field) ? "[no]" + name : name;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100264 }
265
266}