// 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> parseFlags( Flags flags, Iterable 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(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(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(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(flag.key, value); break; } yield CmdLineArg(flag.key, value); } continue; } yield CmdLineArg(null, arg); } // Handle entries after `--`. while (args.moveNext()) { yield CmdLineArg(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 { 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 { final List?> _charFlags = List?>.filled(128, null, growable: false); final Map> _namedFlags = {}; void add(FlagConfig 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? byName(String name) => _namedFlags[name]; FlagConfig? 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 { /// 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); }