blob: 8f3aa6701d9e6553ef542c760b550c982f9e3a57 [file]
// Copyright (c) 2022, 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 'dart:io';
import 'package:args/args.dart';
import 'package:yaml/yaml.dart';
class ConfigError extends Error {
ConfigError(this.message);
final String message;
@override
String toString() => message;
}
/// YAML Reader which enables to override specific values from command line.
class YamlReader {
YamlReader.of(this.cli, this.yaml);
YamlReader.fromYaml(this.yaml) : cli = const {};
YamlReader.fromMap(this.cli) : yaml = const {};
Map<String, String> cli;
Map<dynamic, dynamic> yaml;
/// Parses the provided command line arguments and returns a [YamlReader].
///
/// This is a utility function which does all things a program would do when
/// parsing command line arguments, including exiting from the program when
/// arguments are invalid.
static YamlReader parseArgs(List<String> args,
{bool allowYamlConfig = true}) {
final parser = ArgParser();
parser.addFlag('help', abbr: 'h', help: 'Show this help.');
// Sometimes it's required to change a config value for a single invocation,
// then this option can be used. Conventionally in -D switch is used in
// C to set preprocessor variable & in java to override a config property.
parser.addMultiOption('override',
abbr: 'D',
help: 'Override or assign a config property from command line.');
if (allowYamlConfig) {
parser.addOption('config', abbr: 'c', help: 'Path to YAML config.');
}
final results = parser.parse(args);
if (results['help']) {
stderr.writeln(parser.usage);
exit(1);
}
final configFile = results['config'] as String?;
Map<dynamic, dynamic> yamlMap = {};
if (configFile != null) {
try {
final yamlInput = loadYaml(File(configFile).readAsStringSync(),
sourceUrl: Uri.file(configFile));
if (yamlInput is Map) {
yamlMap = yamlInput;
} else {
throw ConfigError('YAML config must be set of key value pairs');
}
} on Exception catch (e) {
stderr.writeln('cannot read $configFile: $e');
}
}
final regex = RegExp('([a-z-_.]+)=(.+)');
final properties = <String, String>{};
for (var prop in results['override']) {
final match = regex.matchAsPrefix(prop as String);
if (match != null && match.group(0) == prop) {
final propertyName = match.group(1);
final propertyValue = match.group(2);
properties[propertyName!] = propertyValue!;
} else {
throw ConfigError('override does not match expected pattern');
}
}
return YamlReader.of(properties, yamlMap);
}
bool? getBool(String property) {
if (cli.containsKey(property)) {
final v = cli[property]!;
if (v == 'true') {
return true;
}
if (v == 'false') {
return false;
}
throw ConfigError('expected boolean value for $property, got $v');
}
return getYamlValue<bool>(property);
}
String? getString(String property) {
final configValue = cli[property] ?? getYamlValue<String>(property);
return configValue;
}
List<String>? getStringList(String property) {
final configValue = cli[property]?.split(';') ??
getYamlValue<YamlList>(property)?.cast<String>();
return configValue;
}
String? getOneOf(String property, Set<String> values) {
final value = cli[property] ?? getYamlValue<String>(property);
if (value == null || values.contains(value)) {
return value;
}
throw ConfigError('expected one of $values for $property');
}
Map<String, String>? getStringMap(String property) {
final value = getYamlValue<YamlMap>(property);
return value?.cast<String, String>();
}
bool hasValue(String property) => getYamlValue<dynamic>(property) != null;
T? getYamlValue<T>(String property) {
final path = property.split('.');
dynamic cursor = yaml;
String current = '';
for (var i in path) {
if (cursor is YamlMap || cursor is Map) {
cursor = cursor[i];
} else {
throw ConfigError('expected $current to be a YAML map');
}
current = [if (current != '') current, i].join('.');
if (cursor == null) {
return null;
}
}
if (cursor is! T) {
throw ConfigError('expected $T for $property, got ${cursor.runtimeType}');
}
return cursor;
}
}