blob: 84ed2991500cade74c827a45685cf51b486217bc [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:typed_data';
import 'package:charcode/ascii.dart';
/// Parses argument lists based on a [Flags] configuration.
///
/// Arguments are either literals or flags.
///
/// Flags start with `-` or `--`
/// Flags starting with `-` are single-character flags, like `-x`.
/// Single character flag names must be ASCII.
/// Flags starting with `--` are named, like `--expand`.
/// They extend to the end of the argument, or until a `=`.
///
/// Flags can have parameters.
///
/// Single-character flags can have an parameter:
/// * immediately after the charcter, `-ofilename`,
/// * with a `=` between them, `-o=filename`,
/// * or as the next argument, `-o filename`.
///
/// Named flags cannot be directly concatenated with the parameter,
/// but must be one of `--output=filename` or `--output filename`.
/// Named flags conflate non-ASCII alphanumeric characters, like `-` and `_`
/// (but not `=` which delimites a parameter value).
/// Any non-letter, non-digit character sequence is matched by any other,
/// so `--foo-bar`, `--foo_bar` and `--foo.<!>.bar` are all the same.
///
/// Flags can have *optional* parameters. A flag with an optional parameter
/// cannot have its value in the next argument, it *must* use `=` or have
/// the value immediately after a single character flag.
/// It has a default value to use if the parameter is omitted.
///
/// An unrecognized or malformed flag is reported using the [warn]
/// function.
Iterable<CmdLineArg<T>> parseFlags<T>(
Flags<T> flags, Iterable<String> arguments,
[void warn(String warning)?]) sync* {
warn ??= _printWarning;
var args = arguments.iterator;
while (args.moveNext()) {
var arg = args.current;
if (arg.startsWith("-")) {
if (arg.startsWith("-", 1)) {
// Named flags.
if (arg.length == 2) {
// Found `--`. Stop parsing flags.
break;
}
var equals = arg.indexOf("=", 2);
if (equals >= 0) {
var name = arg.substring(2, equals);
var value = arg.substring(equals + 1);
var flag = flags.byName(name);
if (flag != null) {
if (!flag.hasParameter) {
warn("Flag $name should not have a parameter: $arg");
continue;
}
yield CmdLineArg<T>(flag.key, value);
continue;
}
warn("Unknown flag: $arg");
continue;
}
var name = arg.substring(2);
var flag = flags.byName(name);
if (flag != null) {
var value = flag.value;
if (flag.hasParameter &&
!flag.hasOptionalParameter &&
args.moveNext()) {
value = args.current;
}
yield CmdLineArg<T>(flag.key, value);
continue;
}
warn("Unknown flag: $arg");
continue;
}
// Character flag(s).
for (var i = 1; i < arg.length; i++) {
var char = arg.codeUnitAt(i);
var flag = flags.byChar(char);
if (flag == null) {
warn("Unknown flag: ${arg.substring(i, i + 1)}");
continue;
}
var value = flag.value;
if (arg.startsWith("=", i + 1)) {
value = arg.substring(i + 2);
if (!flag.hasParameter) {
warn(
"Flag ${arg.substring(i, i + 1)} should not have a parameter: ${arg.substring(i)}");
break;
}
yield CmdLineArg<T>(flag.key, value);
break;
}
if (flag.hasParameter) {
if (i + 1 < arg.length) {
value = arg.substring(i + 1);
} else if (!flag.hasOptionalParameter && args.moveNext()) {
value = args.current;
}
yield CmdLineArg<T>(flag.key, value);
break;
}
yield CmdLineArg<T>(flag.key, value);
}
continue;
}
yield CmdLineArg<Never>(null, arg);
}
// Handle entries after `--`.
while (args.moveNext()) {
yield CmdLineArg<Never>(null, args.current);
}
}
/// A part of the arguments list recognized as a flag or not.
///
/// If [key] is `null`, the [value] is a plain argument list entry.
/// Otherwise they key corresponds to the flag that was recognized,
/// and [value] is its parameter or default value, if any.
///
class CmdLineArg<T> {
final T? key;
final String? value;
CmdLineArg(this.key, this.value);
bool get isFlag => key != null;
}
/// A flag configuration.
///
/// Collects one or more [FlagConfig] objects and allows quick look-up
/// on character or name.
class Flags<T> {
final List<FlagConfig<T>?> _charFlags =
List<FlagConfig<T>?>.filled(128, null, growable: false);
final Map<String, FlagConfig<T>> _namedFlags = {};
void add(FlagConfig<T> flag) {
var char = flag.flagChar;
if (char != null) {
_charFlags[char] = flag;
}
var name = flag.flagName;
if (name != null) {
_namedFlags[name] = flag;
}
}
void addBoolFlag(T key, String flagChar, String flagName,
[String? description]) {
add(FlagConfig.optionalParameter(key, flagChar, flagName, "true",
description: description, valueDescription: "true"));
add(FlagConfig.optionalParameter(key, null, "no-" + flagName, "false"));
}
FlagConfig<T>? byName(String name) => _namedFlags[name];
FlagConfig<T>? byChar(int char) =>
0 <= char && char <= 217 ? _charFlags[char] : null;
void writeUsage(StringSink buffer) {
const descriptionStart = 28;
var allFlags = [
...{
for (var flag in _namedFlags.values)
if (!flag.flagName!.startsWith("no-") || flag.value != "false") flag,
for (var flag in _charFlags)
if (flag != null && flag.flagName == null) flag
}
]..sort(_flagOrder);
for (var flag in allFlags) {
var name = flag.flagName;
var char = flag.flagChar;
var parameter = flag.valueDescription ?? "VALUE";
var description = flag.description;
var lineLength = 0;
if (char != null) {
buffer
..write(" -")
..writeCharCode(char);
lineLength = 4;
if (name != null) {
buffer..write(', --')..write(name);
lineLength = name.length + 8;
}
} else if (name != null) {
buffer..write(" --")..write(name);
lineLength = name.length + 8;
} else {
continue;
}
if (flag.hasParameter) {
var end = "";
if (flag.hasOptionalParameter) {
buffer.write('[=');
lineLength += 2;
end = "]";
} else {
buffer.write('=');
lineLength += 1;
}
buffer.write(parameter);
lineLength += parameter.length;
buffer.write(end);
lineLength += end.length;
}
if (description != null) {
if (lineLength < descriptionStart) {
do {
buffer.write(' ');
lineLength += 1;
} while (lineLength < descriptionStart);
} else {
buffer.write(' ');
lineLength += 1;
}
var indent = " "; // 30 spaces.
_writeSplitDescription(buffer, description, lineLength, 80, indent);
} else {
buffer.writeln();
}
}
}
void _writeSplitDescription(StringSink output, String description, int indent,
int maxLength, String newLineIndent) {
var index = 0;
var end = index + (maxLength - indent);
end:
while (end < description.length) {
line:
while (description.codeUnitAt(end) != $space) {
end--;
if (end == index) {
end = index + (maxLength - indent) + 1;
while (end < description.length) {
if (description.codeUnitAt(end) == $space) {
break line;
}
end++;
}
break end;
}
}
output.writeln(description.substring(index, end));
index = end + 1;
output.write(newLineIndent);
indent = newLineIndent.length;
end = index + (maxLength - indent);
}
if (index < description.length) {
output.writeln(description.substring(index));
}
}
static int _flagOrder(FlagConfig a, FlagConfig b) {
var aName = a.flagName;
var bName = b.flagName;
if (aName != null) {
if (bName != null) return aName.compareTo(bName);
return aName.codeUnitAt(0) < b.flagChar! ? -1 : 1;
}
if (bName != null) {
return a.flagChar! < bName.codeUnitAt(0) ? -1 : 1;
}
return a.flagChar! - b.flagChar!;
}
}
/// Configuration of a single flag.
class FlagConfig<T> {
/// The user designated key linked to this flag.
final T key;
/// ASCII character code for the single-character flag.
///
/// Must be a digit or letter. Does distinguish case.
final int? flagChar;
/// Flag name.
///
/// Canonicalized to lower-case letters, digits and single `-` characters.
final String? flagName;
/// Whether the flag expects a parameter.
///
/// A flag expecting a parameter which is not optional ([hasOptionalParameter])
/// will require a value in the argument list to be well-formed.
final bool hasParameter;
/// Whether the parameter is optional.
///
/// An optional parameter can be omitted.
final bool hasOptionalParameter;
/// A name for the parameter, if there is a parameter.
///
/// Traditionally an all-upper-case name.
final String? valueDescription;
/// The value associated with the flag.
///
/// A flag without parameters can have a value configured, which allows the same
/// [key] to be used for different flags.
///
/// A flag with an optional parameter will have a default value, which may be
/// null.
final String? value;
/// Description for documentation purposes.
final String? description;
FlagConfig._(
this.key,
String? flagChar,
String? flagName,
this.hasParameter,
this.hasOptionalParameter,
this.value,
this.description,
this.valueDescription)
: flagChar = _checkFlagChar(flagChar),
flagName = canonicalizeName(flagName);
FlagConfig(T key, String? flagChar, String? flagName,
{String? value, String? description, String valueDescription = "VALUE"})
: this._(key, flagChar, flagName, false, false, value, description,
valueDescription);
FlagConfig.requiredParameter(T key, String? flagChar, String? flagName,
{String? description, String valueDescription = "VALUE"})
: this._(key, flagChar, flagName, true, false, null, description,
valueDescription);
FlagConfig.optionalParameter(
T key, String? flagChar, String? flagName, String defaultValue,
{String? description, String valueDescription = "VALUE"})
: this._(key, flagChar, flagName, true, true, defaultValue, description,
valueDescription);
static int? _checkFlagChar(String? flagChar) {
if (flagChar == null) return null;
if (flagChar.length == 1) {
var char = flagChar.codeUnitAt(0);
if (char ^ 0x30 <= 9) return char;
var lc = char | 0x20;
if (lc >= 0x61 && lc <= 0x7b) return char;
}
throw ArgumentError.value(flagChar, "flagChar",
"Must be a single ASCII digit or letter character");
}
}
String? canonicalizeName(String? name) {
if (name == null) return name;
const $dash = 0x2d;
var wasDash = false;
var i = 0;
while (i < name.length) {
var char = name.codeUnitAt(i++);
if (char ^ 0x30 <= 9 || char >= 0x61 && char <= 0x7b) {
wasDash = false;
continue;
}
if (char == $dash && !wasDash) {
wasDash = true;
continue;
}
var bytes = Uint8List(name.length);
var j = 0;
for (; j < i - 1; j++) {
bytes[j] = name.codeUnitAt(j);
}
// Convert all letters to lower-case, all non letter/digits to a single `-`.
outer:
do {
if (char >= 0x41 && char <= 0x5b) {
bytes[j++] = char | 0x20;
i++;
wasDash = false;
} else {
if (!wasDash) {
bytes[j++] = $dash;
i++;
wasDash = true;
}
}
while (i < name.length) {
char = name.codeUnitAt(i++);
if (char ^ 0x30 <= 9 || char >= 0x61 && char <= 0x7b) {
bytes[j++] = char;
wasDash = false;
continue;
}
if (char == $dash && !wasDash) {
bytes[j++] = char;
wasDash = true;
continue;
}
continue outer;
}
break;
} while (true);
return String.fromCharCodes(bytes);
}
return name;
}
void _printWarning(String message) {
print(message);
}