blob: 15e8b479db0b29b6b4d00d9fb8d60ff25755e618 [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
warkahscottde771482020-10-08 08:25:21 -070016import static com.google.devtools.common.options.OptionsParser.STARLARK_SKIPPED_PREFIXES;
17
seancurranb158a0f2021-06-21 13:57:22 -070018import com.github.benmanes.caffeine.cache.CaffeineSpec;
dlr577d9072020-06-26 10:36:04 -070019import com.google.common.base.Ascii;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010020import com.google.common.base.Splitter;
21import com.google.common.collect.ImmutableList;
ccalvarin3e44d5b2017-08-31 06:32:03 +020022import com.google.common.collect.ImmutableMap;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010023import com.google.common.collect.Maps;
ulfjackf9625f02017-07-24 13:01:29 +020024import java.time.Duration;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010025import java.util.Iterator;
26import java.util.List;
27import java.util.Map;
28import java.util.logging.Level;
ulfjackf9625f02017-07-24 13:01:29 +020029import java.util.regex.Matcher;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010030import java.util.regex.Pattern;
31import java.util.regex.PatternSyntaxException;
32
Googler0c126032018-05-03 06:43:51 -070033/** Some convenient converters used by blaze. Note: These are specific to blaze. */
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010034public final class Converters {
warkahscottde771482020-10-08 08:25:21 -070035 /**
36 * The name of the flag used for shorthand aliasing in blaze. {@see
37 * com.google.devtools.build.lib.analysis.config.CoreOptions#commandLineFlagAliases} for the
38 * option definition.
39 */
40 public static final String BLAZE_ALIASING_FLAG = "flag_alias";
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010041
dlr577d9072020-06-26 10:36:04 -070042 private static final ImmutableList<String> ENABLED_REPS =
43 ImmutableList.of("true", "1", "yes", "t", "y");
44
45 private static final ImmutableList<String> DISABLED_REPS =
46 ImmutableList.of("false", "0", "no", "f", "n");
47
Jon Brandvein097e64c2017-03-17 19:58:04 +000048 /** Standard converter for booleans. Accepts common shorthands/synonyms. */
49 public static class BooleanConverter implements Converter<Boolean> {
50 @Override
51 public Boolean convert(String input) throws OptionsParsingException {
52 if (input == null) {
53 return false;
54 }
dlr577d9072020-06-26 10:36:04 -070055 input = Ascii.toLowerCase(input);
56 if (ENABLED_REPS.contains(input)) {
Jon Brandvein097e64c2017-03-17 19:58:04 +000057 return true;
58 }
dlr577d9072020-06-26 10:36:04 -070059 if (DISABLED_REPS.contains(input)) {
Jon Brandvein097e64c2017-03-17 19:58:04 +000060 return false;
61 }
62 throw new OptionsParsingException("'" + input + "' is not a boolean");
63 }
64
65 @Override
66 public String getTypeDescription() {
67 return "a boolean";
68 }
69 }
70
71 /** Standard converter for Strings. */
72 public static class StringConverter implements Converter<String> {
73 @Override
74 public String convert(String input) {
75 return input;
76 }
77
78 @Override
79 public String getTypeDescription() {
80 return "a string";
81 }
82 }
83
84 /** Standard converter for integers. */
85 public static class IntegerConverter implements Converter<Integer> {
86 @Override
87 public Integer convert(String input) throws OptionsParsingException {
88 try {
89 return Integer.decode(input);
90 } catch (NumberFormatException e) {
Googlercaa04512022-03-07 15:55:06 -080091 throw new OptionsParsingException("'" + input + "' is not an int", e);
Jon Brandvein097e64c2017-03-17 19:58:04 +000092 }
93 }
94
95 @Override
96 public String getTypeDescription() {
97 return "an integer";
98 }
99 }
100
101 /** Standard converter for longs. */
102 public static class LongConverter implements Converter<Long> {
103 @Override
104 public Long convert(String input) throws OptionsParsingException {
105 try {
106 return Long.decode(input);
107 } catch (NumberFormatException e) {
Googlercaa04512022-03-07 15:55:06 -0800108 throw new OptionsParsingException("'" + input + "' is not a long", e);
Jon Brandvein097e64c2017-03-17 19:58:04 +0000109 }
110 }
111
112 @Override
113 public String getTypeDescription() {
114 return "a long integer";
115 }
116 }
117
118 /** Standard converter for doubles. */
119 public static class DoubleConverter implements Converter<Double> {
120 @Override
121 public Double convert(String input) throws OptionsParsingException {
122 try {
123 return Double.parseDouble(input);
124 } catch (NumberFormatException e) {
Googlercaa04512022-03-07 15:55:06 -0800125 throw new OptionsParsingException("'" + input + "' is not a double", e);
Jon Brandvein097e64c2017-03-17 19:58:04 +0000126 }
127 }
128
129 @Override
130 public String getTypeDescription() {
131 return "a double";
132 }
133 }
134
135 /** Standard converter for TriState values. */
136 public static class TriStateConverter implements Converter<TriState> {
137 @Override
138 public TriState convert(String input) throws OptionsParsingException {
139 if (input == null) {
140 return TriState.AUTO;
141 }
dlr577d9072020-06-26 10:36:04 -0700142 input = Ascii.toLowerCase(input);
Jon Brandvein097e64c2017-03-17 19:58:04 +0000143 if (input.equals("auto")) {
144 return TriState.AUTO;
145 }
dlr577d9072020-06-26 10:36:04 -0700146 if (ENABLED_REPS.contains(input)) {
Jon Brandvein097e64c2017-03-17 19:58:04 +0000147 return TriState.YES;
148 }
dlr577d9072020-06-26 10:36:04 -0700149 if (DISABLED_REPS.contains(input)) {
Jon Brandvein097e64c2017-03-17 19:58:04 +0000150 return TriState.NO;
151 }
152 throw new OptionsParsingException("'" + input + "' is not a boolean");
153 }
154
155 @Override
156 public String getTypeDescription() {
157 return "a tri-state (auto, yes, no)";
158 }
159 }
160
161 /**
162 * Standard "converter" for Void. Should not actually be invoked. For instance, expansion flags
163 * are usually Void-typed and do not invoke the converter.
164 */
165 public static class VoidConverter implements Converter<Void> {
166 @Override
167 public Void convert(String input) throws OptionsParsingException {
ccalvarin3e44d5b2017-08-31 06:32:03 +0200168 if (input == null || input.equals("null")) {
Jon Brandvein097e64c2017-03-17 19:58:04 +0000169 return null; // expected input, return is unused so null is fine.
170 }
171 throw new OptionsParsingException("'" + input + "' unexpected");
172 }
173
174 @Override
175 public String getTypeDescription() {
176 return "";
177 }
178 }
179
Googler0c126032018-05-03 06:43:51 -0700180 /** Standard converter for the {@link java.time.Duration} type. */
ulfjackf9625f02017-07-24 13:01:29 +0200181 public static class DurationConverter implements Converter<Duration> {
182 private final Pattern durationRegex = Pattern.compile("^([0-9]+)(d|h|m|s|ms)$");
183
184 @Override
185 public Duration convert(String input) throws OptionsParsingException {
186 // To be compatible with the previous parser, '0' doesn't need a unit.
187 if ("0".equals(input)) {
188 return Duration.ZERO;
189 }
190 Matcher m = durationRegex.matcher(input);
191 if (!m.matches()) {
192 throw new OptionsParsingException("Illegal duration '" + input + "'.");
193 }
194 long duration = Long.parseLong(m.group(1));
195 String unit = m.group(2);
Googler0c126032018-05-03 06:43:51 -0700196 switch (unit) {
ulfjackf9625f02017-07-24 13:01:29 +0200197 case "d":
198 return Duration.ofDays(duration);
199 case "h":
200 return Duration.ofHours(duration);
201 case "m":
202 return Duration.ofMinutes(duration);
203 case "s":
204 return Duration.ofSeconds(duration);
205 case "ms":
206 return Duration.ofMillis(duration);
207 default:
Googler0c126032018-05-03 06:43:51 -0700208 throw new IllegalStateException(
209 "This must not happen. Did you update the regex without the switch case?");
ulfjackf9625f02017-07-24 13:01:29 +0200210 }
211 }
212
213 @Override
214 public String getTypeDescription() {
215 return "An immutable length of time.";
216 }
217 }
218
ccalvarin3e44d5b2017-08-31 06:32:03 +0200219 // 1:1 correspondence with UsesOnlyCoreTypes.CORE_TYPES.
ulfjackf9625f02017-07-24 13:01:29 +0200220 /**
Jon Brandvein097e64c2017-03-17 19:58:04 +0000221 * The converters that are available to the options parser by default. These are used if the
222 * {@code @Option} annotation does not specify its own {@code converter}, and its type is one of
223 * the following.
224 */
ccalvarin3e44d5b2017-08-31 06:32:03 +0200225 public static final ImmutableMap<Class<?>, Converter<?>> DEFAULT_CONVERTERS =
226 new ImmutableMap.Builder<Class<?>, Converter<?>>()
227 .put(String.class, new Converters.StringConverter())
228 .put(int.class, new Converters.IntegerConverter())
229 .put(long.class, new Converters.LongConverter())
230 .put(double.class, new Converters.DoubleConverter())
231 .put(boolean.class, new Converters.BooleanConverter())
232 .put(TriState.class, new Converters.TriStateConverter())
233 .put(Duration.class, new Converters.DurationConverter())
234 .put(Void.class, new Converters.VoidConverter())
235 .build();
Jon Brandvein097e64c2017-03-17 19:58:04 +0000236
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100237 /**
Googler0c126032018-05-03 06:43:51 -0700238 * Join a list of words as in English. Examples: "nothing" "one" "one or two" "one and two" "one,
239 * two or three". "one, two and three". The toString method of each element is used.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100240 */
241 static String joinEnglishList(Iterable<?> choices) {
242 StringBuilder buf = new StringBuilder();
243 for (Iterator<?> ii = choices.iterator(); ii.hasNext(); ) {
244 Object choice = ii.next();
245 if (buf.length() > 0) {
246 buf.append(ii.hasNext() ? ", " : " or ");
247 }
248 buf.append(choice);
249 }
250 return buf.length() == 0 ? "nothing" : buf.toString();
251 }
252
janakr49625ea2022-03-18 19:10:01 -0700253 /** Converter for a list of options, separated by some separator character. */
254 public static class SeparatedOptionListConverter implements Converter<ImmutableList<String>> {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100255 private final String separatorDescription;
256 private final Splitter splitter;
philwo207ac6e2019-02-20 12:44:06 -0800257 private final boolean allowEmptyValues;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100258
philwo207ac6e2019-02-20 12:44:06 -0800259 protected SeparatedOptionListConverter(
260 char separator, String separatorDescription, boolean allowEmptyValues) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100261 this.separatorDescription = separatorDescription;
262 this.splitter = Splitter.on(separator);
philwo207ac6e2019-02-20 12:44:06 -0800263 this.allowEmptyValues = allowEmptyValues;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100264 }
265
266 @Override
janakr49625ea2022-03-18 19:10:01 -0700267 public ImmutableList<String> convert(String input) throws OptionsParsingException {
268 ImmutableList<String> result =
philwo207ac6e2019-02-20 12:44:06 -0800269 input.isEmpty() ? ImmutableList.of() : ImmutableList.copyOf(splitter.split(input));
270 if (!allowEmptyValues && result.contains("")) {
philwo849113c2019-02-20 15:09:30 -0800271 // If the list contains exactly the empty string, it means an empty value was passed and we
272 // should instead return an empty list.
273 if (result.size() == 1) {
274 return ImmutableList.of();
275 }
276
philwo207ac6e2019-02-20 12:44:06 -0800277 throw new OptionsParsingException(
278 "Empty values are not allowed as part of this " + getTypeDescription());
279 }
280 return result;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100281 }
282
283 @Override
284 public String getTypeDescription() {
285 return separatorDescription + "-separated list of options";
286 }
287 }
288
Googler0c126032018-05-03 06:43:51 -0700289 public static class CommaSeparatedOptionListConverter extends SeparatedOptionListConverter {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100290 public CommaSeparatedOptionListConverter() {
philwo207ac6e2019-02-20 12:44:06 -0800291 super(',', "comma", true);
292 }
293 }
294
295 public static class CommaSeparatedNonEmptyOptionListConverter
296 extends SeparatedOptionListConverter {
297 public CommaSeparatedNonEmptyOptionListConverter() {
298 super(',', "comma", false);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100299 }
300 }
301
302 public static class ColonSeparatedOptionListConverter extends SeparatedOptionListConverter {
303 public ColonSeparatedOptionListConverter() {
philwo207ac6e2019-02-20 12:44:06 -0800304 super(':', "colon", true);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100305 }
306 }
307
308 public static class LogLevelConverter implements Converter<Level> {
309
seancurranb158a0f2021-06-21 13:57:22 -0700310 static final ImmutableList<Level> LEVELS =
311 ImmutableList.of(
312 Level.OFF,
313 Level.SEVERE,
314 Level.WARNING,
315 Level.INFO,
316 Level.FINE,
317 Level.FINER,
318 Level.FINEST);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100319
320 @Override
321 public Level convert(String input) throws OptionsParsingException {
322 try {
323 int level = Integer.parseInt(input);
seancurranb158a0f2021-06-21 13:57:22 -0700324 return LEVELS.get(level);
Jonathan Bluett-Duncan0df3ddbd2017-08-09 11:13:54 +0200325 } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
Googlercaa04512022-03-07 15:55:06 -0800326 throw new OptionsParsingException("Not a log level: " + input, e);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100327 }
328 }
329
330 @Override
331 public String getTypeDescription() {
seancurranb158a0f2021-06-21 13:57:22 -0700332 return "0 <= an integer <= " + (LEVELS.size() - 1);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100333 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100334 }
335
Googler0c126032018-05-03 06:43:51 -0700336 /** Checks whether a string is part of a set of strings. */
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100337 public static class StringSetConverter implements Converter<String> {
338
339 // TODO(bazel-team): if this class never actually contains duplicates, we could s/List/Set/
340 // here.
341 private final List<String> values;
342
343 public StringSetConverter(String... values) {
344 this.values = ImmutableList.copyOf(values);
345 }
346
347 @Override
348 public String convert(String input) throws OptionsParsingException {
349 if (values.contains(input)) {
350 return input;
351 }
352
353 throw new OptionsParsingException("Not one of " + values);
354 }
355
356 @Override
357 public String getTypeDescription() {
358 return joinEnglishList(values);
359 }
360 }
361
Googler0c126032018-05-03 06:43:51 -0700362 /** Checks whether a string is a valid regex pattern and compiles it. */
Googler21b759d2019-04-03 10:57:09 -0700363 public static class RegexPatternConverter implements Converter<RegexPatternOption> {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100364
365 @Override
Googler21b759d2019-04-03 10:57:09 -0700366 public RegexPatternOption convert(String input) throws OptionsParsingException {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100367 try {
Googler21b759d2019-04-03 10:57:09 -0700368 return RegexPatternOption.create(Pattern.compile(input));
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100369 } catch (PatternSyntaxException e) {
370 throw new OptionsParsingException("Not a valid regular expression: " + e.getMessage());
371 }
372 }
373
374 @Override
375 public String getTypeDescription() {
376 return "a valid Java regular expression";
377 }
378 }
379
Googler0c126032018-05-03 06:43:51 -0700380 /** Limits the length of a string argument. */
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100381 public static class LengthLimitingConverter implements Converter<String> {
382 private final int maxSize;
383
384 public LengthLimitingConverter(int maxSize) {
385 this.maxSize = maxSize;
386 }
387
388 @Override
389 public String convert(String input) throws OptionsParsingException {
390 if (input.length() > maxSize) {
391 throw new OptionsParsingException("Input must be " + getTypeDescription());
392 }
393 return input;
394 }
395
396 @Override
397 public String getTypeDescription() {
398 return "a string <= " + maxSize + " characters";
399 }
400 }
401
Googler0c126032018-05-03 06:43:51 -0700402 /** Checks whether an integer is in the given range. */
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100403 public static class RangeConverter implements Converter<Integer> {
404 final int minValue;
405 final int maxValue;
406
407 public RangeConverter(int minValue, int maxValue) {
408 this.minValue = minValue;
409 this.maxValue = maxValue;
410 }
411
412 @Override
413 public Integer convert(String input) throws OptionsParsingException {
414 try {
415 Integer value = Integer.parseInt(input);
416 if (value < minValue) {
417 throw new OptionsParsingException("'" + input + "' should be >= " + minValue);
418 } else if (value < minValue || value > maxValue) {
419 throw new OptionsParsingException("'" + input + "' should be <= " + maxValue);
420 }
421 return value;
422 } catch (NumberFormatException e) {
Googlercaa04512022-03-07 15:55:06 -0800423 throw new OptionsParsingException("'" + input + "' is not an int", e);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100424 }
425 }
426
427 @Override
428 public String getTypeDescription() {
429 if (minValue == Integer.MIN_VALUE) {
430 if (maxValue == Integer.MAX_VALUE) {
431 return "an integer";
432 } else {
433 return "an integer, <= " + maxValue;
434 }
435 } else if (maxValue == Integer.MAX_VALUE) {
436 return "an integer, >= " + minValue;
437 } else {
438 return "an integer in "
Googler0c126032018-05-03 06:43:51 -0700439 + (minValue < 0 ? "(" + minValue + ")" : minValue)
440 + "-"
441 + maxValue
442 + " range";
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100443 }
444 }
445 }
446
447 /**
Googler0c126032018-05-03 06:43:51 -0700448 * A converter for variable assignments from the parameter list of a blaze command invocation.
449 * Assignments are expected to have the form "name=value", where names and values are defined to
450 * be as permissive as possible.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100451 */
452 public static class AssignmentConverter implements Converter<Map.Entry<String, String>> {
453
454 @Override
Googler0c126032018-05-03 06:43:51 -0700455 public Map.Entry<String, String> convert(String input) throws OptionsParsingException {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100456 int pos = input.indexOf("=");
457 if (pos <= 0) {
Googler0c126032018-05-03 06:43:51 -0700458 throw new OptionsParsingException(
459 "Variable definitions must be in the form of a 'name=value' assignment");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100460 }
461 String name = input.substring(0, pos);
462 String value = input.substring(pos + 1);
463 return Maps.immutableEntry(name, value);
464 }
465
466 @Override
467 public String getTypeDescription() {
468 return "a 'name=value' assignment";
469 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100470 }
471
472 /**
warkahscottde771482020-10-08 08:25:21 -0700473 * A converter for command line flag aliases. It does additional validation on the name and value
474 * of the assignment to ensure they conform to the naming limitations.
475 */
476 public static class FlagAliasConverter extends AssignmentConverter {
477
478 @Override
479 public Map.Entry<String, String> convert(String input) throws OptionsParsingException {
480 Map.Entry<String, String> entry = super.convert(input);
481 String shortForm = entry.getKey();
482 String longForm = entry.getValue();
483
484 String cmdLineAlias = "--" + BLAZE_ALIASING_FLAG + "=" + input;
485
486 if (!Pattern.matches("([\\w])*", shortForm)) {
487 throw new OptionsParsingException(
488 shortForm + " should only consist of word characters to be a valid alias name.",
489 cmdLineAlias);
490 }
491 if (longForm.contains("=")) {
492 throw new OptionsParsingException(
493 "--" + BLAZE_ALIASING_FLAG + " does not support flag value assignment.", cmdLineAlias);
494 }
495
496 // Remove this check if native options are permitted to be aliased
497 longForm = "--" + longForm;
498 if (STARLARK_SKIPPED_PREFIXES.stream().noneMatch(longForm::startsWith)) {
499 throw new OptionsParsingException(
500 "--" + BLAZE_ALIASING_FLAG + " only supports Starlark build settings.", cmdLineAlias);
501 }
502
503 return entry;
504 }
505
506 @Override
507 public String getTypeDescription() {
508 return "a 'name=value' flag alias";
509 }
510 }
511
512 /**
schmitt61e80132019-11-01 11:38:55 -0700513 * Base converter for assignments from a value to a list of values. Both the key type as well as
514 * the type for all instances in the list of values are processed via passed converters.
philwo207ac6e2019-02-20 12:44:06 -0800515 */
schmitt61e80132019-11-01 11:38:55 -0700516 public abstract static class AssignmentToListOfValuesConverter<K, V>
517 implements Converter<Map.Entry<K, List<V>>> {
philwo207ac6e2019-02-20 12:44:06 -0800518
schmitt61e80132019-11-01 11:38:55 -0700519 /** Whether to allow keys in the assignment to be empty (i.e. just a list of values) */
520 public enum AllowEmptyKeys {
521 YES,
522 NO
523 }
524
525 private static final Splitter SPLITTER = Splitter.on(',');
526
527 private final Converter<K> keyConverter;
528 private final Converter<V> valueConverter;
529 private final AllowEmptyKeys allowEmptyKeys;
530
531 public AssignmentToListOfValuesConverter(
532 Converter<K> keyConverter, Converter<V> valueConverter, AllowEmptyKeys allowEmptyKeys) {
533 this.keyConverter = keyConverter;
534 this.valueConverter = valueConverter;
535 this.allowEmptyKeys = allowEmptyKeys;
536 }
philwo207ac6e2019-02-20 12:44:06 -0800537
538 @Override
schmitt61e80132019-11-01 11:38:55 -0700539 public Map.Entry<K, List<V>> convert(String input) throws OptionsParsingException {
philwo207ac6e2019-02-20 12:44:06 -0800540 int pos = input.indexOf("=");
schmitt61e80132019-11-01 11:38:55 -0700541 if (allowEmptyKeys == AllowEmptyKeys.NO && pos <= 0) {
542 throw new OptionsParsingException(
543 "Must be in the form of a 'key=value[,value]' assignment");
544 }
545
546 String key = pos <= 0 ? "" : input.substring(0, pos);
547 List<String> values = SPLITTER.splitToList(input.substring(pos + 1));
548 if (values.contains("")) {
philwo207ac6e2019-02-20 12:44:06 -0800549 // If the list contains exactly the empty string, it means an empty value was passed and we
550 // should instead return an empty list.
schmitt61e80132019-11-01 11:38:55 -0700551 if (values.size() == 1) {
552 values = ImmutableList.of();
philwo207ac6e2019-02-20 12:44:06 -0800553 } else {
554 throw new OptionsParsingException(
555 "Variable definitions must not contain empty strings or leading / trailing commas");
556 }
557 }
schmitt61e80132019-11-01 11:38:55 -0700558 ImmutableList.Builder<V> convertedValues = ImmutableList.builder();
559 for (String value : values) {
560 convertedValues.add(valueConverter.convert(value));
561 }
562 return Maps.immutableEntry(keyConverter.convert(key), convertedValues.build());
563 }
564 }
565
566 /**
567 * A converter for variable assignments from the parameter list of a blaze command invocation.
568 * Assignments are expected to have the form {@code [name=]value1[,..,valueN]}, where names and
569 * values are defined to be as permissive as possible. If no name is provided, "" is used.
570 */
571 public static class StringToStringListConverter
572 extends AssignmentToListOfValuesConverter<String, String> {
573
574 public StringToStringListConverter() {
575 super(new StringConverter(), new StringConverter(), AllowEmptyKeys.YES);
philwo207ac6e2019-02-20 12:44:06 -0800576 }
577
578 @Override
579 public String getTypeDescription() {
steinman0588d1f2019-07-26 14:20:22 -0700580 return "a '[name=]value1[,..,valueN]' assignment";
philwo207ac6e2019-02-20 12:44:06 -0800581 }
582 }
583
584 /**
585 * A converter for variable assignments from the parameter list of a blaze command invocation.
Googler0c126032018-05-03 06:43:51 -0700586 * Assignments are expected to have the form "name[=value]", where names and values are defined to
587 * be as permissive as possible and value part can be optional (in which case it is considered to
588 * be null).
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100589 */
590 public static class OptionalAssignmentConverter implements Converter<Map.Entry<String, String>> {
591
592 @Override
Googler0c126032018-05-03 06:43:51 -0700593 public Map.Entry<String, String> convert(String input) throws OptionsParsingException {
594 int pos = input.indexOf('=');
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100595 if (pos == 0 || input.length() == 0) {
Googler0c126032018-05-03 06:43:51 -0700596 throw new OptionsParsingException(
597 "Variable definitions must be in the form of a 'name=value' or 'name' assignment");
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100598 } else if (pos < 0) {
599 return Maps.immutableEntry(input, null);
600 }
601 String name = input.substring(0, pos);
602 String value = input.substring(pos + 1);
603 return Maps.immutableEntry(name, value);
604 }
605
606 @Override
607 public String getTypeDescription() {
608 return "a 'name=value' assignment with an optional value part";
609 }
Googler0c126032018-05-03 06:43:51 -0700610 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100611
Googler0c126032018-05-03 06:43:51 -0700612 /**
613 * A converter for named integers of the form "[name=]value". When no name is specified, an empty
614 * string is used for the key.
615 */
616 public static class NamedIntegersConverter implements Converter<Map.Entry<String, Integer>> {
617
618 @Override
619 public Map.Entry<String, Integer> convert(String input) throws OptionsParsingException {
620 int pos = input.indexOf('=');
621 if (pos == 0 || input.length() == 0) {
622 throw new OptionsParsingException(
623 "Specify either 'value' or 'name=value', where 'value' is an integer");
624 } else if (pos < 0) {
625 try {
626 return Maps.immutableEntry("", Integer.parseInt(input));
627 } catch (NumberFormatException e) {
Googlercaa04512022-03-07 15:55:06 -0800628 throw new OptionsParsingException("'" + input + "' is not an int", e);
Googler0c126032018-05-03 06:43:51 -0700629 }
630 }
631 String name = input.substring(0, pos);
632 String value = input.substring(pos + 1);
633 try {
634 return Maps.immutableEntry(name, Integer.parseInt(value));
635 } catch (NumberFormatException e) {
Googlercaa04512022-03-07 15:55:06 -0800636 throw new OptionsParsingException("'" + value + "' is not an int", e);
Googler0c126032018-05-03 06:43:51 -0700637 }
638 }
639
640 @Override
641 public String getTypeDescription() {
642 return "an integer or a named integer, 'name=value'";
643 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100644 }
645
646 public static class HelpVerbosityConverter extends EnumConverter<OptionsParser.HelpVerbosity> {
647 public HelpVerbosityConverter() {
648 super(OptionsParser.HelpVerbosity.class, "--help_verbosity setting");
649 }
650 }
651
Googler2db54112017-06-02 17:48:48 -0400652 /**
653 * A converter to check whether an integer denoting a percentage is in a valid range: [0, 100].
654 */
655 public static class PercentageConverter extends RangeConverter {
656 public PercentageConverter() {
657 super(0, 100);
658 }
659 }
fellyeb5f21d2019-12-05 14:20:23 -0800660
661 /**
seancurranb158a0f2021-06-21 13:57:22 -0700662 * A {@link Converter} for {@link com.github.benmanes.caffeine.cache.CaffeineSpec}. The spec may
663 * be empty, in which case this converter returns null.
664 */
665 public static final class CaffeineSpecConverter implements Converter<CaffeineSpec> {
666 @Override
667 public CaffeineSpec convert(String spec) throws OptionsParsingException {
668 try {
669 return CaffeineSpec.parse(spec);
670 } catch (IllegalArgumentException e) {
671 throw new OptionsParsingException("Failed to parse CaffeineSpec: " + e.getMessage(), e);
672 }
673 }
674
675 @Override
676 public String getTypeDescription() {
677 return "Converts to a CaffeineSpec, or null if the input is empty";
678 }
679 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100680}