diff --git a/sources/Commands/BuiltInCommands/ACommandHelp.uc b/sources/Commands/BuiltInCommands/ACommandHelp.uc index bd0c0f9..27bf947 100644 --- a/sources/Commands/BuiltInCommands/ACommandHelp.uc +++ b/sources/Commands/BuiltInCommands/ACommandHelp.uc @@ -268,6 +268,7 @@ private final function PrintParameter(ConsoleWriter cout, Parameter parameter) cout.UseColor(_.color.typeNumber); break; case CPT_Text: + case CPT_Remainder: cout.UseColor(_.color.typeString); break; case CPT_Object: diff --git a/sources/Commands/Command.uc b/sources/Commands/Command.uc index de74b49..9d3fd1b 100644 --- a/sources/Commands/Command.uc +++ b/sources/Commands/Command.uc @@ -61,11 +61,19 @@ enum ErrorType */ enum ParameterType { + // Parses into `BoolBox` CPT_Boolean, + // Parses into `IntBox` CPT_Integer, + // Parses into `FloatBox` CPT_Number, + // Parses into `Text` CPT_Text, + // Special parameter that consumes the rest of the input into `Text` + CPT_Remainder, + // Parses into `AssociativeArray` CPT_Object, + // Parses into `DynamicArray` CPT_Array }; diff --git a/sources/Commands/CommandDataBuilder.uc b/sources/Commands/CommandDataBuilder.uc index d3af681..e5b46d3 100644 --- a/sources/Commands/CommandDataBuilder.uc +++ b/sources/Commands/CommandDataBuilder.uc @@ -812,6 +812,33 @@ public final function CommandDataBuilder ParamTextList( return self; } +/** + * Adds new remainder parameter (required or optional depends on whether + * `RequireTarget()` call happened) to the currently selected + * sub-command / option. + * + * Only fails if provided `name` is `none`. + * + * @param name Name of the parameter, will be copied + * (as it would appear in the generated help info). + * @param variableName Name of the variable that will store this + * parameter's value in `AssociativeArray` after user's command input + * is parsed. Provided value will be copied. + * If left `none`, - will coincide with `name` parameter. + * @return Returns the caller `CommandDataBuilder` to allow for + * method chaining. + */ +public final function CommandDataBuilder ParamRemainder( + Text name, + optional Text variableName) +{ + if (name == none) { + return self; + } + PushParameter(NewParameter(name, CPT_Remainder, false, variableName)); + return self; +} + /** * Adds new object parameter (required or optional depends on whether * `RequireTarget()` call happened) to the currently selected diff --git a/sources/Commands/CommandParser.uc b/sources/Commands/CommandParser.uc index 3a8dd38..7cdd385 100644 --- a/sources/Commands/CommandParser.uc +++ b/sources/Commands/CommandParser.uc @@ -54,7 +54,8 @@ class CommandParser extends AcediaObject * * Finally, to allow users to specify options at any point in command, * we call `TryParsingOptions()` at the beginning of every - * `ParseSingleValue()`, since option definition can appear at any place + * `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. @@ -397,8 +398,13 @@ private final function bool ParseSingleValue( AssociativeArray parsedParameters, Command.Parameter expectedParameter) { - // Before parsing a value we need to check if user has specified any - // options instead. + // 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); + } + // 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. @@ -426,6 +432,9 @@ private final function bool ParseSingleValue( 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); } @@ -549,6 +558,25 @@ private final function bool ParseTextValue( return true; } +// Assumes `commandParser` and `parsedParameters` are not `none`. +// Parses a single `Text` value into given `parsedParameters` +// associative array, consuming all remaining contents. +private final function bool ParseRemainderValue( + AssociativeArray parsedParameters, + Command.Parameter expectedParameter) +{ + local string textValue; + // TODO: use parsing methods into `Text` + // (needs some work for reading formatting `string`s from `Text` objects) + commandParser.Skip().MUntilS(textValue); + if (!commandParser.Ok()) { + return false; + } + RecordParameter(parsedParameters, expectedParameter, + _.text.FromFormattedString(textValue)); + return true; +} + // Assumes `commandParser` and `parsedParameters` are not `none`. // Parses a single JSON object into given `parsedParameters` // associative array. @@ -782,7 +810,10 @@ private final function bool AddOptionByCharacter( private final function bool ParseOptionParameters(Command.Option pickedOption) { local AssociativeArray optionParameters; - if (currentTargetIsOption && currentTarget != CPT_ExtraParameter) { + // 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; } diff --git a/sources/Commands/Tests/MockCommandB.uc b/sources/Commands/Tests/MockCommandB.uc index b8e584c..39db901 100644 --- a/sources/Commands/Tests/MockCommandB.uc +++ b/sources/Commands/Tests/MockCommandB.uc @@ -41,6 +41,8 @@ protected function BuildData(CommandDataBuilder builder) .OptionalParams() .ParamNumberList(P("numeric list"), P("list")) .ParamBoolean(P("maybe")); + builder.Option(P("remainder")) + .ParamRemainder(P("everything")); } defaultproperties diff --git a/sources/Commands/Tests/TEST_Command.uc b/sources/Commands/Tests/TEST_Command.uc index ac4ffcd..1b233ce 100644 --- a/sources/Commands/Tests/TEST_Command.uc +++ b/sources/Commands/Tests/TEST_Command.uc @@ -1,6 +1,6 @@ /** * Set of tests for `Command` class. - * Copyright 2020 Anton Tarasenko + * Copyright 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -23,7 +23,7 @@ class TEST_Command extends TestCase var string queryASuccess1, queryASuccess2, queryASuccess3, queryASuccess4; var string queryAFailure1, queryAFailure2; -var string queryBSuccess1, queryBSuccess2; +var string queryBSuccess1, queryBSuccess2, queryBSuccess3; var string queryBFailure1, queryBFailure2, queryBFailure3; var string queryBFailureUnknownOptionLong, queryBFailureUnknownOptionShort; var string queryBFailureUnused; @@ -59,6 +59,7 @@ protected static function Test_MockB() SubTest_MockBFailed(); SubTest_MockBQ1(); SubTest_MockBQ2(); + SubTest_MockBQ3Remainder(); } protected static function Test_CommandCallErrors() @@ -381,6 +382,26 @@ protected static function SubTest_MockBQ2() TEST_ExpectTrue(IntBox(subArray.GetItem(0)).Get() == 8); } +protected static function SubTest_MockBQ3Remainder() +{ + local CommandCall result; + local DynamicArray subArray; + local AssociativeArray options, subObject; + Issue("Cannot parse command queries with `CPT_Remainder` type parameters."); + result = class'MockCommandB'.static.GetInstance() + .ProcessInput(PRS(default.queryBSuccess3), none); + TEST_ExpectTrue(result.GetParameters().GetLength() == 1); + subArray = DynamicArray(result.GetParameters().GetItem(P("list"))); + TEST_ExpectTrue(FloatBox(subArray.GetItem(0)).Get() == 3); + TEST_ExpectTrue(FloatBox(subArray.GetItem(1)).Get() == -76); + options = result.GetOptions(); + TEST_ExpectTrue(options.GetLength() == 1); + TEST_ExpectTrue(options.HasKey(P("remainder"))); + subObject = AssociativeArray(options.GetItem(P("remainder"))); + TEST_ExpectTrue( Text(subObject.GetItem(P("everything"))).ToPlainString() + == "--type \"value\" -va 8 -sV --forced -T \"\" 32"); +} +// [1, 2, 3, 6] defaultproperties { caseName = "Command" @@ -395,6 +416,7 @@ defaultproperties queryBSuccess1 = "[7, null] --values 1 3 5 2 4 text" queryBSuccess2 = "do --type \"value\" -va 8 -sV --forced -T \"\" " + queryBSuccess3 = "do 3 -76 -r --type \"value\" -va 8 -sV --forced -T \"\" 32" // long then same as short queryBFailure1 = "[] 8 -tv 13" queryBFailure2 = "do 7 5 -sfV --forced yes"