/** * 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 CommandDataBuilder extends AcediaObject dependson(Command); //! This is an auxiliary class for convenient creation of [`Command::Data`] //! using a builder pattern. //! //! ## Implementation //! //! We will store all defined data in two ways: //! //! 1. Selected data: data about parameters for subcommand/option that is //! currently being filled; //! 2. Prepared data: data that was already filled as "selected data" then //! stored in these records. Whenever we want to switch to filling another //! subcommand/option or return already prepared data we must dump //! "selected data" into "prepared data" first and then return the latter. //! //! Builder object is automatically created when new `Command` instance is //! allocated and doesn't normally need to be allocated by hand. // "Prepared data" var private Text commandName, commandGroup; var private Text commandSummary; var private array subcommands; var private array options; var private bool requiresTarget; // Auxiliary arrays signifying that we've started adding optional parameters // into appropriate `subcommands` and `options`. // // All optional parameters must follow strictly after required parameters and // so, after user have started adding optional parameters to subcommand/option, // we prevent them from adding required ones (to that particular // command/option). var private array subcommandsIsOptional; var private array optionsIsOptional; // "Selected data" // `false` means we have selected sub-command, `true` - option var private bool selectedItemIsOption; // `name` for sub-commands, `longName` for options var private Text selectedItemName; // Description of selected sub-command/option var private Text selectedDescription; // Are we filling optional parameters (`true`)? Or required ones (`false`)? var private bool selectionIsOptional; // Array of parameters we are currently filling (either required or optional) var private array selectedParameterArray; var private LoggerAPI.Definition errLongNameTooShort, errShortNameTooLong; var private LoggerAPI.Definition warnSameLongName, warnSameShortName; protected function Constructor() { // Fill empty subcommand (no special key word) by default SubCommand(P("")); } protected function Finalizer() { subcommands.length = 0; subcommandsIsOptional.length = 0; options.length = 0; optionsIsOptional.length = 0; selectedParameterArray.length = 0; commandName = none; commandGroup = none; commandSummary = none; selectedItemName = none; selectedDescription = none; requiresTarget = false; selectedItemIsOption = false; selectionIsOptional = false; } /// Method that starts defining a new sub-command. /// /// Creates new sub-command with a given name (if it's missing) and then selects /// sub-command with a given name to add parameters to. /// /// [`name`] defines name of the sub-command user wants, case-sensitive. /// If `none` is passed, this method will do nothing. public final function SubCommand(BaseText name) { local int subcommandIndex; if (name == none) { return; } if (!selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(name)) { return; } RecordSelection(); subcommandIndex = FindSubCommandIndex(name); if (subcommandIndex < 0) { MakeEmptySelection(name, false); return; } // Load appropriate prepared data, if it exists for // sub-command with name `name` selectedItemIsOption = false; selectedItemName = subcommands[subcommandIndex].name; selectedDescription = subcommands[subcommandIndex].description; selectionIsOptional = subcommandsIsOptional[subcommandIndex] > 0; if (selectionIsOptional) { selectedParameterArray = subcommands[subcommandIndex].optional; } else { selectedParameterArray = subcommands[subcommandIndex].required; } } /// Method that starts defining a new option. /// /// This method checks if some of the recorded options are in conflict with /// given `longName` and `shortName` (already using one and only one of them). /// In case there is no conflict, it creates new option with specified long and /// short names (if such option is missing) and selects option with a long name /// `longName` to add parameters to. /// /// [`longName`] defines long name of the option, case-sensitive (for using /// an option in form "--..."). Must be at least two characters long. /// [`shortName`] defines short name of the option, case-sensitive (for using /// an option in form "-..."). Must be exactly one character. /// /// # Errors /// /// Errors will be logged in case either of arguments are `none`, have /// inappropriate length or are in conflict with each other. public final function Option(BaseText longName, optional BaseText shortName) { local int optionIndex; local BaseText.Character shortNameAsCharacter; // Unlike for `SubCommand()`, we need to ensure that option naming is // correct and does not conflict with existing options // (user might attempt to add two options with same long names and // different short ones). shortNameAsCharacter = GetValidShortName(longName, shortName); if ( !_.text.IsValidCharacter(shortNameAsCharacter) || VerifyNoOptionNamingConflict(longName, shortNameAsCharacter)) { // ^ `GetValidShortName()` and `VerifyNoOptionNamingConflict()` // are responsible for logging warnings/errors return; } SelectOption(longName); // Set short name for new options optionIndex = FindOptionIndex(longName); if (optionIndex < 0) { // We can only be here if option was created for the first time RecordSelection(); // So now it cannot fail optionIndex = FindOptionIndex(longName); options[optionIndex].shortName = shortNameAsCharacter; } } /// Adds description to the selected sub-command / option. /// /// Highlights parts of the description in-between "`" characters. /// /// Does nothing if nothing is yet selected. public final function Describe(BaseText description) { local int fromIndex, toIndex; local BaseText.Formatting keyWordFormatting; local bool lookingForEnd; local MutableText coloredDescription; if (description == none) { return; } keyWordFormatting = _.text.FormattingFromColor(_.color.TextEmphasis); coloredDescription = description.MutableCopy(); while (true) { if (lookingForEnd) { toIndex = coloredDescription.IndexOf(P("`"), fromIndex + 1); } else { fromIndex = coloredDescription.IndexOf(P("`"), toIndex + 1); } if (toIndex < 0 || fromIndex < 0) { break; } if (lookingForEnd) { coloredDescription.ChangeFormatting( keyWordFormatting, fromIndex, toIndex - fromIndex + 1); lookingForEnd = false; } else { lookingForEnd = true; } } coloredDescription.Replace(P("`"), P("")); if (lookingForEnd) { coloredDescription.ChangeFormatting(keyWordFormatting, fromIndex); } _.memory.Free(selectedDescription); selectedDescription = coloredDescription.IntoText(); } /// Sets new group of `Command.Data` under construction. /// /// Group name is meant to be shared among several commands, allowing user to /// filter or fetch commands of a certain group. /// Group name is case-insensitive. public final function Group(BaseText newName) { if (newName != none && newName == commandGroup) { return; } _.memory.Free(commandGroup); if (newName != none) { commandGroup = newName.Copy(); } else { commandGroup = none; } } /// Sets new summary of `Command.Data` under construction. /// /// Summary gives a short description of the command on the whole that will /// be displayed when "help" command is listing available command public final function Summary(BaseText newSummary) { if (newSummary != none && newSummary == commandSummary) { return; } _.memory.Free(commandSummary); if (newSummary != none) { commandSummary = newSummary.Copy(); } else { commandSummary = none; } } /// Makes caller builder to mark `Command.Data` under construction to require /// a player target. public final function RequireTarget() { requiresTarget = true; } /// Any parameters added to currently selected sub-command / option after /// calling this method will be marked as optional. /// /// Further calls when the same sub-command / option is selected will do /// nothing. public final function OptionalParams() { if (selectionIsOptional) { return; } // Record all required parameters first, otherwise there would be no way // to distinguish between them and optional parameters RecordSelection(); selectionIsOptional = true; selectedParameterArray.length = 0; } /// Returns data that has been constructed so far by the caller /// [`CommandDataBuilder`]. /// /// Does not reset progress. public final function Command.Data BorrowData() { local Command.Data newData; // TODO: is this copying needed? RecordSelection(); newData.group = commandGroup; newData.summary = commandSummary; newData.subcommands = subcommands; newData.options = options; newData.requiresTarget = requiresTarget; return newData; } // Adds new parameter to selected sub-command / option private final function PushParameter(Command.Parameter newParameter) { selectedParameterArray[selectedParameterArray.length] = newParameter; } // Fills `Command.ParameterType` struct with given values // (except boolean format). // Assumes `displayName != none`. private final function Command.Parameter NewParameter( BaseText displayName, Command.ParameterType parameterType, bool isListParameter, optional BaseText variableName ) { local Command.Parameter newParameter; newParameter.displayName = displayName.Copy(); newParameter.type = parameterType; newParameter.allowsList = isListParameter; if (variableName != none) { newParameter.variableName = variableName.Copy(); } else { newParameter.variableName = displayName.Copy(); } return newParameter; } /// Adds new boolean parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`format`] defines preferred format of boolean values. /// Command parser will still accept boolean values in any form, this setting /// only affects how parameter will be displayed in generated help. /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command /// input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamBoolean( BaseText name, optional Command.PreferredBooleanFormat format, optional BaseText variableName ) { local Command.Parameter newParam; if (name != none) { newParam = NewParameter(name, CPT_Boolean, false, variableName); newParam.booleanFormat = format; PushParameter(newParam); } } /// Adds new integer list parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// List parameters expect user to enter one or more value of the same type as /// command's arguments. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`format`] defines preferred format of boolean values. /// Command parser will still accept boolean values in any form, this setting /// only affects how /// parameter will be displayed in generated help. /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamBooleanList( BaseText name, optional Command.PreferredBooleanFormat format, optional BaseText variableName ) { local Command.Parameter newParam; if (name != none) { newParam = NewParameter(name, CPT_Boolean, true, variableName); newParam.booleanFormat = format; PushParameter(newParam); } } /// Adds new integer parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamInteger(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_Integer, false, variableName)); } } /// Adds new integer list parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// List parameters expect user to enter one or more value of the same type as /// command's arguments. /// /// [`name`] will become the name of the parameter (it would appear in /// the generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamIntegerList(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_Integer, true, variableName)); } } /// Adds new numeric parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// [`name`] will become the name of the parameter (it would appear in the /// generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamNumber(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_Number, false, variableName)); } } /// Adds new numeric list parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// List parameters expect user to enter one or more value of the same type as /// command's arguments. /// /// [`name`] will become the name of the parameter (it would appear in the /// generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamNumberList(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_Number, true, variableName)); } } /// Adds new text parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// [`name`] will become the name of the parameter (it would appear in the /// generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. /// /// [`aliasSourceName`] defines name of the alias source that must be used to /// auto-resolve this parameter's value. `none` means that parameter will be /// recorded as-is, any other value (either "weapon", "color", "feature", /// "entity" or some kind of custom alias source name) will make values prefixed /// with "$" to be resolved as custom aliases. /// In case auto-resolving is used, value will be recorded as a `HasTable` with /// two fields: "alias" - value provided by user and (in case "$" prefix was /// used) "value" - actual resolved value of an alias. /// If alias has failed to be resolved, `none` will be stored as a value. public final function ParamText( BaseText name, optional BaseText variableName, optional BaseText aliasSourceName ) { local Command.Parameter newParameterValue; if (name == none) { return; } newParameterValue = NewParameter(name, CPT_Text, false, variableName); if (aliasSourceName != none) { newParameterValue.aliasSourceName = aliasSourceName.Copy(); } PushParameter(newParameterValue); } /// Adds new text list parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// List parameters expect user to enter one or more value of the same type as /// command's arguments. /// /// [`name`] will become the name of the parameter (it would appear in the /// generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. If left `none`, - will coincide with /// `name` parameter. /// /// [`aliasSourceName`] defines name of the alias source that must be used to /// auto-resolve this parameter's value. `none` means that parameter will be /// recorded as-is, any other value (either "weapon", "color", "feature", /// "entity" or some kind of custom alias source name) will make values prefixed /// with "$" to be resolved as custom aliases. /// In case auto-resolving is used, value will be recorded as a `HasTable` with /// two fields: "alias" - value provided by user and (in case "$" prefix was /// used) "value" - actual resolved value of an alias. /// If alias has failed to be resolved, `none` will be stored as a value. public final function ParamTextList( BaseText name, optional BaseText variableName, optional BaseText aliasSourceName ) { local Command.Parameter newParameterValue; if (name == none) { return; } newParameterValue = NewParameter(name, CPT_Text, true, variableName); if (aliasSourceName != none) { newParameterValue.aliasSourceName = aliasSourceName.Copy(); } PushParameter(newParameterValue); } /// Adds new remainder parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// Remainder parameter is a special parameter that will simply consume all /// remaining command's input as-is. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamRemainder(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_Remainder, false, variableName)); } } /// Adds new JSON object parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamObject(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_Object, false, variableName)); } } /// Adds new JSON object list parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// List parameters expect user to enter one or more value of the same type as /// command's arguments. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamObjectList(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_Object, true, variableName)); } } /// Adds new JSON array parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// List parameters expect user to enter one or more value of the same type as /// command's arguments. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamArray(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_Array, false, variableName)); } } /// Adds new JSON array list parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// List parameters expect user to enter one or more value of the same type as /// command's arguments. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamArrayList(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_Array, true, variableName)); } } /// Adds new JSON value parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// List parameters expect user to enter one or more value of the same type as /// command's arguments. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamJSON(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_JSON, false, variableName)); } } /// Adds new JSON value list parameter (required or optional depends on whether /// `OptionalParams()` call happened) to the currently selected /// sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// List parameters expect user to enter one or more value of the same type as /// command's arguments. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamJSONList(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_JSON, true, variableName)); } } /// Adds new parameter that defines a set of players (required or optional /// depends on whether `OptionalParams()` call happened) to the currently /// selected sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// List parameters expect user to enter one or more value of the same type as /// command's arguments. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamPlayers(BaseText name, optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_PLAYERS, false, variableName)); } } /// Adds new parameter that defines a list of sets of players (required or /// optional depends on whether `OptionalParams()` call happened) to /// the currently selected sub-command / option. /// /// Only fails if provided `name` is `none`. /// /// List parameters expect user to enter one or more value of the same type as /// command's arguments. /// /// [`name`] will become the name of the parameter /// (it would appear in the generated "help" command info). /// /// [`variableName`] will become key for this parameter's value in `HashTable` /// after user's command input is parsed. /// If left `none`, - will coincide with `name` parameter. public final function ParamPlayersList(BaseText name,optional BaseText variableName) { if (name != none) { PushParameter(NewParameter(name, CPT_PLAYERS, true, variableName)); } } // Find index of sub-command with a given name `name` in `subcommands`. // `-1` if there's not sub-command with such name. // Case-sensitive. private final function int FindSubCommandIndex(BaseText name) { local int i; if (name == none) { return -1; } for (i = 0; i < subcommands.length; i += 1) { if (name.Compare(subcommands[i].name)) { return i; } } return -1; } // Find index of option with a given name `name` in `options`. // `-1` if there's not sub-command with such name. // Case-sensitive. private final function int FindOptionIndex(BaseText longName) { local int i; if (longName == none) { return -1; } for (i = 0; i < options.length; i += 1) { if (longName.Compare(options[i].longName)) { return i; } } return -1; } // Creates an empty selection record for subcommand or option with name (long name) `name`. // Doe not check whether subcommand/option with that name already exists. // Copies passed `name`, assumes that it is not `none`. private final function MakeEmptySelection(BaseText name, bool selectedOption) { selectedItemIsOption = selectedOption; selectedItemName = name.Copy(); selectedDescription = none; selectedParameterArray.length = 0; selectionIsOptional = false; } // Select option with a given long name `longName` from `options`. // If there is no option with specified `longName` in prepared data - creates new record in // selection, otherwise copies previously saved data. // Automatically saves previously selected data into prepared data. // Copies `name` if it has to create new record. private final function SelectOption(BaseText longName) { local int optionIndex; if (longName == none) { return; } if (selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(longName)) { return; } RecordSelection(); optionIndex = FindOptionIndex(longName); if (optionIndex < 0) { MakeEmptySelection(longName, true); return; } // Load appropriate prepared data, if it exists for // option with long name `longName` selectedItemIsOption = true; selectedItemName = options[optionIndex].longName; selectedDescription = options[optionIndex].description; selectionIsOptional = optionsIsOptional[optionIndex] > 0; if (selectionIsOptional) { selectedParameterArray = options[optionIndex].optional; } else { selectedParameterArray = options[optionIndex].required; } } // Saves currently selected data into prepared data. private final function RecordSelection() { if (selectedItemName == none) { return; } if (selectedItemIsOption) { RecordSelectedOption(); } else { RecordSelectedSubCommand(); } } // Saves selected sub-command into prepared records. // Assumes that command and not an option is selected. private final function RecordSelectedSubCommand() { local int selectedSubCommandIndex; local Command.SubCommand newSubcommand; if (selectedItemName == none) { return; } selectedSubCommandIndex = FindSubCommandIndex(selectedItemName); if (selectedSubCommandIndex < 0) { selectedSubCommandIndex = subcommands.length; subcommands[selectedSubCommandIndex] = newSubcommand; } subcommands[selectedSubCommandIndex].name = selectedItemName; subcommands[selectedSubCommandIndex].description = selectedDescription; if (selectionIsOptional) { subcommands[selectedSubCommandIndex].optional = selectedParameterArray; subcommandsIsOptional[selectedSubCommandIndex] = 1; } else { subcommands[selectedSubCommandIndex].required = selectedParameterArray; subcommandsIsOptional[selectedSubCommandIndex] = 0; } } // Saves currently selected option into prepared records. // Assumes that option and not an command is selected. private final function RecordSelectedOption() { local int selectedOptionIndex; local Command.Option newOption; if (selectedItemName == none) { return; } selectedOptionIndex = FindOptionIndex(selectedItemName); if (selectedOptionIndex < 0) { selectedOptionIndex = options.length; options[selectedOptionIndex] = newOption; } options[selectedOptionIndex].longName = selectedItemName; options[selectedOptionIndex].description = selectedDescription; if (selectionIsOptional) { options[selectedOptionIndex].optional = selectedParameterArray; optionsIsOptional[selectedOptionIndex] = 1; } else { options[selectedOptionIndex].required = selectedParameterArray; optionsIsOptional[selectedOptionIndex] = 0; } } // Validates names (printing errors in case of failure) for the option. // Long name must be at least 2 characters long. // Short name must be either: // 1. exactly one character long; // 2. `none`, which leads to deriving `shortName` from `longName` // as a first character. // Anything else will result in logging a failure and rejection of // the option altogether. // Returns `none` if validation failed and chosen short name otherwise // (if `shortName` was used for it - it's value will be copied). private final function BaseText.Character GetValidShortName( BaseText longName, BaseText shortName ) { // Validate `longName` if (longName == none) { return _.text.GetInvalidCharacter(); } if (longName.GetLength() < 2) { _.logger.Auto(errLongNameTooShort).ArgClass(class).Arg(longName.Copy()); return _.text.GetInvalidCharacter(); } // Validate `shortName`, // deriving if from `longName` if necessary & possible if (shortName == none) { return longName.GetCharacter(0); } if (shortName.IsEmpty() || shortName.GetLength() > 1) { _.logger.Auto(errShortNameTooLong).ArgClass(class).Arg(longName.Copy()); return _.text.GetInvalidCharacter(); } return shortName.GetCharacter(0); } // Checks that if any option record has a long/short name from a given pair of // names (`longName`, `shortName`), then it also has another one. // // i.e. we cannot have several options with identical names: // (--silent, -s) and (--sick, -s). private final function bool VerifyNoOptionNamingConflict( BaseText longName, BaseText.Character shortName ) { local int i; local bool sameShortNames, sameLongNames; // To make sure we will search through the up-to-date `options`, // record selection into prepared records. RecordSelection(); for (i = 0; i < options.length; i += 1) { sameShortNames = _.text.AreEqual(shortName, options[i].shortName); sameLongNames = longName.Compare(options[i].longName); if (sameLongNames && !sameShortNames) { _.logger.Auto(warnSameLongName).ArgClass(class).Arg(longName.Copy()); return true; } if (!sameLongNames && sameShortNames) { _.logger.Auto(warnSameLongName).ArgClass(class).Arg(_.text.FromCharacter(shortName)); return true; } } return false; } defaultproperties { errLongNameTooShort = (l=LOG_Error,m="Command `%1` is trying to register an option with a name that is way too short (<2 characters). Option will be discarded: %2") errShortNameTooLong = (l=LOG_Error,m="Command `%1` is trying to register an option with a short name that doesn't consist of just one character. Option will be discarded: %2") warnSameLongName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same long name \"%2\", but different short names. This should not happen, do not expect correct behavior.") warnSameShortName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same short name \"%2\", but different long names. This should not have happened, do not expect correct behavior.") }