|
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Collections.Generic;
using System.CommandLine.Invocation;
using System.Linq;
namespace System.CommandLine.Parsing
{
internal sealed class ParseOperation
{
private readonly List<Token> _tokens;
private readonly ParserConfiguration _configuration;
private readonly string? _rawInput;
private readonly SymbolResultTree _symbolResultTree;
private readonly CommandResult _rootCommandResult;
private int _index;
private CommandResult _innermostCommandResult;
private bool _isTerminatingDirectiveSpecified;
private CommandLineAction? _primaryAction;
private List<CommandLineAction>? _preActions;
private readonly Command _rootCommand;
public ParseOperation(
List<Token> tokens,
Command rootCommand,
ParserConfiguration configuration,
List<string>? tokenizeErrors,
string? rawInput)
{
_tokens = tokens;
_configuration = configuration;
_rootCommand = rootCommand;
_rawInput = rawInput;
_symbolResultTree = new(_rootCommand, tokenizeErrors);
_innermostCommandResult = _rootCommandResult = new CommandResult(
_rootCommand,
CurrentToken,
_symbolResultTree);
_symbolResultTree.Add(_rootCommand, _rootCommandResult);
Advance();
}
private Token CurrentToken => _tokens[_index];
private void Advance() => _index++;
private bool More(out TokenType currentTokenType)
{
bool result = _index < _tokens.Count;
currentTokenType = result ? _tokens[_index].Type : (TokenType)(-1);
return result;
}
internal ParseResult Parse()
{
ParseDirectives();
ParseCommandChildren();
ValidateAndAddDefaultResults();
return new(
_configuration,
_rootCommandResult,
_innermostCommandResult,
_tokens,
_symbolResultTree.UnmatchedTokens,
_symbolResultTree.Errors,
_rawInput,
_primaryAction,
_preActions);
}
private void ParseSubcommand()
{
Command command = (Command)CurrentToken.Symbol!;
_innermostCommandResult = new CommandResult(
command,
CurrentToken,
_symbolResultTree,
_innermostCommandResult);
_symbolResultTree.Add(command, _innermostCommandResult);
Advance();
ParseCommandChildren();
}
private void ParseCommandChildren()
{
int currentArgumentCount = 0;
int currentArgumentIndex = 0;
while (More(out TokenType currentTokenType))
{
// Advance past arguments whose arity has been filled so that
// IsCapturingRemainingTokens checks the correct argument.
var arguments = _innermostCommandResult.Command.Arguments;
while (currentArgumentIndex < arguments.Count &&
currentArgumentCount >= arguments[currentArgumentIndex].Arity.MaximumNumberOfValues)
{
currentArgumentCount = 0;
currentArgumentIndex++;
}
// When the next argument to fill captures remaining tokens,
// consume tokens regardless of type. DoubleDash tokens encountered
// before capture starts (in this dispatch) are still handled normally,
// but once inside ParseCommandArguments they are captured as values.
// For non-Argument tokens (options, commands), only capture after at least one
// positional argument has been filled, so that leading options are parsed normally.
if (currentTokenType != TokenType.DoubleDash &&
IsCapturingRemainingTokens(currentArgumentIndex) &&
(currentTokenType == TokenType.Argument || currentArgumentIndex > 0 || currentArgumentCount > 0))
{
ParseCommandArguments(ref currentArgumentCount, ref currentArgumentIndex, captureRemaining: true);
}
else if (currentTokenType == TokenType.Command)
{
ParseSubcommand();
}
else if (currentTokenType == TokenType.Option)
{
ParseOption();
}
else if (currentTokenType == TokenType.Argument)
{
ParseCommandArguments(ref currentArgumentCount, ref currentArgumentIndex);
}
else
{
AddCurrentTokenToUnmatched();
Advance();
}
}
}
private bool IsCapturingRemainingTokens(int currentArgumentIndex)
{
var arguments = _innermostCommandResult.Command.Arguments;
return currentArgumentIndex < arguments.Count &&
arguments[currentArgumentIndex].CaptureRemainingTokens;
}
private void ParseCommandArguments(ref int currentArgumentCount, ref int currentArgumentIndex, bool captureRemaining = false)
{
while (More(out TokenType currentTokenType) &&
(currentTokenType == TokenType.Argument ||
captureRemaining))
{
while (_innermostCommandResult.Command.HasArguments && currentArgumentIndex < _innermostCommandResult.Command.Arguments.Count)
{
Argument argument = _innermostCommandResult.Command.Arguments[currentArgumentIndex];
if (currentArgumentCount < argument.Arity.MaximumNumberOfValues)
{
if (captureRemaining || CurrentToken.Symbol is null)
{
CurrentToken.Symbol = argument;
}
if (!(_symbolResultTree.TryGetValue(argument, out var symbolResult)
&& symbolResult is ArgumentResult argumentResult))
{
argumentResult =
new ArgumentResult(
argument,
_symbolResultTree,
_innermostCommandResult);
_symbolResultTree.Add(argument, argumentResult);
}
argumentResult.AddToken(CurrentToken);
_innermostCommandResult.AddToken(CurrentToken);
currentArgumentCount++;
Advance();
break;
}
else
{
currentArgumentCount = 0;
currentArgumentIndex++;
}
}
if (currentArgumentCount == 0) // no matching arguments found
{
if (captureRemaining)
{
// Return to ParseCommandChildren so that overflow tokens
// are dispatched normally (e.g. as options or subcommands).
break;
}
AddCurrentTokenToUnmatched();
Advance();
}
}
}
private void ParseOption()
{
Option option = (Option)CurrentToken.Symbol!;
OptionResult optionResult;
if (!_symbolResultTree.TryGetValue(option, out SymbolResult? symbolResult))
{
if (option.Action is not null)
{
// directives have a precedence over --help and --version
if (!_isTerminatingDirectiveSpecified)
{
if (option.Action.Terminating)
{
_primaryAction = option.Action;
}
else
{
AddPreAction(option.Action);
}
}
}
optionResult = new OptionResult(
option,
_symbolResultTree,
CurrentToken,
_innermostCommandResult);
_symbolResultTree.Add(option, optionResult);
}
else
{
optionResult = (OptionResult)symbolResult;
}
optionResult.IdentifierTokenCount++;
Advance();
ParseOptionArguments(optionResult);
}
private void ParseOptionArguments(OptionResult optionResult)
{
var argument = optionResult.Option.Argument;
var contiguousTokens = 0;
int argumentCount = 0;
while (More(out TokenType currentTokenType) && currentTokenType == TokenType.Argument)
{
if (argumentCount >= argument.Arity.MaximumNumberOfValues)
{
if (contiguousTokens > 0)
{
break;
}
if (argument.Arity.MaximumNumberOfValues == 0)
{
break;
}
}
else if (argument.IsBoolean() && !bool.TryParse(CurrentToken.Value, out _))
{
// Don't greedily consume the following token for bool. The presence of the option token (i.e. a flag) is sufficient.
break;
}
if (!(_symbolResultTree.TryGetValue(argument, out SymbolResult? symbolResult)
&& symbolResult is ArgumentResult argumentResult))
{
argumentResult = new ArgumentResult(
argument,
_symbolResultTree,
optionResult);
_symbolResultTree.Add(argument, argumentResult);
}
argumentResult.AddToken(CurrentToken);
optionResult.AddToken(CurrentToken);
argumentCount++;
contiguousTokens++;
Advance();
if (!optionResult.Option.AllowMultipleArgumentsPerToken)
{
return;
}
}
if (argumentCount == 0)
{
if (!_symbolResultTree.ContainsKey(argument))
{
var argumentResult = new ArgumentResult(argument, _symbolResultTree, optionResult);
_symbolResultTree.Add(argument, argumentResult);
}
}
}
private void ParseDirectives()
{
while (More(out TokenType currentTokenType) && currentTokenType == TokenType.Directive)
{
if (_rootCommand is RootCommand { Directives.Count: > 0 })
{
ParseDirective(); // kept in separate method to avoid JIT
}
Advance();
}
void ParseDirective()
{
var token = CurrentToken;
if (token.Symbol is not Directive directive)
{
AddCurrentTokenToUnmatched();
return;
}
DirectiveResult result;
if (_symbolResultTree.TryGetValue(directive, out var directiveResult))
{
result = (DirectiveResult)directiveResult;
result.AddToken(token);
}
else
{
result = new (directive, token, _symbolResultTree);
_symbolResultTree.Add(directive, result);
}
ReadOnlySpan<char> withoutBrackets = token.Value.AsSpan(1, token.Value.Length - 2);
int indexOfColon = withoutBrackets.IndexOf(':');
if (indexOfColon > 0)
{
result.AddValue(withoutBrackets.Slice(indexOfColon + 1).ToString());
}
if (directive.Action is not null)
{
if (directive.Action.Terminating)
{
_primaryAction = directive.Action;
_isTerminatingDirectiveSpecified = true;
}
else
{
AddPreAction(directive.Action);
}
}
}
}
private void AddPreAction(CommandLineAction action)
{
if (_preActions is null)
{
_preActions = new();
}
_preActions.Add(action);
}
private void AddPreActionsForImplicitOptions()
{
foreach (var kvp in _symbolResultTree)
{
if (kvp is { Key: Option { Action: { Terminating: false } action }, Value: OptionResult { Implicit: true } } &&
_primaryAction != action)
{
AddPreAction(action);
}
}
}
private void AddCurrentTokenToUnmatched()
{
if (CurrentToken.Type == TokenType.DoubleDash)
{
return;
}
_symbolResultTree.AddUnmatchedToken(CurrentToken, _innermostCommandResult, _rootCommandResult);
}
private void ValidateAndAddDefaultResults()
{
// Only the innermost command goes through complete validation,
// for other commands only a subset of options is checked.
_innermostCommandResult.Validate(isInnermostCommand: true);
CommandResult? currentResult = _innermostCommandResult.Parent as CommandResult;
while (currentResult is not null)
{
currentResult.Validate(isInnermostCommand: false);
currentResult = currentResult.Parent as CommandResult;
}
if (_primaryAction is null)
{
if (_innermostCommandResult is { Command: { Action: null, HasSubcommands: true } })
{
_symbolResultTree.InsertFirstError(
new ParseError(LocalizationResources.RequiredCommandWasNotProvided(), _innermostCommandResult));
}
if (_innermostCommandResult is { Command.Action.ClearsParseErrors: true } &&
_symbolResultTree.Errors is not null)
{
var errorsNotUnderInnermostCommand = _symbolResultTree
.Errors
.Where(e => e.SymbolResult != _innermostCommandResult)
.ToList();
_symbolResultTree.Errors = errorsNotUnderInnermostCommand;
}
else if (_symbolResultTree.ErrorCount > 0)
{
_primaryAction = new ParseErrorAction();
}
}
else
{
if (_symbolResultTree.ErrorCount > 0 &&
_primaryAction.ClearsParseErrors &&
_symbolResultTree.Errors is not null)
{
foreach (var kvp in _symbolResultTree)
{
var symbol = kvp.Key;
if (symbol is Option { Action: { } optionAction } option)
{
if (_primaryAction == optionAction)
{
var errorsForPrimarySymbol = _symbolResultTree
.Errors
.Where(e => e.SymbolResult is OptionResult r && r.Option == option)
.ToList();
_symbolResultTree.Errors = errorsForPrimarySymbol;
return;
}
}
if (symbol is Command { Action: { } commandAction } command)
{
if (_primaryAction == commandAction)
{
var errorsForPrimarySymbol = _symbolResultTree
.Errors
.Where(e => e.SymbolResult is CommandResult r && r.Command == command)
.ToList();
_symbolResultTree.Errors = errorsForPrimarySymbol;
return;
}
}
}
if (_symbolResultTree.ErrorCount > 0)
{
_symbolResultTree.Errors?.Clear();
}
}
}
AddPreActionsForImplicitOptions();
}
}
} |