blob: 7837865c791d11128f829cc22342e84dae504cac [file] [log] [blame]
// 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 '../messages/codes.dart';
import 'resolve_input_uri.dart';
class CommandLineProblem {
final Message message;
CommandLineProblem(this.message);
CommandLineProblem.deprecated(String message)
: this(templateUnspecified.withArguments(message));
String toString() => message.message;
}
class ParsedOptions {
final Map<String, dynamic> options = <String, dynamic>{};
final List<String> arguments = <String>[];
final Map<String, String> defines = <String, String>{};
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 [specification], 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(
templateFastaCLIArgumentRequired.withArguments(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> {
final String alias;
const AliasValue(this.alias);
bool get requiresValue =>
throw new UnsupportedError("AliasValue.requiresValue");
void processValue(ParsedOptions result, String canonicalArgument,
String argument, String? value) {
throw new UnsupportedError("AliasValue.processValue");
}
}
class UriValue extends ValueSpecification<Uri?> {
const UriValue();
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?> {
final String? defaultValue;
const StringValue({this.defaultValue});
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?> {
final bool? defaultValue;
const BoolValue(this.defaultValue);
bool get requiresValue => false;
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?> {
final int? defaultValue;
final int? noArgValue;
const IntValue({this.defaultValue, this.noArgValue});
bool get requiresValue => noArgValue == null;
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();
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();
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();
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) =>
(isDefines ? parsedOptions.defines : parsedOptions.options[flag]) as T;
}