| // 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. If omitted, the [warn] function defaults to printing |
| /// using the [print] function. |
| Iterable<CmdLineArg<T>> parseFlags<T>( |
| Flags<T> flags, Iterable<String> arguments, |
| [void Function(String warning)? warn]) 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"); |
| } |
| } |
| |
| /// Converts names to canonical form. |
| /// |
| /// Canonical form consists of only *lower case ASCII letters*, |
| /// *decimal digits* and single *dash* characters (`-`) separating letter/digit |
| /// sequences. |
| /// |
| /// All upper-case letters are made lower-case. |
| /// If the input-name contains sequences of non-letter, non-digit characters, |
| /// each sequence is replaced by a single `-`. |
| /// Leading and trailing `-`s are then ignored |
| /// if the result contains anything other than `-`. |
| String? canonicalizeName(String? name) { |
| if (name == null) return name; |
| const $dash = 0x2d; |
| var wasDash = false; |
| var i = 0; |
| var upperCase = 0x20; |
| while (i < name.length) { |
| var char = name.codeUnitAt(i++); |
| var lcChar = char | 0x20; |
| if (char ^ 0x30 <= 9 || lcChar >= 0x61 && lcChar <= 0x7b) { |
| wasDash = false; |
| upperCase &= char; |
| 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) | 0x20; |
| } |
| |
| // Convert all letters to lower-case, all non letter/digits to a single `-`. |
| outer: |
| do { |
| if (!wasDash) { |
| bytes[j++] = $dash; |
| wasDash = true; |
| } |
| while (i < name.length) { |
| char = name.codeUnitAt(i++); |
| var lcChar = char | 0x20; |
| if (char ^ 0x30 <= 9 || lcChar >= 0x61 && lcChar <= 0x7b) { |
| bytes[j++] = lcChar; |
| wasDash = false; |
| continue; |
| } |
| if (char == $dash && !wasDash) { |
| bytes[j++] = char; |
| wasDash = true; |
| continue; |
| } |
| continue outer; |
| } |
| break; |
| } while (true); |
| var start = 0; |
| var end = j; |
| if (end > start + 1) { |
| // Omit leading/trailing dashes. |
| if (bytes[start] == $dash) start++; |
| if (bytes[end - 1] == $dash) end--; |
| } |
| return String.fromCharCodes(Uint8List.sublistView(bytes, start, end)); |
| } |
| return upperCase == 0 ? name.toLowerCase() : name; |
| } |
| |
| void _printWarning(String message) { |
| print(message); |
| } |