| // Copyright (c) 2014, 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:async'; | |
| import 'dart:collection'; | |
| import 'dart:math' as math; | |
| import 'src/arg_parser.dart'; | |
| import 'src/arg_parser_exception.dart'; | |
| import 'src/arg_results.dart'; | |
| import 'src/help_command.dart'; | |
| import 'src/usage_exception.dart'; | |
| import 'src/utils.dart'; | |
| export 'src/usage_exception.dart'; | |
| /// A class for invoking [Command]s based on raw command-line arguments. | |
| /// | |
| /// The type argument `T` represents the type returned by [Command.run] and | |
| /// [CommandRunner.run]; it can be ommitted if you're not using the return | |
| /// values. | |
| class CommandRunner<T> { | |
| /// The name of the executable being run. | |
| /// | |
| /// Used for error reporting and [usage]. | |
| final String executableName; | |
| /// A short description of this executable. | |
| final String description; | |
| /// A single-line template for how to invoke this executable. | |
| /// | |
| /// Defaults to `"$executableName <command> arguments"` (if there is no | |
| /// default command) or `"$executableName [<command>] arguments"` (otherwise). | |
| /// | |
| /// Subclasses can override this for a more specific template. | |
| String get invocation { | |
| var command = '<command>'; | |
| if (argParser.defaultCommand != null) { | |
| command = '[$command]'; | |
| } | |
| return '$executableName $command [arguments]'; | |
| } | |
| /// Generates a string displaying usage information for the executable. | |
| /// | |
| /// This includes usage for the global arguments as well as a list of | |
| /// top-level commands. | |
| String get usage => _wrap('$description\n\n') + _usageWithoutDescription; | |
| /// An optional footer for [usage]. | |
| /// | |
| /// If a subclass overrides this to return a string, it will automatically be | |
| /// added to the end of [usage]. | |
| String? get usageFooter => null; | |
| /// Returns [usage] with [description] removed from the beginning. | |
| String get _usageWithoutDescription { | |
| var usagePrefix = 'Usage:'; | |
| var buffer = StringBuffer(); | |
| buffer.writeln( | |
| '$usagePrefix ${_wrap(invocation, hangingIndent: usagePrefix.length)}\n', | |
| ); | |
| buffer.writeln(_wrap('Global options:')); | |
| buffer.writeln('${argParser.usage}\n'); | |
| buffer.writeln(_getCommandUsage(_commands, | |
| lineLength: argParser.usageLineLength, | |
| defaultCommand: argParser.defaultCommand)); | |
| buffer.writeln(); | |
| buffer.write(_wrap( | |
| 'Run "$executableName help <command>" for more information about a ' | |
| 'command.')); | |
| if (usageFooter != null) { | |
| buffer.write('\n${_wrap(usageFooter!)}'); | |
| } | |
| return buffer.toString(); | |
| } | |
| /// An unmodifiable view of all top-level commands defined for this runner. | |
| Map<String, Command<T>> get commands => UnmodifiableMapView(_commands); | |
| final _commands = <String, Command<T>>{}; | |
| /// The top-level argument parser. | |
| /// | |
| /// Global options should be registered with this parser; they'll end up | |
| /// available via [Command.globalResults]. Commands should be registered with | |
| /// [addCommand] rather than directly on the parser. | |
| ArgParser get argParser => _argParser; | |
| final ArgParser _argParser; | |
| /// The maximum edit distance allowed when suggesting possible intended | |
| /// commands. | |
| /// | |
| /// Set to `0` in order to disable suggestions, defaults to `2`. | |
| final int suggestionDistanceLimit; | |
| CommandRunner(this.executableName, this.description, | |
| {int? usageLineLength, this.suggestionDistanceLimit = 2}) | |
| : _argParser = ArgParser(usageLineLength: usageLineLength) { | |
| argParser.addFlag('help', | |
| abbr: 'h', negatable: false, help: 'Print this usage information.'); | |
| addCommand(HelpCommand<T>()); | |
| } | |
| /// Prints the usage information for this runner. | |
| /// | |
| /// This is called internally by [run] and can be overridden by subclasses to | |
| /// control how output is displayed or integrate with a logging system. | |
| void printUsage() => print(usage); | |
| /// Throws a [UsageException] with [message]. | |
| Never usageException(String message) => | |
| throw UsageException(message, _usageWithoutDescription); | |
| /// Adds [Command] as a top-level command to this runner. | |
| /// | |
| /// If [isDefault] is `true` then added command will be designated as a | |
| /// default one. Default command is selected if no other sibling command | |
| /// matches. Only a single leaf-command can be designated as a default. | |
| void addCommand(Command<T> command, {bool isDefault = false}) { | |
| if (isDefault && command.subcommands.isNotEmpty) { | |
| throw ArgumentError('default command must be a leaf command'); | |
| } | |
| if (isDefault && argParser.defaultCommand != null) { | |
| throw StateError('default command already defined'); | |
| } | |
| var names = [command.name, ...command.aliases]; | |
| for (var name in names) { | |
| _commands[name] = command; | |
| argParser.addCommand(name, command.argParser); | |
| } | |
| if (isDefault) { | |
| argParser.defaultCommand = command.name; | |
| } | |
| command._runner = this; | |
| } | |
| /// Parses [args] and invokes [Command.run] on the chosen command. | |
| /// | |
| /// This always returns a [Future] in case the command is asynchronous. The | |
| /// [Future] will throw a [UsageException] if [args] was invalid. | |
| Future<T?> run(Iterable<String> args) => | |
| Future.sync(() => runCommand(parse(args))); | |
| /// Parses [args] and returns the result, converting an [ArgParserException] | |
| /// to a [UsageException]. | |
| /// | |
| /// This is notionally a protected method. It may be overridden or called from | |
| /// subclasses, but it shouldn't be called externally. | |
| ArgResults parse(Iterable<String> args) { | |
| try { | |
| return argParser.parse(args); | |
| } on ArgParserException catch (error) { | |
| if (error.commands.isEmpty) usageException(error.message); | |
| var command = commands[error.commands.first]!; | |
| for (var commandName in error.commands.skip(1)) { | |
| command = command.subcommands[commandName]!; | |
| } | |
| command.usageException(error.message); | |
| } | |
| } | |
| /// Runs the command specified by [topLevelResults]. | |
| /// | |
| /// This is notionally a protected method. It may be overridden or called from | |
| /// subclasses, but it shouldn't be called externally. | |
| /// | |
| /// It's useful to override this to handle global flags and/or wrap the entire | |
| /// command in a block. For example, you might handle the `--verbose` flag | |
| /// here to enable verbose logging before running the command. | |
| /// | |
| /// This returns the return value of [Command.run]. | |
| Future<T?> runCommand(ArgResults topLevelResults) async { | |
| var argResults = topLevelResults; | |
| var commands = _commands; | |
| Command? command; | |
| var commandString = executableName; | |
| while (commands.isNotEmpty) { | |
| if (argResults.command == null) { | |
| if (argResults.rest.isEmpty) { | |
| if (command == null) { | |
| // No top-level command was chosen. | |
| printUsage(); | |
| return null; | |
| } | |
| command.usageException('Missing subcommand for "$commandString".'); | |
| } else { | |
| var requested = argResults.rest[0]; | |
| // Build up a help message containing similar commands, if found. | |
| var similarCommands = | |
| _similarCommandsText(requested, commands.values); | |
| if (command == null) { | |
| usageException( | |
| 'Could not find a command named "$requested".$similarCommands'); | |
| } | |
| command.usageException('Could not find a subcommand named ' | |
| '"$requested" for "$commandString".$similarCommands'); | |
| } | |
| } | |
| // Step into the command. | |
| argResults = argResults.command!; | |
| command = commands[argResults.name]!; | |
| command._globalResults = topLevelResults; | |
| command._argResults = argResults; | |
| commands = command._subcommands as Map<String, Command<T>>; | |
| commandString += ' ${argResults.name}'; | |
| if (argResults.options.contains('help') && argResults.flag('help')) { | |
| command.printUsage(); | |
| return null; | |
| } | |
| } | |
| if (topLevelResults.flag('help')) { | |
| command!.printUsage(); | |
| return null; | |
| } | |
| // Make sure there aren't unexpected arguments. | |
| if (!command!.takesArguments && argResults.rest.isNotEmpty) { | |
| command.usageException( | |
| 'Command "${argResults.name}" does not take any arguments.'); | |
| } | |
| return (await command.run()) as T?; | |
| } | |
| // Returns help text for commands similar to `name`, in sorted order. | |
| String _similarCommandsText(String name, Iterable<Command<T>> commands) { | |
| if (suggestionDistanceLimit <= 0) return ''; | |
| var distances = <Command<T>, int>{}; | |
| var candidates = | |
| SplayTreeSet<Command<T>>((a, b) => distances[a]! - distances[b]!); | |
| for (var command in commands) { | |
| if (command.hidden) continue; | |
| for (var alias in [ | |
| command.name, | |
| ...command.aliases, | |
| ...command.suggestionAliases | |
| ]) { | |
| var distance = _editDistance(name, alias); | |
| if (distance <= suggestionDistanceLimit) { | |
| distances[command] = | |
| math.min(distances[command] ?? distance, distance); | |
| candidates.add(command); | |
| } | |
| } | |
| } | |
| if (candidates.isEmpty) return ''; | |
| var similar = StringBuffer(); | |
| similar | |
| ..writeln() | |
| ..writeln() | |
| ..writeln('Did you mean one of these?'); | |
| for (var command in candidates) { | |
| similar.writeln(' ${command.name}'); | |
| } | |
| return similar.toString(); | |
| } | |
| String _wrap(String text, {int? hangingIndent}) => wrapText(text, | |
| length: argParser.usageLineLength, hangingIndent: hangingIndent); | |
| } | |
| /// A single command. | |
| /// | |
| /// A command is known as a "leaf command" if it has no subcommands and is meant | |
| /// to be run. Leaf commands must override [run]. | |
| /// | |
| /// A command with subcommands is known as a "branch command" and cannot be run | |
| /// itself. It should call [addSubcommand] (often from the constructor) to | |
| /// register subcommands. | |
| abstract class Command<T> { | |
| /// The name of this command. | |
| String get name; | |
| /// A description of this command, included in [usage]. | |
| String get description; | |
| /// A short description of this command, included in [parent]'s | |
| /// [CommandRunner.usage]. | |
| /// | |
| /// This defaults to the first line of [description]. | |
| String get summary => description.split('\n').first; | |
| /// The command's category. | |
| /// | |
| /// Displayed in [parent]'s [CommandRunner.usage]. Commands with categories | |
| /// will be grouped together, and displayed after commands without a category. | |
| String get category => ''; | |
| /// A single-line template for how to invoke this command (e.g. `"pub get | |
| /// `package`"`). | |
| String get invocation { | |
| var parents = [name]; | |
| for (var command = parent; command != null; command = command.parent) { | |
| parents.add(command.name); | |
| } | |
| parents.add(runner!.executableName); | |
| var invocation = parents.reversed.join(' '); | |
| if (argParser.defaultCommand != null) { | |
| return '$invocation [<subcommand>] [arguments]'; | |
| } else if (_subcommands.isNotEmpty) { | |
| return '$invocation <subcommand> [arguments]'; | |
| } else { | |
| return '$invocation [arguments]'; | |
| } | |
| } | |
| /// The command's parent command, if this is a subcommand. | |
| /// | |
| /// This will be `null` until [addSubcommand] has been called with | |
| /// this command. | |
| Command<T>? get parent => _parent; | |
| Command<T>? _parent; | |
| /// The command runner for this command. | |
| /// | |
| /// This will be `null` until [CommandRunner.addCommand] has been called with | |
| /// this command or one of its parents. | |
| CommandRunner<T>? get runner { | |
| if (parent == null) return _runner; | |
| return parent!.runner; | |
| } | |
| CommandRunner<T>? _runner; | |
| /// The parsed global argument results. | |
| /// | |
| /// This will be `null` until just before [Command.run] is called. | |
| ArgResults? get globalResults => _globalResults; | |
| ArgResults? _globalResults; | |
| /// The parsed argument results for this command. | |
| /// | |
| /// This will be `null` until just before [Command.run] is called. | |
| ArgResults? get argResults => _argResults; | |
| ArgResults? _argResults; | |
| /// The argument parser for this command. | |
| /// | |
| /// Options for this command should be registered with this parser (often in | |
| /// the constructor); they'll end up available via [argResults]. Subcommands | |
| /// should be registered with [addSubcommand] rather than directly on the | |
| /// parser. | |
| /// | |
| /// This can be overridden to change the arguments passed to the `ArgParser` | |
| /// constructor. | |
| ArgParser get argParser => _argParser; | |
| final _argParser = ArgParser(); | |
| /// Generates a string displaying usage information for this command. | |
| /// | |
| /// This includes usage for the command's arguments as well as a list of | |
| /// subcommands, if there are any. | |
| String get usage => _wrap('$description\n\n') + _usageWithoutDescription; | |
| /// An optional footer for [usage]. | |
| /// | |
| /// If a subclass overrides this to return a string, it will automatically be | |
| /// added to the end of [usage]. | |
| String? get usageFooter => null; | |
| String _wrap(String text, {int? hangingIndent}) { | |
| return wrapText(text, | |
| length: argParser.usageLineLength, hangingIndent: hangingIndent); | |
| } | |
| /// Returns [usage] with [description] removed from the beginning. | |
| String get _usageWithoutDescription { | |
| var length = argParser.usageLineLength; | |
| var usagePrefix = 'Usage: '; | |
| var buffer = StringBuffer() | |
| ..writeln( | |
| usagePrefix + _wrap(invocation, hangingIndent: usagePrefix.length)) | |
| ..writeln(argParser.usage); | |
| if (_subcommands.isNotEmpty) { | |
| buffer.writeln(); | |
| buffer.writeln(_getCommandUsage(_subcommands, | |
| isSubcommand: true, | |
| lineLength: length, | |
| defaultCommand: argParser.defaultCommand)); | |
| } | |
| buffer.writeln(); | |
| buffer.write( | |
| _wrap('Run "${runner!.executableName} help" to see global options.')); | |
| if (usageFooter != null) { | |
| buffer.writeln(); | |
| buffer.write(_wrap(usageFooter!)); | |
| } | |
| return buffer.toString(); | |
| } | |
| /// An unmodifiable view of all sublevel commands of this command. | |
| Map<String, Command<T>> get subcommands => UnmodifiableMapView(_subcommands); | |
| final _subcommands = <String, Command<T>>{}; | |
| /// Whether or not this command should be hidden from help listings. | |
| /// | |
| /// This is intended to be overridden by commands that want to mark themselves | |
| /// hidden. | |
| /// | |
| /// By default, leaf commands are always visible. Branch commands are visible | |
| /// as long as any of their leaf commands are visible. | |
| bool get hidden { | |
| // Leaf commands are visible by default. | |
| if (_subcommands.isEmpty) return false; | |
| // Otherwise, a command is hidden if all of its subcommands are. | |
| return _subcommands.values.every((subcommand) => subcommand.hidden); | |
| } | |
| /// Whether or not this command takes positional arguments in addition to | |
| /// options. | |
| /// | |
| /// If false, [CommandRunner.run] will throw a [UsageException] if arguments | |
| /// are provided. Defaults to true. | |
| /// | |
| /// This is intended to be overridden by commands that don't want to receive | |
| /// arguments. It has no effect for branch commands. | |
| bool get takesArguments => true; | |
| /// Alternate names for this command. | |
| /// | |
| /// These names won't be used in the documentation, but they will work when | |
| /// invoked on the command line. | |
| /// | |
| /// This is intended to be overridden. | |
| List<String> get aliases => const []; | |
| /// Alternate non-functional names for this command. | |
| /// | |
| /// These names won't be used in the documentation, and also they won't work | |
| /// when invoked on the command line. But if an unknown command is used it | |
| /// will be matched against this when creating suggestions. | |
| /// | |
| /// A name does not have to be repeated both here and in [aliases]. | |
| /// | |
| /// This is intended to be overridden. | |
| List<String> get suggestionAliases => const []; | |
| Command() { | |
| if (!argParser.allowsAnything) { | |
| argParser.addFlag('help', | |
| abbr: 'h', negatable: false, help: 'Print this usage information.'); | |
| } | |
| } | |
| /// Runs this command. | |
| /// | |
| /// The return value is wrapped in a `Future` if necessary and returned by | |
| /// [CommandRunner.runCommand]. | |
| FutureOr<T>? run() { | |
| throw UnimplementedError(_wrap('Leaf command $this must implement run().')); | |
| } | |
| /// Adds [Command] as a subcommand of this. | |
| /// | |
| /// If [isDefault] is `true` then added command will be designated as a | |
| /// default one. Default subcommand is selected if no other sibling subcommand | |
| /// matches. Only a single leaf-command can be designated as a default. | |
| void addSubcommand(Command<T> command, {bool isDefault = false}) { | |
| if (isDefault && command.subcommands.isNotEmpty) { | |
| throw ArgumentError('default command must be a leaf command'); | |
| } | |
| if (isDefault && argParser.defaultCommand != null) { | |
| throw StateError('default command already defined'); | |
| } | |
| var names = [command.name, ...command.aliases]; | |
| for (var name in names) { | |
| _subcommands[name] = command; | |
| argParser.addCommand(name, command.argParser); | |
| } | |
| if (isDefault) { | |
| argParser.defaultCommand = command.name; | |
| } | |
| command._parent = this; | |
| } | |
| /// Prints the usage information for this command. | |
| /// | |
| /// This is called internally by [run] and can be overridden by subclasses to | |
| /// control how output is displayed or integrate with a logging system. | |
| void printUsage() => print(usage); | |
| /// Throws a [UsageException] with [message]. | |
| Never usageException(String message) => | |
| throw UsageException(_wrap(message), _usageWithoutDescription); | |
| } | |
| /// Returns a string representation of [commands] fit for use in a usage string. | |
| /// | |
| /// [isSubcommand] indicates whether the commands should be called "commands" or | |
| /// "subcommands". | |
| /// | |
| /// [defaultCommand] indicate which command (if any) is designated as default. | |
| String _getCommandUsage(Map<String, Command> commands, | |
| {bool isSubcommand = false, int? lineLength, String? defaultCommand}) { | |
| // Don't include aliases. | |
| var names = | |
| commands.keys.where((name) => !commands[name]!.aliases.contains(name)); | |
| // Filter out hidden ones, unless they are all hidden. | |
| var visible = names.where((name) => !commands[name]!.hidden); | |
| if (visible.isNotEmpty) names = visible; | |
| // Show names in the order they were first added | |
| names = names.toList(); | |
| // Group the commands by category. | |
| var commandsByCategory = SplayTreeMap<String, List<Command>>(); | |
| for (var name in names) { | |
| var category = commands[name]!.category; | |
| commandsByCategory.putIfAbsent(category, () => []).add(commands[name]!); | |
| } | |
| final categories = commandsByCategory.keys.toList(); | |
| var length = names.map((name) => name.length).reduce(math.max); | |
| var buffer = StringBuffer('Available ${isSubcommand ? "sub" : ""}commands:'); | |
| var columnStart = length + 5; | |
| for (var category in categories) { | |
| if (category != '') { | |
| buffer.writeln(); | |
| buffer.writeln(); | |
| buffer.write(category); | |
| } | |
| for (var command in commandsByCategory[category]!) { | |
| var defaultMarker = defaultCommand == command.name ? '(default) ' : ''; | |
| var lines = wrapTextAsLines(defaultMarker + command.summary, | |
| start: columnStart, length: lineLength); | |
| buffer.writeln(); | |
| buffer.write(' ${padRight(command.name, length)} ${lines.first}'); | |
| for (var line in lines.skip(1)) { | |
| buffer.writeln(); | |
| buffer.write(' ' * columnStart); | |
| buffer.write(line); | |
| } | |
| } | |
| } | |
| if (defaultCommand != null) { | |
| buffer.writeln(); | |
| buffer.writeln(); | |
| buffer.write(wrapText( | |
| 'Default command ($defaultCommand) will be selected if no command' | |
| ' is explicitly specified.', | |
| length: lineLength)); | |
| } | |
| return buffer.toString(); | |
| } | |
| /// Returns the edit distance between `from` and `to`. | |
| // | |
| /// Allows for edits, deletes, substitutions, and swaps all as single cost. | |
| /// | |
| /// See https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance#Optimal_string_alignment_distance | |
| int _editDistance(String from, String to) { | |
| // Add a space in front to mimic indexing by 1 instead of 0. | |
| from = ' $from'; | |
| to = ' $to'; | |
| var distances = [ | |
| for (var i = 0; i < from.length; i++) | |
| [ | |
| for (var j = 0; j < to.length; j++) | |
| if (i == 0) j else if (j == 0) i else 0, | |
| ], | |
| ]; | |
| for (var i = 1; i < from.length; i++) { | |
| for (var j = 1; j < to.length; j++) { | |
| // Removals from `from`. | |
| var min = distances[i - 1][j] + 1; | |
| // Additions to `from`. | |
| min = math.min(min, distances[i][j - 1] + 1); | |
| // Substitutions (and equality). | |
| min = math.min( | |
| min, | |
| distances[i - 1][j - 1] + | |
| // Cost is zero if substitution was not actually necessary. | |
| (from[i] == to[j] ? 0 : 1)); | |
| // Allows for basic swaps, but no additional edits of swapped regions. | |
| if (i > 1 && j > 1 && from[i] == to[j - 1] && from[i - 1] == to[j]) { | |
| min = math.min(min, distances[i - 2][j - 2] + 1); | |
| } | |
| distances[i][j] = min; | |
| } | |
| } | |
| return distances.last.last; | |
| } |