| // Copyright (c) 2015, 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. | |
| part of cli; | |
| // Splits a line into a list of string args. Each arg retains any | |
| // trailing whitespace so that we can reconstruct the original command | |
| // line from the pieces. | |
| List<String> _splitLine(String line) { | |
| line = line.trimLeft(); | |
| var args = <String>[]; | |
| int pos = 0; | |
| while (pos < line.length) { | |
| int startPos = pos; | |
| // Advance to end of word. | |
| for (; pos < line.length && line[pos] != ' '; pos++) ; | |
| // Advance to end of spaces. | |
| for (; pos < line.length && line[pos] == ' '; pos++) ; | |
| args.add(line.substring(startPos, pos)); | |
| } | |
| return args; | |
| } | |
| // Concatenates the first 'count' args. | |
| String _concatArgs(List<String> args, int count) { | |
| if (count == 0) { | |
| return ''; | |
| } | |
| return '${args.sublist(0, count).join('')}'; | |
| } | |
| // Shared functionality for RootCommand and Command. | |
| abstract class _CommandBase { | |
| _CommandBase(List<Command> children) { | |
| _children.addAll(children); | |
| for (var child in _children) { | |
| child._parent = this; | |
| } | |
| } | |
| // A command may optionally have sub-commands. | |
| List<Command> _children = <Command>[]; | |
| _CommandBase? _parent; | |
| int get _depth => (_parent == null ? 0 : _parent!._depth + 1); | |
| // Override in subclasses to provide command-specific argument completion. | |
| // | |
| // Given a list of arguments to this command, provide a list of | |
| // possible completions for those arguments. | |
| Future<List<String>> complete(List<String> args) => | |
| new Future.value(<String>[]); | |
| // Override in subclasses to provide command-specific execution. | |
| Future run(List<String> args); | |
| // Returns a list of local subcommands which match the args. | |
| List<Command> _matchLocal(String argWithSpace, bool preferExact) { | |
| var matches = <Command>[]; | |
| var arg = argWithSpace.trimRight(); | |
| for (var child in _children) { | |
| if (child.name.startsWith(arg)) { | |
| if (preferExact && ((child.name == arg) || (child.alias == arg))) { | |
| return [child]; | |
| } | |
| matches.add(child); | |
| } | |
| } | |
| return matches; | |
| } | |
| // Returns the set of commands could be triggered by a list of | |
| // arguments. | |
| List<Command> _match(List<String> args, bool preferExact) { | |
| if (args.isEmpty) { | |
| return <Command>[]; | |
| } | |
| bool lastArg = (args.length == 1); | |
| var matches = _matchLocal(args[0], !lastArg || preferExact); | |
| if (matches.isEmpty) { | |
| return <Command>[]; | |
| } else if (matches.length == 1) { | |
| var childMatches = matches[0]._match(args.sublist(1), preferExact); | |
| if (childMatches.isEmpty) { | |
| return matches; | |
| } else { | |
| return childMatches; | |
| } | |
| } else { | |
| return matches; | |
| } | |
| } | |
| // Builds a list of completions for this command. | |
| Future<List<String>> _buildCompletions( | |
| List<String> args, | |
| bool addEmptyString, | |
| ) { | |
| return complete(args.sublist(_depth, args.length)).then((completions) { | |
| if (addEmptyString && | |
| completions.isEmpty && | |
| args[args.length - 1] == '') { | |
| // Special case allowance for an empty particle at the end of | |
| // the command. | |
| completions = <String>['']; | |
| } | |
| var prefix = _concatArgs(args, _depth); | |
| return completions.map((str) => '${prefix}${str}').toList(); | |
| }); | |
| } | |
| } | |
| // The root of a tree of commands. | |
| class RootCommand extends _CommandBase { | |
| RootCommand(List<Command> children, [List<String>? history]) | |
| : this._(children, history ?? ['']); | |
| RootCommand._(List<Command> children, List<String> history) | |
| : history = history, | |
| historyPos = history.length - 1, | |
| super(children); | |
| // Provides a list of possible completions for a line of text. | |
| Future<List<String>> completeCommand(String line) { | |
| var args = _splitLine(line); | |
| bool showAll = line.endsWith(' ') || args.isEmpty; | |
| if (showAll) { | |
| // Adding an empty string to the end causes us to match all | |
| // subcommands of the last command. | |
| args.add(''); | |
| } | |
| var commands = _match(args, false); | |
| if (commands.isEmpty) { | |
| // No matching commands. | |
| return new Future.value(<String>[]); | |
| } | |
| int matchLen = commands[0]._depth; | |
| if (matchLen < args.length) { | |
| // We were able to find a command which matches a prefix of the | |
| // args, but not the full list. | |
| if (commands.length == 1) { | |
| // The matching command is unique. Attempt to provide local | |
| // argument completion from the command. | |
| return commands[0]._buildCompletions(args, true); | |
| } else { | |
| // An ambiguous prefix match leaves us nowhere. The user is | |
| // typing a bunch of stuff that we don't know how to complete. | |
| return new Future.value(<String>[]); | |
| } | |
| } | |
| // We have found a set of commands which match all of the args. | |
| // Return the completion strings. | |
| var prefix = _concatArgs(args, args.length - 1); | |
| var completions = commands | |
| .map((command) => '${prefix}${command.name} ') | |
| .toList(); | |
| if (matchLen == args.length) { | |
| // If we are showing all possibilities, also include local | |
| // completions for the parent command. | |
| return commands[0]._parent!._buildCompletions(args, false).then(( | |
| localCompletions, | |
| ) { | |
| completions.addAll(localCompletions); | |
| return completions; | |
| }); | |
| } | |
| return new Future.value(completions); | |
| } | |
| // Runs a command. | |
| Future runCommand(String line) { | |
| _historyAdvance(line); | |
| var args = _splitLine(line); | |
| var commands = _match(args, true); | |
| if (commands.isEmpty) { | |
| return new Future.error(new NoSuchCommandException(line)); | |
| } else if (commands.length == 1) { | |
| return commands[0].run(args.sublist(commands[0]._depth)); | |
| } else { | |
| return new Future.error(new AmbiguousCommandException(line, commands)); | |
| } | |
| } | |
| // Find all matching commands. Useful for implementing help systems. | |
| List<Command> matchCommand(List<String> args, bool preferExact) { | |
| if (args.isEmpty) { | |
| // Adding an empty string to the end causes us to match all | |
| // subcommands of the last command. | |
| args.add(''); | |
| } | |
| return _match(args, preferExact); | |
| } | |
| // Command line history always contains one slot to hold the current | |
| // line, so we start off with one entry. | |
| List<String> history; | |
| int historyPos; | |
| String historyPrev(String line) { | |
| if (historyPos == 0) { | |
| return line; | |
| } | |
| history[historyPos] = line; | |
| historyPos--; | |
| return history[historyPos]; | |
| } | |
| String historyNext(String line) { | |
| if (historyPos == history.length - 1) { | |
| return line; | |
| } | |
| history[historyPos] = line; | |
| historyPos++; | |
| return history[historyPos]; | |
| } | |
| void _historyAdvance(String line) { | |
| // Replace the last history line. | |
| historyPos = history.length - 1; | |
| history[historyPos] = line; | |
| // Create an empty spot for the next line. | |
| history.add(''); | |
| historyPos++; | |
| } | |
| Future run(List<String> args) { | |
| throw 'should-not-execute-the-root-command'; | |
| } | |
| String toString() => 'RootCommand'; | |
| } | |
| // A node in the command tree. | |
| abstract class Command extends _CommandBase { | |
| Command(this.name, List<Command> children) : super(children); | |
| final String name; | |
| String? alias; | |
| String get fullName { | |
| if (_parent is RootCommand) { | |
| return name; | |
| } else { | |
| Command parent = _parent as Command; | |
| return '${parent.fullName} $name'; | |
| } | |
| } | |
| String toString() => 'Command(${name})'; | |
| } | |
| abstract class CommandException implements Exception {} | |
| class AmbiguousCommandException extends CommandException { | |
| AmbiguousCommandException(this.command, this.matches); | |
| final String command; | |
| final List<Command> matches; | |
| @override | |
| String toString() { | |
| List<String> matchNames = matches | |
| .map((Command command) => '${command.fullName}') | |
| .toList(); | |
| return "Command '$command' is ambiguous: $matchNames"; | |
| } | |
| } | |
| class NoSuchCommandException extends CommandException { | |
| NoSuchCommandException(this.command); | |
| final String command; | |
| @override | |
| String toString() => "No such command: '$command'"; | |
| } |