| // Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'package:_fe_analyzer_shared/src/messages/diagnostic.dart' as diag; | |
| import '../messages/codes.dart'; | |
| import 'resolve_input_uri.dart'; | |
| class CommandLineProblem { | |
| final Message message; | |
| CommandLineProblem(this.message); | |
| CommandLineProblem.deprecated(String message) | |
| : this(diag.unspecified.withArguments(message: message)); | |
| @override | |
| String toString() => message.problemMessage; | |
| } | |
| class ParsedOptions { | |
| final Map<String, dynamic> options = <String, dynamic>{}; | |
| final List<String> arguments = <String>[]; | |
| final Map<String, String> defines = <String, String>{}; | |
| @override | |
| String toString() => "ParsedArguments($options, $arguments)"; | |
| /// Returns arguments stored as line separated text. | |
| static List<String> readOptionsFile(String optionsFile) { | |
| return optionsFile | |
| .split('\n') | |
| .map((String line) => line.trim()) | |
| .where((line) => line.isNotEmpty) | |
| .toList(); | |
| } | |
| /// Parses a list of command-line [arguments] into options and arguments. | |
| /// | |
| /// An /option/ is something that, normally, starts with `-` or `--` (one or | |
| /// two dashes). However, as a special case `/?` and `/h` are also recognized | |
| /// as options for increased compatibility with Windows. An option can have a | |
| /// value. | |
| /// | |
| /// An /argument/ is something that isn't an option, for example, a file name. | |
| /// | |
| /// The specification is a map of options to one of the following values: | |
| /// * the type literal `Uri`, representing an option value of type [Uri], | |
| /// * the type literal `int`, representing an option value of type [int], | |
| /// * the bool literal `false`, representing a boolean option that is turned | |
| /// off by default, | |
| /// * the bool literal `true, representing a boolean option that is turned on | |
| /// by default, | |
| /// * or the string literal `","`, representing a comma-separated list of | |
| /// values. | |
| /// | |
| /// If [arguments] contains `"--"`, anything before is parsed as options, and | |
| /// arguments; anything following is treated as arguments (even if starting | |
| /// with, for example, a `-`). | |
| /// | |
| /// If an option isn't found in [options], an error is thrown. | |
| /// | |
| /// Boolean options do not require an option value, but an optional value can | |
| /// be provided using the forms `--option=value` where `value` can be `true` | |
| /// or `yes` to turn on the option, or `false` or `no` to turn it off. If no | |
| /// option value is specified, a boolean option is turned on. | |
| /// | |
| /// All other options require an option value, either on the form `--option | |
| /// value` or `--option=value`. | |
| static ParsedOptions parse(List<String> arguments, List<Option>? options) { | |
| options ??= []; | |
| Map<String, ValueSpecification>? specification = {}; | |
| void addSpec(String flag, ValueSpecification spec) { | |
| if (specification.containsKey(flag)) { | |
| throw new CommandLineProblem.deprecated("Duplicate option '${flag}'."); | |
| } | |
| specification[flag] = spec; | |
| } | |
| for (Option option in options) { | |
| addSpec(option.flag, option.spec); | |
| for (String alias in option.aliases) { | |
| addSpec(alias, new AliasValue(option.flag)); | |
| } | |
| } | |
| ParsedOptions result = new ParsedOptions(); | |
| int index = arguments.indexOf("--"); | |
| Iterable<String> nonOptions = const <String>[]; | |
| Iterator<String> iterator = arguments.iterator; | |
| if (index != -1) { | |
| nonOptions = arguments.skip(index + 1); | |
| iterator = arguments.take(index).iterator; | |
| } | |
| while (iterator.moveNext()) { | |
| String argument = iterator.current; | |
| if (argument.startsWith("-") || argument == "/?" || argument == "/h") { | |
| String? value; | |
| if (argument.startsWith("-D")) { | |
| value = argument.substring("-D".length); | |
| argument = "-D"; | |
| } else { | |
| index = argument.indexOf("="); | |
| if (index != -1) { | |
| value = argument.substring(index + 1); | |
| argument = argument.substring(0, index); | |
| } | |
| } | |
| ValueSpecification? valueSpecification = specification[argument]; | |
| if (valueSpecification == null) { | |
| throw new CommandLineProblem.deprecated( | |
| "Unknown option '$argument'.", | |
| ); | |
| } | |
| String canonicalArgument = argument; | |
| if (valueSpecification.alias != null) { | |
| canonicalArgument = valueSpecification.alias as String; | |
| valueSpecification = specification[valueSpecification.alias]; | |
| } | |
| if (valueSpecification == null) { | |
| throw new CommandLineProblem.deprecated( | |
| "Unknown option alias '$canonicalArgument'.", | |
| ); | |
| } | |
| final bool requiresValue = valueSpecification.requiresValue; | |
| if (requiresValue && value == null) { | |
| if (!iterator.moveNext()) { | |
| throw new CommandLineProblem( | |
| diag.fastaCLIArgumentRequired.withArguments( | |
| argumentName: argument, | |
| ), | |
| ); | |
| } | |
| value = iterator.current; | |
| } | |
| valueSpecification.processValue( | |
| result, | |
| canonicalArgument, | |
| argument, | |
| value, | |
| ); | |
| } else { | |
| result.arguments.add(argument); | |
| } | |
| } | |
| specification.forEach((String key, ValueSpecification value) { | |
| if (value.defaultValue != null) { | |
| result.options[key] ??= value.defaultValue; | |
| } | |
| }); | |
| result.arguments.addAll(nonOptions); | |
| return result; | |
| } | |
| } | |
| abstract class ValueSpecification<T> { | |
| const ValueSpecification(); | |
| String? get alias => null; | |
| T? get defaultValue => null; | |
| bool get requiresValue => true; | |
| void processValue( | |
| ParsedOptions result, | |
| String canonicalArgument, | |
| String argument, | |
| String? value, | |
| ); | |
| } | |
| class AliasValue<T> extends ValueSpecification<T> { | |
| @override | |
| final String alias; | |
| const AliasValue(this.alias); | |
| @override | |
| bool get requiresValue => | |
| throw new UnsupportedError("AliasValue.requiresValue"); | |
| @override | |
| void processValue( | |
| ParsedOptions result, | |
| String canonicalArgument, | |
| String argument, | |
| String? value, | |
| ) { | |
| throw new UnsupportedError("AliasValue.processValue"); | |
| } | |
| } | |
| class UriValue extends ValueSpecification<Uri?> { | |
| const UriValue(); | |
| @override | |
| void processValue( | |
| ParsedOptions result, | |
| String canonicalArgument, | |
| String argument, | |
| String? value, | |
| ) { | |
| if (result.options.containsKey(canonicalArgument)) { | |
| throw new CommandLineProblem.deprecated( | |
| "Multiple values for '$argument': " | |
| "'${result.options[canonicalArgument]}' and '$value'.", | |
| ); | |
| } | |
| // TODO(ahe): resolve Uris lazily, so that schemes provided by | |
| // other flags can be used for parsed command-line arguments too. | |
| result.options[canonicalArgument] = resolveInputUri(value!); | |
| } | |
| } | |
| class StringValue extends ValueSpecification<String?> { | |
| @override | |
| final String? defaultValue; | |
| const StringValue({this.defaultValue}); | |
| @override | |
| void processValue( | |
| ParsedOptions result, | |
| String canonicalArgument, | |
| String argument, | |
| String? value, | |
| ) { | |
| if (result.options.containsKey(canonicalArgument)) { | |
| throw new CommandLineProblem.deprecated( | |
| "Multiple values for '$argument': " | |
| "'${result.options[canonicalArgument]}' and '$value'.", | |
| ); | |
| } | |
| result.options[canonicalArgument] = value!; | |
| } | |
| } | |
| class BoolValue extends ValueSpecification<bool?> { | |
| @override | |
| final bool? defaultValue; | |
| const BoolValue(this.defaultValue); | |
| @override | |
| bool get requiresValue => false; | |
| @override | |
| void processValue( | |
| ParsedOptions result, | |
| String canonicalArgument, | |
| String argument, | |
| String? value, | |
| ) { | |
| if (result.options.containsKey(canonicalArgument)) { | |
| throw new CommandLineProblem.deprecated( | |
| "Multiple values for '$argument': " | |
| "'${result.options[canonicalArgument]}' and '$value'.", | |
| ); | |
| } | |
| bool parsedValue; | |
| if (value == null || value == "true" || value == "yes") { | |
| parsedValue = true; | |
| } else if (value == "false" || value == "no") { | |
| parsedValue = false; | |
| } else { | |
| throw new CommandLineProblem.deprecated( | |
| "Value for '$argument' is '$value', " | |
| "but expected one of: 'true', 'false', 'yes', or 'no'.", | |
| ); | |
| } | |
| result.options[canonicalArgument] = parsedValue; | |
| } | |
| } | |
| class IntValue extends ValueSpecification<int?> { | |
| @override | |
| final int? defaultValue; | |
| final int? noArgValue; | |
| const IntValue({this.defaultValue, this.noArgValue}); | |
| @override | |
| bool get requiresValue => noArgValue == null; | |
| @override | |
| void processValue( | |
| ParsedOptions result, | |
| String canonicalArgument, | |
| String argument, | |
| String? value, | |
| ) { | |
| if (result.options.containsKey(canonicalArgument)) { | |
| throw new CommandLineProblem.deprecated( | |
| "Multiple values for '$argument': " | |
| "'${result.options[canonicalArgument]}' and '$value'.", | |
| ); | |
| } | |
| int? parsedValue = noArgValue; | |
| if (value != null) { | |
| parsedValue = int.tryParse(value); | |
| } | |
| if (parsedValue == null) { | |
| throw new CommandLineProblem.deprecated( | |
| "Value for '$argument', '$value', isn't an int.", | |
| ); | |
| } | |
| result.options[canonicalArgument] = parsedValue; | |
| } | |
| } | |
| class DefineValue extends ValueSpecification<Map<String, String>> { | |
| const DefineValue(); | |
| @override | |
| void processValue( | |
| ParsedOptions result, | |
| String canonicalArgument, | |
| String argument, | |
| String? value, | |
| ) { | |
| int index = value!.indexOf('='); | |
| String name; | |
| String expression; | |
| if (index != -1) { | |
| name = value.substring(0, index); | |
| expression = value.substring(index + 1); | |
| } else { | |
| name = value; | |
| expression = value; | |
| } | |
| result.defines[name] = expression; | |
| } | |
| } | |
| class StringListValue extends ValueSpecification<List<String>?> { | |
| const StringListValue(); | |
| @override | |
| void processValue( | |
| ParsedOptions result, | |
| String canonicalArgument, | |
| String argument, | |
| String? value, | |
| ) { | |
| List<String> values = result.options[canonicalArgument] ??= <String>[]; | |
| values.addAll(value!.split(",")); | |
| } | |
| } | |
| class UriListValue extends ValueSpecification<List<Uri>?> { | |
| const UriListValue(); | |
| @override | |
| void processValue( | |
| ParsedOptions result, | |
| String canonicalArgument, | |
| String argument, | |
| String? value, | |
| ) { | |
| List<Uri> values = result.options[canonicalArgument] ??= <Uri>[]; | |
| values.addAll(value!.split(",").map(resolveInputUri)); | |
| } | |
| } | |
| class Option<T> { | |
| final String flag; | |
| final ValueSpecification<T?> spec; | |
| final bool isDefines; | |
| final List<String> aliases; | |
| const Option( | |
| this.flag, | |
| this.spec, { | |
| this.isDefines = false, | |
| this.aliases = const [], | |
| }); | |
| T read(ParsedOptions parsedOptions) { | |
| if (isDefines) { | |
| return parsedOptions.defines as T; | |
| } | |
| return parsedOptions.options[flag] as T; | |
| } | |
| List<String> remove(List<String> arguments) { | |
| arguments.remove(flag); | |
| for (String alias in aliases) { | |
| arguments.remove(alias); | |
| } | |
| return arguments; | |
| } | |
| } |