aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/common/options/OptionsUsage.java
blob: e79a062ce45997e8bd9e3c21617b84728cdc3f4b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// Copyright 2014 The Bazel Authors. 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<? extends OptionsBase> optionsClass, StringBuilder usage) {
    List<Field> 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.,
   * <code>label</code>, <code>float</ode>, <code>path</code>...). Example outputs of this
   * function are for, respectively, a tristate flag <code>tristate_flag</code>, a enum
   * flag <code>enum_flag</code> which can take <code>value1</code>, <code>value2</code> and
   * <code>value3</code>, a path fragment flag <code>path_flag</code>, a string flag
   * <code>string_flag</code> and a void flag <code>void_flag</code>:
   * <pre>
   *   --tristate_flag={auto,yes,no}
   *   --notristate_flag
   *   --enum_flag={value1,value2,value3}
   *   --path_flag=path
   *   --string_flag=
   *   --void_flag
   * </pre>
   *
   * @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<Field> BY_NAME = new Comparator<Field>() {
    @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<Field> BY_CATEGORY = new Comparator<Field>() {
    @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;
  }

}