Add support for compound arguments for flags

Signed-off-by: Irmo van den Berge <irmo.vandenberge@ziggo.nl>
This commit is contained in:
Irmo van den Berge 2020-12-23 17:53:16 +01:00 committed by Alexander Söderberg
parent 23c0ad77f9
commit a978adc79f
3 changed files with 343 additions and 174 deletions

View file

@ -55,6 +55,7 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Queue; import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -552,6 +553,11 @@ public final class CommandTree<C> {
final @NonNull Queue<@NonNull String> commandQueue, final @NonNull Queue<@NonNull String> commandQueue,
final @NonNull Node<@Nullable CommandArgument<C, ?>> child final @NonNull Node<@Nullable CommandArgument<C, ?>> child
) { ) {
/* If argument has no value associated, break out early */
if (child.getValue() == null) {
return Collections.emptyList();
}
/* When we get in here, we need to treat compound arguments a little differently */ /* When we get in here, we need to treat compound arguments a little differently */
if (child.getValue() instanceof CompoundArgument) { if (child.getValue() instanceof CompoundArgument) {
@SuppressWarnings("unchecked") final CompoundArgument<?, C, ?> compoundArgument = (CompoundArgument<?, C, ?>) child @SuppressWarnings("unchecked") final CompoundArgument<?, C, ?> compoundArgument = (CompoundArgument<?, C, ?>) child
@ -566,18 +572,27 @@ public final class CommandTree<C> {
commandContext.store("__parsing_argument__", i + 2); commandContext.store("__parsing_argument__", i + 2);
} }
} }
} else if (child.getValue() instanceof FlagArgument) { } else if (child.getValue().getParser() instanceof FlagArgument.FlagArgumentParser) {
/* Remove all but last */
while (commandQueue.size() > 1) { /*
commandContext.store(FlagArgument.FLAG_META, commandQueue.remove()); * Use the flag argument parser to deduce what flag is being suggested right now
* If empty, then no flag value is being typed, and the different flag options should
* be suggested instead.
*
* Note: the method parseCurrentFlag() will remove all but the last element from
* the queue!
*/
@SuppressWarnings("unchecked")
FlagArgument.FlagArgumentParser<C> parser = (FlagArgument.FlagArgumentParser<C>) child.getValue().getParser();
Optional<String> lastFlag = parser.parseCurrentFlag(commandContext, commandQueue);
if (lastFlag.isPresent()) {
commandContext.store(FlagArgument.FLAG_META, lastFlag.get());
} }
} else if (child.getValue() != null } else if (GenericTypeReflector.erase(child.getValue().getValueType().getType()).isArray()) {
&& GenericTypeReflector.erase(child.getValue().getValueType().getType()).isArray()) {
while (commandQueue.size() > 1) { while (commandQueue.size() > 1) {
commandQueue.remove(); commandQueue.remove();
} }
} else if (child.getValue() != null } else if (commandQueue.size() <= child.getValue().getParser().getRequestedArgumentCount()) {
&& commandQueue.size() <= child.getValue().getParser().getRequestedArgumentCount()) {
for (int i = 0; i < child.getValue().getParser().getRequestedArgumentCount() - 1 for (int i = 0; i < child.getValue().getParser().getRequestedArgumentCount() - 1
&& commandQueue.size() > 1; i++) { && commandQueue.size() > 1; i++) {
commandContext.store( commandContext.store(
@ -587,57 +602,53 @@ public final class CommandTree<C> {
} }
} }
if (child.getValue() != null) { if (commandQueue.isEmpty()) {
if (commandQueue.isEmpty()) { return Collections.emptyList();
return Collections.emptyList(); } else if (child.isLeaf() && commandQueue.size() < 2) {
} else if (child.isLeaf() && commandQueue.size() < 2) {
commandContext.setCurrentArgument(child.getValue());
return child.getValue().getSuggestionsProvider().apply(commandContext, commandQueue.peek());
} else if (child.isLeaf()) {
if (child.getValue() instanceof CompoundArgument) {
final String last = ((LinkedList<String>) commandQueue).getLast();
commandContext.setCurrentArgument(child.getValue());
return child.getValue().getSuggestionsProvider().apply(commandContext, last);
}
return Collections.emptyList();
} else if (commandQueue.peek().isEmpty()) {
commandContext.setCurrentArgument(child.getValue());
return child.getValue().getSuggestionsProvider().apply(commandContext, commandQueue.remove());
}
// Store original input command queue before the parsers below modify it
final Queue<String> commandQueueOriginal = new LinkedList<>(commandQueue);
// START: Preprocessing
final ArgumentParseResult<Boolean> preParseResult = child.getValue().preprocess(
commandContext,
commandQueue
);
final boolean preParseSuccess = !preParseResult.getFailure().isPresent()
&& preParseResult.getParsedValue().orElse(false);
// END: Preprocessing
if (preParseSuccess) {
// START: Parsing
commandContext.setCurrentArgument(child.getValue());
final ArgumentParseResult<?> result = child.getValue().getParser().parse(commandContext, commandQueue);
if (result.getParsedValue().isPresent() && !commandQueue.isEmpty()) {
commandContext.store(child.getValue().getName(), result.getParsedValue().get());
return this.getSuggestions(commandContext, commandQueue, child);
}
// END: Parsing
}
// Restore original command input queue
commandQueue.clear();
commandQueue.addAll(commandQueueOriginal);
// Fallback: use suggestion provider of argument
commandContext.setCurrentArgument(child.getValue()); commandContext.setCurrentArgument(child.getValue());
return child.getValue().getSuggestionsProvider().apply(commandContext, stringOrEmpty(commandQueue.peek())); return child.getValue().getSuggestionsProvider().apply(commandContext, commandQueue.peek());
} else if (child.isLeaf()) {
if (child.getValue() instanceof CompoundArgument) {
final String last = ((LinkedList<String>) commandQueue).getLast();
commandContext.setCurrentArgument(child.getValue());
return child.getValue().getSuggestionsProvider().apply(commandContext, last);
}
return Collections.emptyList();
} else if (commandQueue.peek().isEmpty()) {
commandContext.setCurrentArgument(child.getValue());
return child.getValue().getSuggestionsProvider().apply(commandContext, commandQueue.remove());
} }
return Collections.emptyList(); // Store original input command queue before the parsers below modify it
final Queue<String> commandQueueOriginal = new LinkedList<>(commandQueue);
// START: Preprocessing
final ArgumentParseResult<Boolean> preParseResult = child.getValue().preprocess(
commandContext,
commandQueue
);
final boolean preParseSuccess = !preParseResult.getFailure().isPresent()
&& preParseResult.getParsedValue().orElse(false);
// END: Preprocessing
if (preParseSuccess) {
// START: Parsing
commandContext.setCurrentArgument(child.getValue());
final ArgumentParseResult<?> result = child.getValue().getParser().parse(commandContext, commandQueue);
if (result.getParsedValue().isPresent() && !commandQueue.isEmpty()) {
commandContext.store(child.getValue().getName(), result.getParsedValue().get());
return this.getSuggestions(commandContext, commandQueue, child);
}
// END: Parsing
}
// Restore original command input queue
commandQueue.clear();
commandQueue.addAll(commandQueueOriginal);
// Fallback: use suggestion provider of argument
commandContext.setCurrentArgument(child.getValue());
return child.getValue().getSuggestionsProvider().apply(commandContext, stringOrEmpty(commandQueue.peek()));
} }
private @NonNull String stringOrEmpty(final @Nullable String string) { private @NonNull String stringOrEmpty(final @Nullable String string) {

View file

@ -40,6 +40,7 @@ import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Optional;
import java.util.Queue; import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.function.BiFunction; import java.util.function.BiFunction;
@ -104,131 +105,66 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
} }
@Override @Override
@SuppressWarnings({"unchecked", "rawtypes"})
public @NonNull ArgumentParseResult<@NonNull Object> parse( public @NonNull ArgumentParseResult<@NonNull Object> parse(
final @NonNull CommandContext<@NonNull C> commandContext, final @NonNull CommandContext<@NonNull C> commandContext,
final @NonNull Queue<@NonNull String> inputQueue final @NonNull Queue<@NonNull String> inputQueue
) { ) {
/* final FlagParser parser = new FlagParser();
This argument must necessarily be the last so we can just consume all remaining input. This argument type return parser.parse(commandContext, inputQueue);
is similar to a greedy string in that sense. But, we need to keep all flag logic contained to the parser }
*/
final Set<CommandFlag<?>> parsedFlags = new HashSet<>();
CommandFlag<?> currentFlag = null;
for (final @NonNull String string : inputQueue) { /**
if (string.startsWith("-") && currentFlag == null) { * Parse command input to figure out what flag is currently being
if (string.startsWith("--")) { * typed at the end of the input queue. If no flag value is being
final String flagName = string.substring(2); * inputed, returns {@link Optional#empty()}.<br>
for (final CommandFlag<?> flag : this.flags) { * <br>
if (flagName.equalsIgnoreCase(flag.getName())) { * Will consume all but the last element from the input queue.
currentFlag = flag; *
break; * @param commandContext Command context
} * @param inputQueue The input queue of arguments
} * @return current flag being typed, or <i>empty()</i> if none is
} else { */
final String flagName = string.substring(1); public @NonNull Optional<String> parseCurrentFlag(
if (flagName.length() > 1) { final @NonNull CommandContext<@NonNull C> commandContext,
boolean oneAdded = false; final @NonNull Queue<@NonNull String> inputQueue
/* This is a multi-alias flag, find all flags that apply */ ) {
for (final CommandFlag<?> flag : this.flags) { /* If empty, nothing to do */
if (flag.getCommandArgument() != null) { if (inputQueue.isEmpty()) {
continue; return Optional.empty();
} }
for (final String alias : flag.getAliases()) {
if (flagName.toLowerCase(Locale.ENGLISH).contains(alias.toLowerCase(Locale.ENGLISH))) { /* Before parsing, retrieve the last known input of the queue */
if (parsedFlags.contains(flag)) { String lastInputValue = "";
return ArgumentParseResult.failure(new FlagParseException( for (String input : inputQueue) {
string, lastInputValue = input;
FailureReason.DUPLICATE_FLAG, }
commandContext
)); /* Parse, but ignore the result of parsing */
} final FlagParser parser = new FlagParser();
parsedFlags.add(flag); parser.parse(commandContext, inputQueue);
commandContext.flags().addPresenceFlag(flag);
oneAdded = true; /*
break; * Remove all but the last element from the command input queue
} * If the parser parsed the entire queue, restore the last typed
} * input obtained earlier.
} */
/* We need to parse at least one flag */ if (inputQueue.isEmpty()) {
if (!oneAdded) { inputQueue.add(lastInputValue);
return ArgumentParseResult.failure(new FlagParseException( } else {
string, while (inputQueue.size() > 1) {
FailureReason.NO_FLAG_STARTED, inputQueue.remove();
commandContext
));
}
continue;
} else {
for (final CommandFlag<?> flag : this.flags) {
for (final String alias : flag.getAliases()) {
if (alias.equalsIgnoreCase(flagName)) {
currentFlag = flag;
break;
}
}
}
}
}
if (currentFlag == null) {
return ArgumentParseResult.failure(new FlagParseException(
string,
FailureReason.UNKNOWN_FLAG,
commandContext
));
} else if (parsedFlags.contains(currentFlag)) {
return ArgumentParseResult.failure(new FlagParseException(
string,
FailureReason.DUPLICATE_FLAG,
commandContext
));
}
parsedFlags.add(currentFlag);
if (currentFlag.getCommandArgument() == null) {
/* It's a presence flag */
commandContext.flags().addPresenceFlag(currentFlag);
/* We don't want to parse a value for this flag */
currentFlag = null;
}
} else {
if (currentFlag == null) {
return ArgumentParseResult.failure(new FlagParseException(
string,
FailureReason.NO_FLAG_STARTED,
commandContext
));
} else {
final ArgumentParseResult<?> result =
((CommandArgument) currentFlag.getCommandArgument())
.getParser()
.parse(
commandContext,
new LinkedList<>(Collections.singletonList(string))
);
if (result.getFailure().isPresent()) {
return ArgumentParseResult.failure(result.getFailure().get());
} else if (result.getParsedValue().isPresent()) {
final CommandFlag erasedFlag = currentFlag;
final Object value = result.getParsedValue().get();
commandContext.flags().addValueFlag(erasedFlag, value);
currentFlag = null;
} else {
throw new IllegalStateException("Neither result or value were present. Panicking.");
}
}
} }
} }
if (currentFlag != null) {
return ArgumentParseResult.failure(new FlagParseException( /*
currentFlag.getName(), * Map to name of the flag.
FailureReason.MISSING_ARGUMENT, *
commandContext * Note: legacy API made it that FLAG_META stores not the flag name,
)); * but the - or -- prefixed name or alias of the flag(s) instead.
} * This can be removed in the future.
/* We've consumed everything */ */
inputQueue.clear(); //return parser.currentFlagBeingParsed.map(CommandFlag::getName);
return ArgumentParseResult.success(FLAG_PARSE_RESULT_OBJECT); return parser.currentFlagNameBeingParsed;
} }
@Override @Override
@ -342,6 +278,166 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
return suggestions(commandContext, input); return suggestions(commandContext, input);
} }
/**
* Helper class to parse the command input queue into flags
* and flag values. On failure the intermediate results
* can be obtained, which are used for providing suggestions.
*/
private class FlagParser {
/** The current flag whose value is being parsed */
@SuppressWarnings("unused")
private Optional<CommandFlag<?>> currentFlagBeingParsed = Optional.empty();
/**
* The name of the current flag being parsed, can be obsoleted in the future.
* This name includes the - or -- prefix.
*/
private Optional<String> currentFlagNameBeingParsed = Optional.empty();
@SuppressWarnings({"unchecked", "rawtypes"})
public @NonNull ArgumentParseResult<@NonNull Object> parse(
final @NonNull CommandContext<@NonNull C> commandContext,
final @NonNull Queue<@NonNull String> inputQueue
) {
/*
This argument must necessarily be the last so we can just consume all remaining input. This argument type
is similar to a greedy string in that sense. But, we need to keep all flag logic contained to the parser
*/
final Set<CommandFlag<?>> parsedFlags = new HashSet<>();
CommandFlag<?> currentFlag = null;
String currentFlagName = null;
String string;
while ((string = inputQueue.peek()) != null) {
/* No longer typing the value of the current flag */
this.currentFlagBeingParsed = Optional.empty();
this.currentFlagNameBeingParsed = Optional.empty();
/* Parse next flag name to set */
if (string.startsWith("-") && currentFlag == null) {
/* Remove flag argument from input queue */
inputQueue.poll();
if (string.startsWith("--")) {
final String flagName = string.substring(2);
for (final CommandFlag<?> flag : FlagArgumentParser.this.flags) {
if (flagName.equalsIgnoreCase(flag.getName())) {
currentFlag = flag;
currentFlagName = string;
break;
}
}
} else {
final String flagName = string.substring(1);
if (flagName.length() > 1) {
boolean oneAdded = false;
/* This is a multi-alias flag, find all flags that apply */
for (final CommandFlag<?> flag : FlagArgumentParser.this.flags) {
if (flag.getCommandArgument() != null) {
continue;
}
for (final String alias : flag.getAliases()) {
if (flagName.toLowerCase(Locale.ENGLISH).contains(alias.toLowerCase(Locale.ENGLISH))) {
if (parsedFlags.contains(flag)) {
return ArgumentParseResult.failure(new FlagParseException(
string,
FailureReason.DUPLICATE_FLAG,
commandContext
));
}
parsedFlags.add(flag);
commandContext.flags().addPresenceFlag(flag);
oneAdded = true;
break;
}
}
}
/* We need to parse at least one flag */
if (!oneAdded) {
return ArgumentParseResult.failure(new FlagParseException(
string,
FailureReason.NO_FLAG_STARTED,
commandContext
));
}
continue;
} else {
for (final CommandFlag<?> flag : FlagArgumentParser.this.flags) {
for (final String alias : flag.getAliases()) {
if (alias.equalsIgnoreCase(flagName)) {
currentFlag = flag;
currentFlagName = string;
break;
}
}
}
}
}
if (currentFlag == null) {
return ArgumentParseResult.failure(new FlagParseException(
string,
FailureReason.UNKNOWN_FLAG,
commandContext
));
} else if (parsedFlags.contains(currentFlag)) {
return ArgumentParseResult.failure(new FlagParseException(
string,
FailureReason.DUPLICATE_FLAG,
commandContext
));
}
parsedFlags.add(currentFlag);
if (currentFlag.getCommandArgument() == null) {
/* It's a presence flag */
commandContext.flags().addPresenceFlag(currentFlag);
/* We don't want to parse a value for this flag */
currentFlag = null;
}
} else {
if (currentFlag == null) {
return ArgumentParseResult.failure(new FlagParseException(
string,
FailureReason.NO_FLAG_STARTED,
commandContext
));
} else {
/* Mark this flag as the one currently being typed */
this.currentFlagBeingParsed = Optional.of(currentFlag);
this.currentFlagNameBeingParsed = Optional.of(currentFlagName);
final ArgumentParseResult<?> result =
((CommandArgument) currentFlag.getCommandArgument())
.getParser()
.parse(
commandContext,
inputQueue
);
if (result.getFailure().isPresent()) {
return ArgumentParseResult.failure(result.getFailure().get());
} else if (result.getParsedValue().isPresent()) {
final CommandFlag erasedFlag = currentFlag;
final Object value = result.getParsedValue().get();
commandContext.flags().addValueFlag(erasedFlag, value);
currentFlag = null;
} else {
throw new IllegalStateException("Neither result or value were present. Panicking.");
}
}
}
}
/* Queue ran out while a flag argument needs to be parsed still */
if (currentFlag != null) {
return ArgumentParseResult.failure(new FlagParseException(
currentFlag.getName(),
FailureReason.MISSING_ARGUMENT,
commandContext
));
}
/* We've consumed everything */
return ArgumentParseResult.success(FLAG_PARSE_RESULT_OBJECT);
}
}
} }
/** /**

View file

@ -23,11 +23,14 @@
// //
package cloud.commandframework; package cloud.commandframework;
import cloud.commandframework.arguments.compound.ArgumentTriplet;
import cloud.commandframework.arguments.standard.BooleanArgument; import cloud.commandframework.arguments.standard.BooleanArgument;
import cloud.commandframework.arguments.standard.EnumArgument; import cloud.commandframework.arguments.standard.EnumArgument;
import cloud.commandframework.arguments.standard.IntegerArgument; import cloud.commandframework.arguments.standard.IntegerArgument;
import cloud.commandframework.arguments.standard.StringArgument; import cloud.commandframework.arguments.standard.StringArgument;
import cloud.commandframework.types.tuples.Pair; import cloud.commandframework.types.tuples.Pair;
import cloud.commandframework.types.tuples.Triplet;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -87,6 +90,16 @@ public class CommandSuggestionsTest {
.flag(manager.flagBuilder("third").withAliases("t")) .flag(manager.flagBuilder("third").withAliases("t"))
.build()); .build());
manager.command(manager.commandBuilder("flags3")
.flag(manager.flagBuilder("compound")
.withArgument(ArgumentTriplet.of(manager, "triplet",
Triplet.of("x", "y", "z"),
Triplet.of(int.class, int.class, int.class))
.simple()))
.flag(manager.flagBuilder("presence").withAliases("p"))
.flag(manager.flagBuilder("single")
.withArgument(IntegerArgument.of("value"))));
manager.command(manager.commandBuilder("numbers").argument(IntegerArgument.of("num"))); manager.command(manager.commandBuilder("numbers").argument(IntegerArgument.of("num")));
manager.command(manager.commandBuilder("numberswithfollowingargument").argument(IntegerArgument.of("num")) manager.command(manager.commandBuilder("numberswithfollowingargument").argument(IntegerArgument.of("num"))
.argument(BooleanArgument.of("another_argument"))); .argument(BooleanArgument.of("another_argument")));
@ -211,6 +224,55 @@ public class CommandSuggestionsTest {
final String input6 = "flags2 -f -s"; final String input6 = "flags2 -f -s";
final List<String> suggestions6 = manager.suggest(new TestCommandSender(), input6); final List<String> suggestions6 = manager.suggest(new TestCommandSender(), input6);
Assertions.assertEquals(Arrays.asList("-st", "-s"), suggestions6); Assertions.assertEquals(Arrays.asList("-st", "-s"), suggestions6);
/* When an incorrect flag is specified, should resolve to listing flags */
final String input7 = "flags2 --invalid ";
final List<String> suggestions7 = manager.suggest(new TestCommandSender(), input7);
Assertions.assertEquals(Arrays.asList("--first", "--second", "--third", "-f", "-s", "-t"), suggestions7);
}
@Test
void testCompoundFlags() {
final String input = "flags3 ";
final List<String> suggestions = manager.suggest(new TestCommandSender(), input);
Assertions.assertEquals(Arrays.asList("--compound", "--presence", "--single", "-p"), suggestions);
final String input2 = "flags3 --c";
final List<String> suggestions2 = manager.suggest(new TestCommandSender(), input2);
Assertions.assertEquals(Arrays.asList("--compound"), suggestions2);
final String input3 = "flags3 --compound ";
final List<String> suggestions3 = manager.suggest(new TestCommandSender(), input3);
Assertions.assertEquals(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"), suggestions3);
final String input4 = "flags3 --compound 1";
final List<String> suggestions4 = manager.suggest(new TestCommandSender(), input4);
Assertions.assertEquals(Arrays.asList("1", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19"), suggestions4);
final String input5 = "flags3 --compound 22 ";
final List<String> suggestions5 = manager.suggest(new TestCommandSender(), input5);
Assertions.assertEquals(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"), suggestions5);
final String input6 = "flags3 --compound 22 1";
final List<String> suggestions6 = manager.suggest(new TestCommandSender(), input6);
Assertions.assertEquals(Arrays.asList("1", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19"), suggestions6);
/* We've typed compound already, so that flag should be omitted from the suggestions */
final String input7 = "flags3 --compound 22 33 44 ";
final List<String> suggestions7 = manager.suggest(new TestCommandSender(), input7);
Assertions.assertEquals(Arrays.asList("--presence", "--single", "-p"), suggestions7);
final String input8 = "flags3 --compound 22 33 44 --pres";
final List<String> suggestions8 = manager.suggest(new TestCommandSender(), input8);
Assertions.assertEquals(Arrays.asList("--presence"), suggestions8);
final String input9 = "flags3 --compound 22 33 44 --presence ";
final List<String> suggestions9 = manager.suggest(new TestCommandSender(), input9);
Assertions.assertEquals(Arrays.asList("--single"), suggestions9);
final String input10 = "flags3 --compound 22 33 44 --single ";
final List<String> suggestions10 = manager.suggest(new TestCommandSender(), input10);
Assertions.assertEquals(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"), suggestions10);
} }
@Test @Test