/** * Author: dkanus * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore * License: GPL * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * * Acedia is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3 of the License, or * (at your option) any later version. * * Acedia is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ class CommandParser extends AcediaObject dependson(Command); //! Class specialized for parsing user input of the command's call into //![ `Command.CallData`] structure with the information about all parsed //! arguments. //! //! [`CommandParser`] is not made to parse the whole input: //! //! * Command's name needs to be parsed and resolved as an alias before using //! this parser - it won't do this hob for you; //! * List of targeted players must also be parsed using [`PlayersParser`] - //! [`CommandParser`] won't do this for you; //! * Optionally one can also decide on the referred subcommand and pass it into //! [`ParseWith()`] method. If subcommand's name is not passed - //! [`CommandParser`] will try to parse it itself. //! This feature is used to add support for subcommand aliases. //! //! However, above steps are handled by [`Commands_Feature`] and one only needs to //! call that feature's [`HandleInput()`] methods to pass user input with command //! call line there. //! //! # Usage //! //! Allocate [`CommandParser`] and call [`ParseWith()`] method, providing it with: //! //! 1. [`Parser`], filled with command call input; //! 2. Command's data that describes subcommands, options and their parameters //! for the command, which call we are parsing; //! 3. (Optionally) [`EPlayer`] reference to the player that initiated //! the command call; //! 4. (Optionally) Subcommand to be used - this will prevent [`CommandParser`] //! from parsing subcommand name itself. Used for implementing aliases that //! refer to a particular subcommand. //! //! # Implementation //! //! [`CommandParser`] stores both its state and command data, relevant to parsing, //! as its member variables during the whole parsing process, instead of passing //! that data around in every single method. //! //! We will give a brief overview of how around 20 parsing methods below are //! interconnected. //! //! The only public method [`ParseWith()`] is used to start parsing and it uses //! [`PickSubCommand()`] to first try and figure out what sub command is //! intended by user's input. //! //! Main bulk of the work is done by [`ParseParameterArrays()`] method, for //! simplicity broken into two [`ParseRequiredParameterArray()`] and //! [`ParseOptionalParameterArray()`] methods that can parse //! parameters for both command itself and it's options. //! //! They go through arrays of required and optional parameters, calling //! [`ParseParameter()`] for each parameters, which in turn can make several //! calls of [`ParseSingleValue()`] to parse parameters' values: it is called //! once for single-valued parameters, but possibly several times for list //! parameters that can contain several values. //! //! So main parsing method looks something like: //! //! ``` //! ParseParameterArrays() { //! loop ParseParameter() { //! loop ParseSingleValue() //! } //! } //! ``` //! //! [`ParseSingleValue()`] is essentially that redirects it's method call to //! another, more specific, parsing method based on the parameter type. //! //! Finally, to allow users to specify options at any point in command, we call //! [`TryParsingOptions()`] at the beginning of every [`ParseSingleValue()`] //! (the only parameter that has higher priority than options is //! [`CPT_Remainder`]), since option definition can appear at any place between //! parameters. We also call `TryParsingOptions()` *after* we've parsed all //! command's parameters, since that case won't be detected by parsing them //! *before* every parameter. //! //! [`TryParsingOptions()`] itself simply tries to detect "-" and "--" prefixes //! (filtering out negative numeric values) and then redirect the call to either //! of more specialized methods: [`ParseLongOption()`] or //! [`ParseShortOption()`], that can in turn make another //! [`ParseParameterArrays()`] call, if specified option has parameters. //! //! NOTE: [`ParseParameterArrays()`] can only nest in itself once, since option //! declaration always interrupts previous option's parameter list. //! Rest of the methods perform simple auxiliary functions. // Describes which parameters we are currently parsing, classifying them // as either "necessary" or "extra". // // E.g. if last require parameter is a list of integers, // then after parsing first integer we are: // // * Still parsing required *parameter* "integer list"; // * But no more integers are *necessary* for successful parsing. // // Therefore we consider parameter "necessary" if the lack of it will // result in failed parsing and "extra" otherwise. enum ParsingTarget { // We are in the process of parsing required parameters, that must all // be present. // This case does not include parsing last required parameter: it needs // to be treated differently to track when we change from "necessary" to // "extra" parameters. CPT_NecessaryParameter, // We are parsing last necessary parameter. CPT_LastNecessaryParameter, // We are not parsing extra parameters that can be safely omitted. CPT_ExtraParameter, }; // Parser filled with user input. var private Parser commandParser; // Data for sub-command specified by both command we are parsing // and user's input; determined early during parsing. var private Command.SubCommand pickedSubCommand; // Options available for the command we are parsing. var private array availableOptions; // Result variable we are filling during the parsing process, // should be `none` outside of [`self.ParseWith()`] method call. var private Command.CallData nextResult; // Parser for player parameters, setup with a caller for current parsing var private PlayersParser currentPlayersParser; // Current [`ParsingTarget`], see it's enum description for more details var private ParsingTarget currentTarget; // `true` means we are parsing parameters for a command's option and // `false` means we are parsing command's own parameters var private bool currentTargetIsOption; // If we are parsing parameters for an option (`currentTargetIsOption == true`) // this variable will store that option's data. var private Command.Option targetOption; // Last successful state of [`commandParser`]. var Parser.ParserState confirmedState; // Options we have so far encountered during parsing, necessary since we want // to forbid specifying th same option more than once. var private array usedOptions; // Literals that can be used as boolean values var private array booleanTrueEquivalents; var private array booleanFalseEquivalents; var LoggerAPI.Definition errNoSubCommands; protected function Finalizer() { Reset(); } /// Parses user's input given in [`parser`] using command's information given by /// [`commandData`]. /// /// Optionally, sub-command can be specified for the [`CommandParser`] to use /// via [`specifiedSubCommand`] argument. /// If this argument's value is `none` - it will be parsed from [`parser`]'s /// data instead. /// /// Returns results of parsing, described by [`Command.CallData`]. /// Returned object is guaranteed to be not `none`. public final function Command.CallData ParseWith( Parser parser, Command.Data commandData, EPlayer callerPlayer, optional BaseText specifiedSubCommand ) { local HashTable commandParameters; // Temporary object to return `nextResult` while setting variable to `none` local Command.CallData toReturn; nextResult.parameters = _.collections.EmptyHashTable(); nextResult.options = _.collections.EmptyHashTable(); if (commandData.subCommands.length == 0) { DeclareError(CET_NoSubCommands, none); toReturn = nextResult; Reset(); return toReturn; } if (parser == none || !parser.Ok()) { DeclareError(CET_BadParser, none); toReturn = nextResult; Reset(); return toReturn; } commandParser = parser; availableOptions = commandData.options; currentPlayersParser = PlayersParser(_.memory.Allocate(class'PlayersParser')); currentPlayersParser.SetSelf(callerPlayer); // (subcommand) (parameters, possibly with options) and nothing else! PickSubCommand(commandData, specifiedSubCommand); nextResult.subCommandName = pickedSubCommand.name.Copy(); commandParameters = ParseParameterArrays(pickedSubCommand.required, pickedSubCommand.optional); AssertNoTrailingInput(); // make sure there is nothing else if (commandParser.Ok()) { nextResult.parameters = commandParameters; } else { _.memory.Free(commandParameters); } // Clean up toReturn = nextResult; Reset(); return toReturn; } // Zero important variables private final function Reset() { local Command.CallData blankCallData; _.memory.Free(currentPlayersParser); currentPlayersParser = none; // We didn't create this one and are not meant to free it either commandParser = none; nextResult = blankCallData; currentTarget = CPT_NecessaryParameter; currentTargetIsOption = false; usedOptions.length = 0; } // Auxiliary method for recording errors private final function DeclareError(Command.ErrorType type, optional BaseText cause) { nextResult.parsingError = type; if (cause != none) { nextResult.errorCause = cause.Copy(); } if (commandParser != none) { commandParser.Fail(); } } // Assumes `commandParser != none`, is in successful state. // // Picks a sub command based on it's contents (parser's pointer must be before // where subcommand's name is specified). // // If [`specifiedSubCommand`] is not `none` - will always use that value instead // of parsing it from [`commandParser`]. private final function PickSubCommand(Command.Data commandData, BaseText specifiedSubCommand) { local int i; local MutableText candidateSubCommandName; local Command.SubCommand emptySubCommand; local array allSubCommands; allSubCommands = commandData.subCommands; if (allSubcommands.length == 0) { _.logger.Auto(errNoSubCommands).ArgClass(class); pickedSubCommand = emptySubCommand; return; } // Get candidate name confirmedState = commandParser.GetCurrentState(); if (specifiedSubCommand != none) { candidateSubCommandName = specifiedSubCommand.MutableCopy(); } else { commandParser.Skip().MUntil(candidateSubCommandName,, true); } // Try matching it to sub commands pickedSubCommand = allSubcommands[0]; if (candidateSubCommandName.IsEmpty()) { candidateSubCommandName.FreeSelf(); return; } for (i = 0; i < allSubcommands.length; i += 1) { if (candidateSubCommandName.Compare(allSubcommands[i].name)) { candidateSubCommandName.FreeSelf(); pickedSubCommand = allSubcommands[i]; return; } } // We will only reach here if we did not match any sub commands, // meaning that whatever consumed by[ `candidateSubCommandName`] probably // has a different meaning. commandParser.RestoreState(confirmedState); } // Assumes `commandParser` is not `none` // Declares an error if `commandParser` still has any input left private final function AssertNoTrailingInput() { local Text remainder; if (!commandParser.Ok()) return; if (commandParser.Skip().GetRemainingLength() <= 0) return; remainder = commandParser.GetRemainder(); DeclareError(CET_UnusedCommandParameters, remainder); remainder.FreeSelf(); } // Assumes `commandParser` is not `none`. // Parses given required and optional parameters along with any possible option // declarations. // Returns `HashTable` filled with (variable, parsed value) pairs. // Failure is equal to `commandParser` entering into a failed state. private final function HashTable ParseParameterArrays( array requiredParameters, array optionalParameters ) { local HashTable parsedParameters; if (!commandParser.Ok()) { return none; } parsedParameters = _.collections.EmptyHashTable(); // Parse parameters ParseRequiredParameterArray(parsedParameters, requiredParameters); ParseOptionalParameterArray(parsedParameters, optionalParameters); // Parse trailing options while (TryParsingOptions()); return parsedParameters; } // Assumes `commandParser` and `parsedParameters` are not `none`. // // Parses given required parameters along with any possible option declarations into given // `parsedParameters` `HashTable`. private final function ParseRequiredParameterArray( HashTable parsedParameters, array requiredParameters ) { local int i; if (!commandParser.Ok()) { return; } currentTarget = CPT_NecessaryParameter; while (i < requiredParameters.length) { if (i == requiredParameters.length - 1) { currentTarget = CPT_LastNecessaryParameter; } // Parse parameters one-by-one, reporting appropriate errors if (!ParseParameter(parsedParameters, requiredParameters[i])) { // Any failure to parse required parameter leads to error if (currentTargetIsOption) { DeclareError( CET_NoRequiredParamForOption, targetOption.longName); } else { DeclareError( CET_NoRequiredParam, requiredParameters[i].displayName); } return; } i += 1; } currentTarget = CPT_ExtraParameter; } // Assumes `commandParser` and `parsedParameters` are not `none`. // // Parses given optional parameters along with any possible option declarations // into given `parsedParameters` hash table. private final function ParseOptionalParameterArray( HashTable parsedParameters, array optionalParameters ) { local int i; if (!commandParser.Ok()) { return; } while (i < optionalParameters.length) { confirmedState = commandParser.GetCurrentState(); // Parse parameters one-by-one, reporting appropriate errors if (!ParseParameter(parsedParameters, optionalParameters[i])) { // Propagate errors if (nextResult.parsingError != CET_None) { return; } // Failure to parse optional parameter is fine if // it is caused by that parameters simply missing commandParser.RestoreState(confirmedState); break; } i += 1; } } // Assumes `commandParser` and `parsedParameters` are not `none`. // // Parses one given parameter along with any possible option declarations into // given `parsedParameters` `HashTable`. // // Returns `true` if we've successfully parsed given parameter without any // errors. private final function bool ParseParameter( HashTable parsedParameters, Command.Parameter expectedParameter ) { local bool parsedEnough; confirmedState = commandParser.GetCurrentState(); while (ParseSingleValue(parsedParameters, expectedParameter)) { if (currentTarget == CPT_LastNecessaryParameter) { currentTarget = CPT_ExtraParameter; } parsedEnough = true; // We are done if there is either no more input or we only needed // to parse a single value if (!expectedParameter.allowsList) { return true; } if (commandParser.Skip().HasFinished()) { return true; } confirmedState = commandParser.GetCurrentState(); } // We only succeeded in parsing if we've parsed enough for // a given parameter and did not encounter any errors if (parsedEnough && nextResult.parsingError == CET_None) { commandParser.RestoreState(confirmedState); return true; } // Clean up any values `ParseSingleValue` might have recorded parsedParameters.RemoveItem(expectedParameter.variableName); return false; } // Assumes `commandParser` and `parsedParameters` are not `none`. // // Parses a single value for a given parameter (e.g. one integer for integer or // integer list parameter types) along with any possible option declarations // into given `parsedParameters`. // // Returns `true` if we've successfully parsed a single value without // any errors. private final function bool ParseSingleValue( HashTable parsedParameters, Command.Parameter expectedParameter ) { // Before parsing any other value we need to check if user has specified any options instead. // // However this might lead to errors if we are already parsing necessary parameters of another // option: we must handle such situation and report an error. if (currentTargetIsOption) { // There is no problem is option's parameter is remainder if (expectedParameter.type == CPT_Remainder) { return ParseRemainderValue(parsedParameters, expectedParameter); } if (currentTarget != CPT_ExtraParameter && TryParsingOptions()) { DeclareError(CET_NoRequiredParamForOption, targetOption.longName); return false; } } while (TryParsingOptions()); // First we try `CPT_Remainder` parameter, since it is a special case that // consumes all further input if (expectedParameter.type == CPT_Remainder) { return ParseRemainderValue(parsedParameters, expectedParameter); } // Propagate errors after parsing options if (nextResult.parsingError != CET_None) { return false; } // Try parsing one of the variable types if (expectedParameter.type == CPT_Boolean) { return ParseBooleanValue(parsedParameters, expectedParameter); } else if (expectedParameter.type == CPT_Integer) { return ParseIntegerValue(parsedParameters, expectedParameter); } else if (expectedParameter.type == CPT_Number) { return ParseNumberValue(parsedParameters, expectedParameter); } else if (expectedParameter.type == CPT_Text) { return ParseTextValue(parsedParameters, expectedParameter); } else if (expectedParameter.type == CPT_Remainder) { return ParseRemainderValue(parsedParameters, expectedParameter); } else if (expectedParameter.type == CPT_Object) { return ParseObjectValue(parsedParameters, expectedParameter); } else if (expectedParameter.type == CPT_Array) { return ParseArrayValue(parsedParameters, expectedParameter); } else if (expectedParameter.type == CPT_JSON) { return ParseJSONValue(parsedParameters, expectedParameter); } else if (expectedParameter.type == CPT_Players) { return ParsePlayersValue(parsedParameters, expectedParameter); } return false; } // Assumes `commandParser` and `parsedParameters` are not `none`. // // Parses a single boolean value into given `parsedParameters` hash table. private final function bool ParseBooleanValue( HashTable parsedParameters, Command.Parameter expectedParameter ) { local int i; local bool isValidBooleanLiteral; local bool booleanValue; local MutableText parsedLiteral; commandParser.Skip().MUntil(parsedLiteral,, true); if (!commandParser.Ok()) { _.memory.Free(parsedLiteral); return false; } // Try to match parsed literal to any recognizable boolean literals for (i = 0; i < booleanTrueEquivalents.length; i += 1) { if (parsedLiteral.CompareToString(booleanTrueEquivalents[i], SCASE_INSENSITIVE)) { isValidBooleanLiteral = true; booleanValue = true; break; } } for (i = 0; i < booleanFalseEquivalents.length; i += 1) { if (isValidBooleanLiteral) { break; } if (parsedLiteral.CompareToString(booleanFalseEquivalents[i], SCASE_INSENSITIVE)) { isValidBooleanLiteral = true; booleanValue = false; } } parsedLiteral.FreeSelf(); if (!isValidBooleanLiteral) { return false; } RecordParameter(parsedParameters, expectedParameter, _.box.bool(booleanValue)); return true; } // Assumes `commandParser` and `parsedParameters` are not `none`. // Parses a single integer value into given `parsedParameters` hash table. private final function bool ParseIntegerValue( HashTable parsedParameters, Command.Parameter expectedParameter ) { local int integerValue; commandParser.Skip().MInteger(integerValue); if (!commandParser.Ok()) { return false; } RecordParameter(parsedParameters, expectedParameter, _.box.int(integerValue)); return true; } // Assumes `commandParser` and `parsedParameters` are not `none`. // Parses a single number (float) value into given `parsedParameters` // hash table. private final function bool ParseNumberValue( HashTable parsedParameters, Command.Parameter expectedParameter ) { local float numberValue; commandParser.Skip().MNumber(numberValue); if (!commandParser.Ok()) { return false; } RecordParameter(parsedParameters, expectedParameter, _.box.float(numberValue)); return true; } // Assumes `commandParser` and `parsedParameters` are not `none`. // Parses a single `Text` value into given `parsedParameters` // hash table. private final function bool ParseTextValue( HashTable parsedParameters, Command.Parameter expectedParameter ) { local bool failedParsing; local MutableText textValue; local Parser.ParserState initialState; local HashTable resolvedPair; // (needs some work for reading formatting `string`s from `Text` objects) initialState = commandParser.Skip().GetCurrentState(); // Try manually parsing as a string literal first, since then we will // allow empty `textValue` as a result commandParser.MStringLiteral(textValue); failedParsing = !commandParser.Ok(); // Otherwise - empty values are not allowed if (failedParsing) { _.memory.Free(textValue); commandParser.RestoreState(initialState).MString(textValue); failedParsing = (!commandParser.Ok() || textValue.IsEmpty()); } if (failedParsing) { _.memory.Free(textValue); commandParser.Fail(); return false; } resolvedPair = AutoResolveAlias(textValue, expectedParameter.aliasSourceName); if (resolvedPair != none) { RecordParameter(parsedParameters, expectedParameter, resolvedPair); _.memory.Free(textValue); } else { RecordParameter(parsedParameters, expectedParameter, textValue.IntoText()); } return true; } // Resolves alias and returns it, along with the resolved value, if parameter // was specified to be auto-resolved. // Returns `none` otherwise. private final function HashTable AutoResolveAlias(MutableText textValue, Text aliasSourceName) { local HashTable result; local Text resolvedValue, immutableValue; if (textValue == none) return none; if (aliasSourceName == none) return none; // Always create `HashTable` with at least "alias" key result = _.collections.EmptyHashTable(); immutableValue = textValue.Copy(); result.SetItem(P("alias"), immutableValue); _.memory.Free(immutableValue); // Add "value" key only after we've checked for "$" prefix if (!textValue.StartsWithS("$")) { result.SetItem(P("value"), immutableValue); return result; } if (aliasSourceName.Compare(P("weapon"))) { resolvedValue = _.alias.ResolveWeapon(textValue, true); } else if (aliasSourceName.Compare(P("color"))) { resolvedValue = _.alias.ResolveColor(textValue, true); } else if (aliasSourceName.Compare(P("feature"))) { resolvedValue = _.alias.ResolveFeature(textValue, true); } else if (aliasSourceName.Compare(P("entity"))) { resolvedValue = _.alias.ResolveEntity(textValue, true); } else { resolvedValue = _.alias.ResolveCustom(aliasSourceName, textValue, true); } result.SetItem(P("value"), resolvedValue); _.memory.Free(resolvedValue); return result; } // Assumes `commandParser` and `parsedParameters` are not `none`. // // Parses a single `Text` value into given `parsedParameters` hash table, // consuming all remaining contents. private final function bool ParseRemainderValue( HashTable parsedParameters, Command.Parameter expectedParameter ) { local MutableText value; commandParser.Skip().MUntil(value); if (!commandParser.Ok()) { return false; } RecordParameter(parsedParameters, expectedParameter, value.IntoText()); return true; } // Assumes `commandParser` and `parsedParameters` are not `none`. // // Parses a single JSON object into given `parsedParameters` hash table. private final function bool ParseObjectValue( HashTable parsedParameters, Command.Parameter expectedParameter ) { local HashTable objectValue; objectValue = _.json.ParseHashTableWith(commandParser); if (!commandParser.Ok()) { return false; } RecordParameter(parsedParameters, expectedParameter, objectValue); return true; } // Assumes `commandParser` and `parsedParameters` are not `none`. // Parses a single JSON array into given `parsedParameters` hash table. private final function bool ParseArrayValue( HashTable parsedParameters, Command.Parameter expectedParameter ) { local ArrayList arrayValue; arrayValue = _.json.ParseArrayListWith(commandParser); if (!commandParser.Ok()) { return false; } RecordParameter(parsedParameters, expectedParameter, arrayValue); return true; } // Assumes `commandParser` and `parsedParameters` are not `none`. // Parses a single JSON value into given `parsedParameters` // hash table. private final function bool ParseJSONValue( HashTable parsedParameters, Command.Parameter expectedParameter ) { local AcediaObject jsonValue; jsonValue = _.json.ParseWith(commandParser); if (!commandParser.Ok()) { return false; } RecordParameter(parsedParameters, expectedParameter, jsonValue); return true; } // Assumes `commandParser` and `parsedParameters` are not `none`. // Parses a single JSON value into given `parsedParameters` hash table. private final function bool ParsePlayersValue( HashTable parsedParameters, Command.Parameter expectedParameter) { local ArrayList resultPlayerList; local array targetPlayers; currentPlayersParser.ParseWith(commandParser); if (commandParser.Ok()) { targetPlayers = currentPlayersParser.GetPlayers(); } else { return false; } resultPlayerList = _.collections.NewArrayList(targetPlayers); _.memory.FreeMany(targetPlayers); RecordParameter(parsedParameters, expectedParameter, resultPlayerList); return true; } // Assumes `parsedParameters` is not `none`. // // Records `value` for a given `parameter` into a given `parametersArray`. // If parameter is not a list type - simply records `value` as value under // `parameter.variableName` key. // If parameter is a list type - pushed value at the end of an array, recorded at // `parameter.variableName` key (creating it if missing). // // All recorded values are managed by `parametersArray`. private final function RecordParameter( HashTable parametersArray, Command.Parameter parameter, /*take*/ AcediaObject value ) { local ArrayList parameterVariable; if (!parameter.allowsList) { parametersArray.SetItem(parameter.variableName, value); _.memory.Free(value); return; } parameterVariable = ArrayList(parametersArray.GetItem(parameter.variableName)); if (parameterVariable == none) { parameterVariable = _.collections.EmptyArrayList(); } parameterVariable.AddItem(value); _.memory.Free(value); parametersArray.SetItem(parameter.variableName, parameterVariable); _.memory.Free(parameterVariable); } // Assumes `commandParser` is not `none`. // // Tries to parse an option declaration (along with all of it's parameters) with // `commandParser`. // // Returns `true` on success and `false` otherwise. // // In case of failure to detect option declaration also reverts state of // `commandParser` to that before `TryParsingOptions()` call. // However, if option declaration was present, but invalid (or had invalid // parameters) parser will be left in a failed state. private final function bool TryParsingOptions() { local int temporaryInt; if (!commandParser.Ok()) { return false; } confirmedState = commandParser.GetCurrentState(); // Long options commandParser.Skip().Match(P("--")); if (commandParser.Ok()) { return ParseLongOption(); } // Filter out negative numbers that start similarly to short options: // -3, -5.7, -.9 commandParser .RestoreState(confirmedState) .Skip() .Match(P("-")) .MUnsignedInteger(temporaryInt, 10, 1); if (commandParser.Ok()) { commandParser.RestoreState(confirmedState); return false; } commandParser.RestoreState(confirmedState).Skip().Match(P("-.")); if (commandParser.Ok()) { commandParser.RestoreState(confirmedState); return false; } // Short options commandParser.RestoreState(confirmedState).Skip().Match(P("-")); if (commandParser.Ok()) { return ParseShortOption(); } commandParser.RestoreState(confirmedState); return false; } // Assumes `commandParser` is not `none`. // // Tries to parse a long option name along with all of it's possible parameters // with `commandParser`. // // Returns `true` on success and `false` otherwise. At the point this method is // called, option declaration is already assumed to be detected and any failure // implies parsing error (ending in failed `Command.CallData`). private final function bool ParseLongOption() { local int i, optionIndex; local MutableText optionName; commandParser.MUntil(optionName,, true); if (!commandParser.Ok()) { return false; } while (optionIndex < availableOptions.length) { if (optionName.Compare(availableOptions[optionIndex].longName)) break; optionIndex += 1; } if (optionIndex >= availableOptions.length) { DeclareError(CET_UnknownOption, optionName); optionName.FreeSelf(); return false; } for (i = 0; i < usedOptions.length; i += 1) { if (optionName.Compare(usedOptions[i].longName)) { DeclareError(CET_RepeatedOption, optionName); optionName.FreeSelf(); return false; } } //usedOptions[usedOptions.length] = availableOptions[optionIndex]; optionName.FreeSelf(); return ParseOptionParameters(availableOptions[optionIndex]); } // Assumes `commandParser` and `nextResult` are not `none`. // // Tries to parse a short option name along with all of it's possible parameters // with `commandParser`. // // Returns `true` on success and `false` otherwise. At the point this // method is called, option declaration is already assumed to be detected // and any failure implies parsing error (ending in failed `Command.CallData`). private final function bool ParseShortOption() { local int i; local bool pickedOptionWithParameters; local MutableText optionsList; commandParser.MUntil(optionsList,, true); if (!commandParser.Ok()) { optionsList.FreeSelf(); return false; } for (i = 0; i < optionsList.GetLength(); i += 1) { if (nextResult.parsingError != CET_None) break; pickedOptionWithParameters = AddOptionByCharacter( optionsList.GetCharacter(i), optionsList, pickedOptionWithParameters) || pickedOptionWithParameters; } optionsList.FreeSelf(); return (nextResult.parsingError == CET_None); } // Assumes `commandParser` and `nextResult` are not `none`. // // Auxiliary method that adds option by it's short version's character // `optionCharacter`. // // It also accepts `optionSourceList` that describes short option expression // (e.g. "-rtV") from // which it originated for error reporting and `forbidOptionWithParameters` // that, when set to `true`, forces this method to cause the // `CET_MultipleOptionsWithParams` error if new option has non-empty parameters. // // Method returns `true` if added option had non-empty parameters and `false` // otherwise. // // Any parsing failure inside this method always causes // `nextError.DeclareError()` call, so you can use `nextResult.IsSuccessful()` // to check if method has failed. private final function bool AddOptionByCharacter( BaseText.Character optionCharacter, BaseText optionSourceList, bool forbidOptionWithParameters ) { local int i; local bool optionHasParameters; // Prevent same option appearing twice for (i = 0; i < usedOptions.length; i += 1) { if (_.text.AreEqual(optionCharacter, usedOptions[i].shortName)) { DeclareError(CET_RepeatedOption, usedOptions[i].longName); return false; } } // If it's a new option - look it up in all available options for (i = 0; i < availableOptions.length; i += 1) { if (!_.text.AreEqual(optionCharacter, availableOptions[i].shortName)) { continue; } usedOptions[usedOptions.length] = availableOptions[i]; optionHasParameters = (availableOptions[i].required.length > 0 || availableOptions[i].optional.length > 0); // Enforce `forbidOptionWithParameters` flag restriction if (optionHasParameters && forbidOptionWithParameters) { DeclareError(CET_MultipleOptionsWithParams, optionSourceList); return optionHasParameters; } // Parse parameters (even if they are empty) and bail commandParser.Skip(); ParseOptionParameters(availableOptions[i]); break; } if (i >= availableOptions.length) { DeclareError(CET_UnknownShortOption); } return optionHasParameters; } // Auxiliary method for parsing option's parameters (including empty ones). // Automatically fills `nextResult` with parsed parameters (or `none` if option // has no parameters). // Assumes `commandParser` and `nextResult` are not `none`. private final function bool ParseOptionParameters(Command.Option pickedOption) { local HashTable optionParameters; // If we are already parsing other option's parameters and did not finish // parsing all required ones - we cannot start another option if (currentTargetIsOption && currentTarget != CPT_ExtraParameter) { DeclareError(CET_NoRequiredParamForOption, targetOption.longName); return false; } if (pickedOption.required.length == 0 && pickedOption.optional.length == 0) { nextResult.options.SetItem(pickedOption.longName, none); return true; } currentTargetIsOption = true; targetOption = pickedOption; optionParameters = ParseParameterArrays( pickedOption.required, pickedOption.optional); currentTargetIsOption = false; if (commandParser.Ok()) { nextResult.options.SetItem(pickedOption.longName, optionParameters); _.memory.Free(optionParameters); return true; } _.memory.Free(optionParameters); return false; } defaultproperties { booleanTrueEquivalents(0) = "true" booleanTrueEquivalents(1) = "enable" booleanTrueEquivalents(2) = "on" booleanTrueEquivalents(3) = "yes" booleanFalseEquivalents(0) = "false" booleanFalseEquivalents(1) = "disable" booleanFalseEquivalents(2) = "off" booleanFalseEquivalents(3) = "no" errNoSubCommands = (l=LOG_Error,m="`GetSubCommand()` method was called on a command `%1` with zero defined sub-commands.") }