mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-07-01 18:38:16 +08:00
Move Spectre.Console.Cli to it's own package
This commit is contained in:

committed by
Patrik Svensson

parent
b600832e00
commit
36ca22ffac
34
src/Spectre.Console.Cli/Internal/Parsing/CommandTree.cs
Normal file
34
src/Spectre.Console.Cli/Internal/Parsing/CommandTree.cs
Normal file
@ -0,0 +1,34 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
internal sealed class CommandTree
|
||||
{
|
||||
public CommandInfo Command { get; }
|
||||
public List<MappedCommandParameter> Mapped { get; }
|
||||
public List<CommandParameter> Unmapped { get; }
|
||||
public CommandTree? Parent { get; }
|
||||
public CommandTree? Next { get; set; }
|
||||
public bool ShowHelp { get; set; }
|
||||
|
||||
public CommandTree(CommandTree? parent, CommandInfo command)
|
||||
{
|
||||
Parent = parent;
|
||||
Command = command;
|
||||
Mapped = new List<MappedCommandParameter>();
|
||||
Unmapped = new List<CommandParameter>();
|
||||
}
|
||||
|
||||
public ICommand CreateCommand(ITypeResolver resolver)
|
||||
{
|
||||
if (Command.Delegate != null)
|
||||
{
|
||||
return new DelegateCommand(Command.Delegate);
|
||||
}
|
||||
|
||||
if (resolver.Resolve(Command.CommandType) is ICommand command)
|
||||
{
|
||||
return command;
|
||||
}
|
||||
|
||||
throw CommandParseException.CouldNotCreateCommand(Command.CommandType);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
internal static class CommandTreeExtensions
|
||||
{
|
||||
public static CommandTree? GetRootCommand(this CommandTree node)
|
||||
{
|
||||
while (node.Parent != null)
|
||||
{
|
||||
node = node.Parent;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public static CommandTree GetLeafCommand(this CommandTree node)
|
||||
{
|
||||
while (node.Next != null)
|
||||
{
|
||||
node = node.Next;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public static bool HasArguments(this CommandTree tree)
|
||||
{
|
||||
return tree.Command.Parameters.OfType<CommandArgument>().Any();
|
||||
}
|
||||
|
||||
public static CommandArgument? FindArgument(this CommandTree tree, int position)
|
||||
{
|
||||
return tree.Command.Parameters
|
||||
.OfType<CommandArgument>()
|
||||
.FirstOrDefault(c => c.Position == position);
|
||||
}
|
||||
|
||||
public static CommandOption? FindOption(this CommandTree tree, string name, bool longOption, CaseSensitivity sensitivity)
|
||||
{
|
||||
return tree.Command.Parameters
|
||||
.OfType<CommandOption>()
|
||||
.FirstOrDefault(o => longOption
|
||||
? o.LongNames.Contains(name, sensitivity.GetStringComparer(CommandPart.LongOption))
|
||||
: o.ShortNames.Contains(name, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
public static bool IsOptionMappedWithParent(this CommandTree tree, string name, bool longOption)
|
||||
{
|
||||
var node = tree.Parent;
|
||||
while (node != null)
|
||||
{
|
||||
var option = node.Command?.Parameters.OfType<CommandOption>()
|
||||
.FirstOrDefault(o => longOption
|
||||
? o.LongNames.Contains(name, StringComparer.Ordinal)
|
||||
: o.ShortNames.Contains(name, StringComparer.Ordinal));
|
||||
|
||||
if (option != null)
|
||||
{
|
||||
return node.Mapped.Any(p => p.Parameter == option);
|
||||
}
|
||||
|
||||
node = node.Parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
383
src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs
Normal file
383
src/Spectre.Console.Cli/Internal/Parsing/CommandTreeParser.cs
Normal file
@ -0,0 +1,383 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
internal class CommandTreeParser
|
||||
{
|
||||
private readonly CommandModel _configuration;
|
||||
private readonly ParsingMode _parsingMode;
|
||||
private readonly CommandOptionAttribute _help;
|
||||
|
||||
public CaseSensitivity CaseSensitivity { get; }
|
||||
|
||||
public enum State
|
||||
{
|
||||
Normal = 0,
|
||||
Remaining = 1,
|
||||
}
|
||||
|
||||
public CommandTreeParser(CommandModel configuration, ICommandAppSettings settings, ParsingMode? parsingMode = null)
|
||||
{
|
||||
if (settings is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(settings));
|
||||
}
|
||||
|
||||
_configuration = configuration;
|
||||
_parsingMode = parsingMode ?? _configuration.ParsingMode;
|
||||
_help = new CommandOptionAttribute("-h|--help");
|
||||
|
||||
CaseSensitivity = settings.CaseSensitivity;
|
||||
}
|
||||
|
||||
public CommandTreeParserResult Parse(IEnumerable<string> args)
|
||||
{
|
||||
var context = new CommandTreeParserContext(args, _parsingMode);
|
||||
|
||||
var tokenizerResult = CommandTreeTokenizer.Tokenize(context.Arguments);
|
||||
var tokens = tokenizerResult.Tokens;
|
||||
var rawRemaining = tokenizerResult.Remaining;
|
||||
|
||||
var result = default(CommandTree);
|
||||
if (tokens.Count > 0)
|
||||
{
|
||||
// Not a command?
|
||||
var token = tokens.Current;
|
||||
if (token == null)
|
||||
{
|
||||
// Should not happen, but the compiler isn't
|
||||
// smart enough to realize this...
|
||||
throw new CommandRuntimeException("Could not get current token.");
|
||||
}
|
||||
|
||||
if (token.TokenKind != CommandTreeToken.Kind.String)
|
||||
{
|
||||
// Got a default command?
|
||||
if (_configuration.DefaultCommand != null)
|
||||
{
|
||||
result = ParseCommandParameters(context, _configuration.DefaultCommand, null, tokens);
|
||||
return new CommandTreeParserResult(
|
||||
result, new RemainingArguments(context.GetRemainingArguments(), rawRemaining));
|
||||
}
|
||||
|
||||
// Show help?
|
||||
if (_help?.IsMatch(token.Value) == true)
|
||||
{
|
||||
return new CommandTreeParserResult(
|
||||
null, new RemainingArguments(context.GetRemainingArguments(), rawRemaining));
|
||||
}
|
||||
|
||||
// Unexpected option.
|
||||
throw CommandParseException.UnexpectedOption(context.Arguments, token);
|
||||
}
|
||||
|
||||
// Does the token value match a command?
|
||||
var command = _configuration.FindCommand(token.Value, CaseSensitivity);
|
||||
if (command == null)
|
||||
{
|
||||
if (_configuration.DefaultCommand != null)
|
||||
{
|
||||
result = ParseCommandParameters(context, _configuration.DefaultCommand, null, tokens);
|
||||
return new CommandTreeParserResult(
|
||||
result, new RemainingArguments(context.GetRemainingArguments(), rawRemaining));
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the command.
|
||||
result = ParseCommand(context, _configuration, null, tokens);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Is there a default command?
|
||||
if (_configuration.DefaultCommand != null)
|
||||
{
|
||||
result = ParseCommandParameters(context, _configuration.DefaultCommand, null, tokens);
|
||||
}
|
||||
}
|
||||
|
||||
return new CommandTreeParserResult(
|
||||
result, new RemainingArguments(context.GetRemainingArguments(), rawRemaining));
|
||||
}
|
||||
|
||||
private CommandTree ParseCommand(
|
||||
CommandTreeParserContext context,
|
||||
ICommandContainer current,
|
||||
CommandTree? parent,
|
||||
CommandTreeTokenStream stream)
|
||||
{
|
||||
// Find the command.
|
||||
var commandToken = stream.Consume(CommandTreeToken.Kind.String);
|
||||
if (commandToken == null)
|
||||
{
|
||||
throw new CommandRuntimeException("Could not consume token when parsing command.");
|
||||
}
|
||||
|
||||
var command = current.FindCommand(commandToken.Value, CaseSensitivity);
|
||||
if (command == null)
|
||||
{
|
||||
throw CommandParseException.UnknownCommand(_configuration, parent, context.Arguments, commandToken);
|
||||
}
|
||||
|
||||
return ParseCommandParameters(context, command, parent, stream);
|
||||
}
|
||||
|
||||
private CommandTree ParseCommandParameters(
|
||||
CommandTreeParserContext context,
|
||||
CommandInfo command,
|
||||
CommandTree? parent,
|
||||
CommandTreeTokenStream stream)
|
||||
{
|
||||
context.ResetArgumentPosition();
|
||||
|
||||
var node = new CommandTree(parent, command);
|
||||
while (stream.Peek() != null)
|
||||
{
|
||||
var token = stream.Peek();
|
||||
if (token == null)
|
||||
{
|
||||
// Should not happen, but the compiler isn't
|
||||
// smart enough to realize this...
|
||||
throw new CommandRuntimeException("Could not get the next token.");
|
||||
}
|
||||
|
||||
switch (token.TokenKind)
|
||||
{
|
||||
case CommandTreeToken.Kind.LongOption:
|
||||
// Long option
|
||||
ParseOption(context, stream, token, node, true);
|
||||
break;
|
||||
case CommandTreeToken.Kind.ShortOption:
|
||||
// Short option
|
||||
ParseOption(context, stream, token, node, false);
|
||||
break;
|
||||
case CommandTreeToken.Kind.String:
|
||||
// Command
|
||||
ParseString(context, stream, node);
|
||||
break;
|
||||
case CommandTreeToken.Kind.Remaining:
|
||||
// Remaining
|
||||
stream.Consume(CommandTreeToken.Kind.Remaining);
|
||||
context.State = State.Remaining;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Encountered unknown token ({token.TokenKind}).");
|
||||
}
|
||||
}
|
||||
|
||||
// Add unmapped parameters.
|
||||
foreach (var parameter in node.Command.Parameters)
|
||||
{
|
||||
if (node.Mapped.All(m => m.Parameter != parameter))
|
||||
{
|
||||
node.Unmapped.Add(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private void ParseString(
|
||||
CommandTreeParserContext context,
|
||||
CommandTreeTokenStream stream,
|
||||
CommandTree node)
|
||||
{
|
||||
if (context.State == State.Remaining)
|
||||
{
|
||||
stream.Consume(CommandTreeToken.Kind.String);
|
||||
return;
|
||||
}
|
||||
|
||||
var token = stream.Expect(CommandTreeToken.Kind.String);
|
||||
|
||||
// Command?
|
||||
var command = node.Command.FindCommand(token.Value, CaseSensitivity);
|
||||
if (command != null)
|
||||
{
|
||||
if (context.State == State.Normal)
|
||||
{
|
||||
node.Next = ParseCommand(context, node.Command, node, stream);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Current command has no arguments?
|
||||
if (!node.HasArguments())
|
||||
{
|
||||
throw CommandParseException.UnknownCommand(_configuration, node, context.Arguments, token);
|
||||
}
|
||||
|
||||
// Argument?
|
||||
var parameter = node.FindArgument(context.CurrentArgumentPosition);
|
||||
if (parameter == null)
|
||||
{
|
||||
// No parameters left. Any commands after this?
|
||||
if (node.Command.Children.Count > 0 || node.Command.IsDefaultCommand)
|
||||
{
|
||||
throw CommandParseException.UnknownCommand(_configuration, node, context.Arguments, token);
|
||||
}
|
||||
|
||||
throw CommandParseException.CouldNotMatchArgument(context.Arguments, token);
|
||||
}
|
||||
|
||||
// Yes, this was an argument.
|
||||
if (parameter.ParameterKind == ParameterKind.Vector)
|
||||
{
|
||||
// Vector
|
||||
var current = stream.Current;
|
||||
while (current?.TokenKind == CommandTreeToken.Kind.String)
|
||||
{
|
||||
var value = stream.Consume(CommandTreeToken.Kind.String)?.Value;
|
||||
node.Mapped.Add(new MappedCommandParameter(parameter, value));
|
||||
current = stream.Current;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scalar
|
||||
var value = stream.Consume(CommandTreeToken.Kind.String)?.Value;
|
||||
node.Mapped.Add(new MappedCommandParameter(parameter, value));
|
||||
context.IncreaseArgumentPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseOption(
|
||||
CommandTreeParserContext context,
|
||||
CommandTreeTokenStream stream,
|
||||
CommandTreeToken token,
|
||||
CommandTree node,
|
||||
bool isLongOption)
|
||||
{
|
||||
// Consume the option token.
|
||||
stream.Consume(isLongOption ? CommandTreeToken.Kind.LongOption : CommandTreeToken.Kind.ShortOption);
|
||||
|
||||
if (context.State == State.Normal)
|
||||
{
|
||||
// Find the option.
|
||||
var option = node.FindOption(token.Value, isLongOption, CaseSensitivity);
|
||||
if (option != null)
|
||||
{
|
||||
node.Mapped.Add(new MappedCommandParameter(
|
||||
option, ParseOptionValue(context, stream, token, node, option)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Help?
|
||||
if (_help?.IsMatch(token.Value) == true)
|
||||
{
|
||||
node.ShowHelp = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (context.State == State.Remaining)
|
||||
{
|
||||
ParseOptionValue(context, stream, token, node, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.ParsingMode == ParsingMode.Strict)
|
||||
{
|
||||
throw CommandParseException.UnknownOption(context.Arguments, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
ParseOptionValue(context, stream, token, node, null);
|
||||
}
|
||||
}
|
||||
|
||||
private string? ParseOptionValue(
|
||||
CommandTreeParserContext context,
|
||||
CommandTreeTokenStream stream,
|
||||
CommandTreeToken token,
|
||||
CommandTree current,
|
||||
CommandParameter? parameter)
|
||||
{
|
||||
var value = default(string);
|
||||
|
||||
// Parse the value of the token (if any).
|
||||
var valueToken = stream.Peek();
|
||||
if (valueToken?.TokenKind == CommandTreeToken.Kind.String)
|
||||
{
|
||||
var parseValue = true;
|
||||
if (token.TokenKind == CommandTreeToken.Kind.ShortOption && token.IsGrouped)
|
||||
{
|
||||
parseValue = false;
|
||||
}
|
||||
|
||||
if (context.State == State.Normal && parseValue)
|
||||
{
|
||||
// Is this a command?
|
||||
if (current.Command.FindCommand(valueToken.Value, CaseSensitivity) == null)
|
||||
{
|
||||
if (parameter != null)
|
||||
{
|
||||
if (parameter.ParameterKind == ParameterKind.Flag)
|
||||
{
|
||||
if (!CliConstants.AcceptedBooleanValues.Contains(valueToken.Value, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
// Flags cannot be assigned a value.
|
||||
throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token);
|
||||
}
|
||||
}
|
||||
|
||||
value = stream.Consume(CommandTreeToken.Kind.String)?.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unknown parameter value.
|
||||
value = stream.Consume(CommandTreeToken.Kind.String)?.Value;
|
||||
|
||||
// In relaxed parsing mode?
|
||||
if (context.ParsingMode == ParsingMode.Relaxed)
|
||||
{
|
||||
context.AddRemainingArgument(token.Value, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
context.AddRemainingArgument(token.Value, parseValue ? valueToken.Value : null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (context.State == State.Remaining || context.ParsingMode == ParsingMode.Relaxed)
|
||||
{
|
||||
context.AddRemainingArgument(token.Value, null);
|
||||
}
|
||||
}
|
||||
|
||||
// No value?
|
||||
if (context.State == State.Normal)
|
||||
{
|
||||
if (value == null && parameter != null)
|
||||
{
|
||||
if (parameter.ParameterKind == ParameterKind.Flag)
|
||||
{
|
||||
value = "true";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (parameter is CommandOption option)
|
||||
{
|
||||
if (parameter.IsFlagValue())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
throw CommandParseException.OptionHasNoValue(context.Arguments, token, option);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This should not happen at all. If it does, it's because we've added a new
|
||||
// option type which isn't a CommandOption for some reason.
|
||||
throw new InvalidOperationException($"Found invalid parameter type '{parameter.GetType().FullName}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
internal class CommandTreeParserContext
|
||||
{
|
||||
private readonly List<string> _args;
|
||||
private readonly Dictionary<string, List<string?>> _remaining;
|
||||
|
||||
public IReadOnlyList<string> Arguments => _args;
|
||||
public int CurrentArgumentPosition { get; private set; }
|
||||
public CommandTreeParser.State State { get; set; }
|
||||
public ParsingMode ParsingMode { get; }
|
||||
|
||||
public CommandTreeParserContext(IEnumerable<string> args, ParsingMode parsingMode)
|
||||
{
|
||||
_args = new List<string>(args);
|
||||
_remaining = new Dictionary<string, List<string?>>(StringComparer.Ordinal);
|
||||
|
||||
ParsingMode = parsingMode;
|
||||
}
|
||||
|
||||
public void ResetArgumentPosition()
|
||||
{
|
||||
CurrentArgumentPosition = 0;
|
||||
}
|
||||
|
||||
public void IncreaseArgumentPosition()
|
||||
{
|
||||
CurrentArgumentPosition++;
|
||||
}
|
||||
|
||||
public void AddRemainingArgument(string key, string? value)
|
||||
{
|
||||
if (State == CommandTreeParser.State.Remaining || ParsingMode == ParsingMode.Relaxed)
|
||||
{
|
||||
if (!_remaining.ContainsKey(key))
|
||||
{
|
||||
_remaining.Add(key, new List<string?>());
|
||||
}
|
||||
|
||||
_remaining[key].Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "Bug in analyzer?")]
|
||||
public ILookup<string, string?> GetRemainingArguments()
|
||||
{
|
||||
return _remaining
|
||||
.SelectMany(pair => pair.Value, (pair, value) => new { pair.Key, value })
|
||||
.ToLookup(pair => pair.Key, pair => (string?)pair.value);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
// Consider removing this in favor for value tuples at some point.
|
||||
internal sealed class CommandTreeParserResult
|
||||
{
|
||||
public CommandTree? Tree { get; }
|
||||
public IRemainingArguments Remaining { get; }
|
||||
|
||||
public CommandTreeParserResult(CommandTree? tree, IRemainingArguments remaining)
|
||||
{
|
||||
Tree = tree;
|
||||
Remaining = remaining;
|
||||
}
|
||||
}
|
26
src/Spectre.Console.Cli/Internal/Parsing/CommandTreeToken.cs
Normal file
26
src/Spectre.Console.Cli/Internal/Parsing/CommandTreeToken.cs
Normal file
@ -0,0 +1,26 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
internal sealed class CommandTreeToken
|
||||
{
|
||||
public Kind TokenKind { get; }
|
||||
public int Position { get; }
|
||||
public string Value { get; }
|
||||
public string Representation { get; }
|
||||
public bool IsGrouped { get; set; }
|
||||
|
||||
public enum Kind
|
||||
{
|
||||
String,
|
||||
LongOption,
|
||||
ShortOption,
|
||||
Remaining,
|
||||
}
|
||||
|
||||
public CommandTreeToken(Kind kind, int position, string value, string representation)
|
||||
{
|
||||
TokenKind = kind;
|
||||
Position = position;
|
||||
Value = value;
|
||||
Representation = representation;
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
internal sealed class CommandTreeTokenStream : IReadOnlyList<CommandTreeToken>
|
||||
{
|
||||
private readonly List<CommandTreeToken> _tokens;
|
||||
private int _position;
|
||||
|
||||
public int Count => _tokens.Count;
|
||||
|
||||
public CommandTreeToken this[int index] => _tokens[index];
|
||||
|
||||
public CommandTreeToken? Current
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_position >= Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _tokens[_position];
|
||||
}
|
||||
}
|
||||
|
||||
public CommandTreeTokenStream(IEnumerable<CommandTreeToken> tokens)
|
||||
{
|
||||
_tokens = new List<CommandTreeToken>(tokens ?? Enumerable.Empty<CommandTreeToken>());
|
||||
_position = 0;
|
||||
}
|
||||
|
||||
public CommandTreeToken? Peek(int index = 0)
|
||||
{
|
||||
var position = _position + index;
|
||||
if (position >= Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _tokens[position];
|
||||
}
|
||||
|
||||
public CommandTreeToken? Consume()
|
||||
{
|
||||
if (_position >= Count)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var token = _tokens[_position];
|
||||
_position++;
|
||||
return token;
|
||||
}
|
||||
|
||||
public CommandTreeToken? Consume(CommandTreeToken.Kind type)
|
||||
{
|
||||
Expect(type);
|
||||
return Consume();
|
||||
}
|
||||
|
||||
public CommandTreeToken Expect(CommandTreeToken.Kind expected)
|
||||
{
|
||||
if (Current == null)
|
||||
{
|
||||
throw CommandParseException.ExpectedTokenButFoundNull(expected);
|
||||
}
|
||||
|
||||
var found = Current.TokenKind;
|
||||
if (expected != found)
|
||||
{
|
||||
throw CommandParseException.ExpectedTokenButFoundOther(expected, found);
|
||||
}
|
||||
|
||||
return Current;
|
||||
}
|
||||
|
||||
public IEnumerator<CommandTreeToken> GetEnumerator()
|
||||
{
|
||||
return _tokens.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
299
src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs
Normal file
299
src/Spectre.Console.Cli/Internal/Parsing/CommandTreeTokenizer.cs
Normal file
@ -0,0 +1,299 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
internal static class CommandTreeTokenizer
|
||||
{
|
||||
public enum Mode
|
||||
{
|
||||
Normal = 0,
|
||||
Remaining = 1,
|
||||
}
|
||||
|
||||
// Consider removing this in favor for value tuples at some point.
|
||||
public sealed class CommandTreeTokenizerResult
|
||||
{
|
||||
public CommandTreeTokenStream Tokens { get; }
|
||||
public IReadOnlyList<string> Remaining { get; }
|
||||
|
||||
public CommandTreeTokenizerResult(CommandTreeTokenStream tokens, IReadOnlyList<string> remaining)
|
||||
{
|
||||
Tokens = tokens;
|
||||
Remaining = remaining;
|
||||
}
|
||||
}
|
||||
|
||||
public static CommandTreeTokenizerResult Tokenize(IEnumerable<string> args)
|
||||
{
|
||||
var tokens = new List<CommandTreeToken>();
|
||||
var position = 0;
|
||||
var previousReader = default(TextBuffer);
|
||||
var context = new CommandTreeTokenizerContext();
|
||||
|
||||
foreach (var arg in args)
|
||||
{
|
||||
var start = position;
|
||||
var reader = new TextBuffer(previousReader, arg);
|
||||
|
||||
// Parse the token.
|
||||
position = ParseToken(context, reader, position, start, tokens);
|
||||
context.FlushRemaining();
|
||||
|
||||
previousReader = reader;
|
||||
}
|
||||
|
||||
previousReader?.Dispose();
|
||||
|
||||
return new CommandTreeTokenizerResult(
|
||||
new CommandTreeTokenStream(tokens),
|
||||
context.Remaining);
|
||||
}
|
||||
|
||||
private static int ParseToken(CommandTreeTokenizerContext context, TextBuffer reader, int position, int start, List<CommandTreeToken> tokens)
|
||||
{
|
||||
while (reader.Peek() != -1)
|
||||
{
|
||||
if (reader.ReachedEnd)
|
||||
{
|
||||
position += reader.Position - start;
|
||||
break;
|
||||
}
|
||||
|
||||
var character = reader.Peek();
|
||||
|
||||
// Eat whitespace
|
||||
if (char.IsWhiteSpace(character))
|
||||
{
|
||||
reader.Consume();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (character == '-')
|
||||
{
|
||||
// Option
|
||||
tokens.AddRange(ScanOptions(context, reader));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Command or argument
|
||||
tokens.Add(ScanString(context, reader));
|
||||
}
|
||||
|
||||
// Flush remaining tokens
|
||||
context.FlushRemaining();
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
private static CommandTreeToken ScanString(
|
||||
CommandTreeTokenizerContext context,
|
||||
TextBuffer reader,
|
||||
char[]? stop = null)
|
||||
{
|
||||
if (reader.TryPeek(out var character))
|
||||
{
|
||||
// Is this a quoted string?
|
||||
if (character == '\"')
|
||||
{
|
||||
return ScanQuotedString(context, reader);
|
||||
}
|
||||
}
|
||||
|
||||
var position = reader.Position;
|
||||
var builder = new StringBuilder();
|
||||
while (!reader.ReachedEnd)
|
||||
{
|
||||
var current = reader.Peek();
|
||||
if (stop?.Contains(current) ?? false)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
reader.Read(); // Consume
|
||||
context.AddRemaining(current);
|
||||
builder.Append(current);
|
||||
}
|
||||
|
||||
var value = builder.ToString();
|
||||
return new CommandTreeToken(CommandTreeToken.Kind.String, position, value.Trim(), value);
|
||||
}
|
||||
|
||||
private static CommandTreeToken ScanQuotedString(CommandTreeTokenizerContext context, TextBuffer reader)
|
||||
{
|
||||
var position = reader.Position;
|
||||
|
||||
context.FlushRemaining();
|
||||
reader.Consume('\"');
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var terminated = false;
|
||||
while (!reader.ReachedEnd)
|
||||
{
|
||||
var character = reader.Peek();
|
||||
if (character == '\"')
|
||||
{
|
||||
terminated = true;
|
||||
reader.Read();
|
||||
break;
|
||||
}
|
||||
|
||||
builder.Append(reader.Read());
|
||||
}
|
||||
|
||||
if (!terminated)
|
||||
{
|
||||
var unterminatedQuote = builder.ToString();
|
||||
var token = new CommandTreeToken(CommandTreeToken.Kind.String, position, unterminatedQuote, $"\"{unterminatedQuote}");
|
||||
throw CommandParseException.UnterminatedQuote(reader.Original, token);
|
||||
}
|
||||
|
||||
var quotedString = builder.ToString();
|
||||
|
||||
// Add to the context
|
||||
context.AddRemaining(quotedString);
|
||||
|
||||
return new CommandTreeToken(
|
||||
CommandTreeToken.Kind.String,
|
||||
position, quotedString,
|
||||
quotedString);
|
||||
}
|
||||
|
||||
private static IEnumerable<CommandTreeToken> ScanOptions(CommandTreeTokenizerContext context, TextBuffer reader)
|
||||
{
|
||||
var result = new List<CommandTreeToken>();
|
||||
|
||||
var position = reader.Position;
|
||||
|
||||
reader.Consume('-');
|
||||
context.AddRemaining('-');
|
||||
|
||||
if (!reader.TryPeek(out var character))
|
||||
{
|
||||
var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, "-", "-");
|
||||
throw CommandParseException.OptionHasNoName(reader.Original, token);
|
||||
}
|
||||
|
||||
switch (character)
|
||||
{
|
||||
case '-':
|
||||
var option = ScanLongOption(context, reader, position);
|
||||
if (option != null)
|
||||
{
|
||||
result.Add(option);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
result.AddRange(ScanShortOptions(context, reader, position));
|
||||
break;
|
||||
}
|
||||
|
||||
if (reader.TryPeek(out character))
|
||||
{
|
||||
// Encountered a separator?
|
||||
if (character == '=' || character == ':')
|
||||
{
|
||||
reader.Read(); // Consume
|
||||
context.AddRemaining(character);
|
||||
|
||||
if (!reader.TryPeek(out _))
|
||||
{
|
||||
var token = new CommandTreeToken(CommandTreeToken.Kind.String, reader.Position, "=", "=");
|
||||
throw CommandParseException.OptionValueWasExpected(reader.Original, token);
|
||||
}
|
||||
|
||||
result.Add(ScanString(context, reader));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IEnumerable<CommandTreeToken> ScanShortOptions(CommandTreeTokenizerContext context, TextBuffer reader, int position)
|
||||
{
|
||||
var result = new List<CommandTreeToken>();
|
||||
while (!reader.ReachedEnd)
|
||||
{
|
||||
var current = reader.Peek();
|
||||
if (char.IsWhiteSpace(current))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Encountered a separator?
|
||||
if (current == '=' || current == ':')
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (char.IsLetter(current))
|
||||
{
|
||||
context.AddRemaining(current);
|
||||
reader.Read(); // Consume
|
||||
|
||||
var value = current.ToString(CultureInfo.InvariantCulture);
|
||||
result.Add(result.Count == 0
|
||||
? new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, value, $"-{value}")
|
||||
: new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position + result.Count, value, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create a token representing the short option.
|
||||
var tokenPosition = position + 1 + result.Count;
|
||||
var represntation = current.ToString(CultureInfo.InvariantCulture);
|
||||
var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, tokenPosition, represntation, represntation);
|
||||
throw CommandParseException.InvalidShortOptionName(reader.Original, token);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.Count > 1)
|
||||
{
|
||||
foreach (var item in result)
|
||||
{
|
||||
item.IsGrouped = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static CommandTreeToken ScanLongOption(CommandTreeTokenizerContext context, TextBuffer reader, int position)
|
||||
{
|
||||
reader.Consume('-');
|
||||
context.AddRemaining('-');
|
||||
|
||||
if (reader.ReachedEnd)
|
||||
{
|
||||
// Rest of the arguments are remaining ones.
|
||||
context.Mode = Mode.Remaining;
|
||||
return new CommandTreeToken(CommandTreeToken.Kind.Remaining, position, "--", "--");
|
||||
}
|
||||
|
||||
var name = ScanString(context, reader, new[] { '=', ':' });
|
||||
|
||||
// Perform validation of the name.
|
||||
if (name.Value.Length == 0)
|
||||
{
|
||||
throw CommandParseException.LongOptionNameIsMissing(reader, position);
|
||||
}
|
||||
|
||||
if (name.Value.Length == 1)
|
||||
{
|
||||
throw CommandParseException.LongOptionNameIsOneCharacter(reader, position, name.Value);
|
||||
}
|
||||
|
||||
if (char.IsDigit(name.Value[0]))
|
||||
{
|
||||
throw CommandParseException.LongOptionNameStartWithDigit(reader, position, name.Value);
|
||||
}
|
||||
|
||||
for (var index = 0; index < name.Value.Length; index++)
|
||||
{
|
||||
if (!char.IsLetterOrDigit(name.Value[index]) && name.Value[index] != '-' && name.Value[index] != '_')
|
||||
{
|
||||
throw CommandParseException.LongOptionNameContainSymbol(reader, position + 2 + index, name.Value[index]);
|
||||
}
|
||||
}
|
||||
|
||||
return new CommandTreeToken(CommandTreeToken.Kind.LongOption, position, name.Value, $"--{name.Value}");
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
internal sealed class CommandTreeTokenizerContext
|
||||
{
|
||||
private readonly StringBuilder _builder;
|
||||
private readonly List<string> _remaining;
|
||||
|
||||
public CommandTreeTokenizer.Mode Mode { get; set; }
|
||||
public IReadOnlyList<string> Remaining => _remaining;
|
||||
|
||||
public CommandTreeTokenizerContext()
|
||||
{
|
||||
_builder = new StringBuilder();
|
||||
_remaining = new List<string>();
|
||||
}
|
||||
|
||||
public void AddRemaining(char character)
|
||||
{
|
||||
if (Mode == CommandTreeTokenizer.Mode.Remaining)
|
||||
{
|
||||
_builder.Append(character);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddRemaining(string text)
|
||||
{
|
||||
if (Mode == CommandTreeTokenizer.Mode.Remaining)
|
||||
{
|
||||
_builder.Append(text);
|
||||
}
|
||||
}
|
||||
|
||||
public void FlushRemaining()
|
||||
{
|
||||
if (Mode == CommandTreeTokenizer.Mode.Remaining)
|
||||
{
|
||||
if (_builder.Length > 0)
|
||||
{
|
||||
_remaining.Add(_builder.ToString());
|
||||
_builder.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
namespace Spectre.Console.Cli;
|
||||
|
||||
// Consider removing this in favor for value tuples at some point.
|
||||
internal sealed class MappedCommandParameter
|
||||
{
|
||||
public CommandParameter Parameter { get; }
|
||||
public string? Value { get; }
|
||||
|
||||
public MappedCommandParameter(CommandParameter parameter, string? value)
|
||||
{
|
||||
Parameter = parameter;
|
||||
Value = value;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user