diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java index c6306265..42715f20 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java @@ -55,6 +55,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Queue; import java.util.Set; import java.util.stream.Collectors; @@ -552,6 +553,11 @@ public final class CommandTree { final @NonNull Queue<@NonNull String> commandQueue, final @NonNull Node<@Nullable CommandArgument> 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 */ if (child.getValue() instanceof CompoundArgument) { @SuppressWarnings("unchecked") final CompoundArgument compoundArgument = (CompoundArgument) child @@ -566,18 +572,27 @@ public final class CommandTree { commandContext.store("__parsing_argument__", i + 2); } } - } else if (child.getValue() instanceof FlagArgument) { - /* Remove all but last */ - while (commandQueue.size() > 1) { - commandContext.store(FlagArgument.FLAG_META, commandQueue.remove()); + } else if (child.getValue().getParser() instanceof FlagArgument.FlagArgumentParser) { + + /* + * 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 parser = (FlagArgument.FlagArgumentParser) child.getValue().getParser(); + Optional lastFlag = parser.parseCurrentFlag(commandContext, commandQueue); + if (lastFlag.isPresent()) { + commandContext.store(FlagArgument.FLAG_META, lastFlag.get()); } - } else if (child.getValue() != null - && GenericTypeReflector.erase(child.getValue().getValueType().getType()).isArray()) { + } else if (GenericTypeReflector.erase(child.getValue().getValueType().getType()).isArray()) { while (commandQueue.size() > 1) { commandQueue.remove(); } - } else if (child.getValue() != null - && commandQueue.size() <= child.getValue().getParser().getRequestedArgumentCount()) { + } else if (commandQueue.size() <= child.getValue().getParser().getRequestedArgumentCount()) { for (int i = 0; i < child.getValue().getParser().getRequestedArgumentCount() - 1 && commandQueue.size() > 1; i++) { commandContext.store( @@ -587,57 +602,53 @@ public final class CommandTree { } } - if (child.getValue() != null) { - if (commandQueue.isEmpty()) { - return Collections.emptyList(); - } 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) 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 commandQueueOriginal = new LinkedList<>(commandQueue); - - // START: Preprocessing - final ArgumentParseResult 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 + if (commandQueue.isEmpty()) { + return Collections.emptyList(); + } else if (child.isLeaf() && commandQueue.size() < 2) { 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) 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 commandQueueOriginal = new LinkedList<>(commandQueue); + + // START: Preprocessing + final ArgumentParseResult 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) { diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java b/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java index 6fa5c941..0a9875d0 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/compound/FlagArgument.java @@ -40,6 +40,7 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.Queue; import java.util.Set; import java.util.function.BiFunction; @@ -104,131 +105,66 @@ public final class FlagArgument extends CommandArgument { } @Override - @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> parsedFlags = new HashSet<>(); - CommandFlag currentFlag = null; + final FlagParser parser = new FlagParser(); + return parser.parse(commandContext, inputQueue); + } - for (final @NonNull String string : inputQueue) { - if (string.startsWith("-") && currentFlag == null) { - if (string.startsWith("--")) { - final String flagName = string.substring(2); - for (final CommandFlag flag : this.flags) { - if (flagName.equalsIgnoreCase(flag.getName())) { - currentFlag = flag; - 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 : 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 : 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."); - } - } + /** + * Parse command input to figure out what flag is currently being + * typed at the end of the input queue. If no flag value is being + * inputed, returns {@link Optional#empty()}.
+ *
+ * Will consume all but the last element from the input queue. + * + * @param commandContext Command context + * @param inputQueue The input queue of arguments + * @return current flag being typed, or empty() if none is + */ + public @NonNull Optional parseCurrentFlag( + final @NonNull CommandContext<@NonNull C> commandContext, + final @NonNull Queue<@NonNull String> inputQueue + ) { + /* If empty, nothing to do */ + if (inputQueue.isEmpty()) { + return Optional.empty(); + } + + /* Before parsing, retrieve the last known input of the queue */ + String lastInputValue = ""; + for (String input : inputQueue) { + lastInputValue = input; + } + + /* Parse, but ignore the result of parsing */ + final FlagParser parser = new FlagParser(); + parser.parse(commandContext, inputQueue); + + /* + * 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. + */ + if (inputQueue.isEmpty()) { + inputQueue.add(lastInputValue); + } else { + while (inputQueue.size() > 1) { + inputQueue.remove(); } } - if (currentFlag != null) { - return ArgumentParseResult.failure(new FlagParseException( - currentFlag.getName(), - FailureReason.MISSING_ARGUMENT, - commandContext - )); - } - /* We've consumed everything */ - inputQueue.clear(); - return ArgumentParseResult.success(FLAG_PARSE_RESULT_OBJECT); + + /* + * Map to name of the flag. + * + * 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. + */ + //return parser.currentFlagBeingParsed.map(CommandFlag::getName); + return parser.currentFlagNameBeingParsed; } @Override @@ -342,6 +278,166 @@ public final class FlagArgument extends CommandArgument { 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> 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 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> 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); + } + } } /** diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java index 9f2173ea..7af522f8 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java @@ -23,11 +23,14 @@ // package cloud.commandframework; +import cloud.commandframework.arguments.compound.ArgumentTriplet; import cloud.commandframework.arguments.standard.BooleanArgument; import cloud.commandframework.arguments.standard.EnumArgument; import cloud.commandframework.arguments.standard.IntegerArgument; import cloud.commandframework.arguments.standard.StringArgument; import cloud.commandframework.types.tuples.Pair; +import cloud.commandframework.types.tuples.Triplet; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -87,6 +90,16 @@ public class CommandSuggestionsTest { .flag(manager.flagBuilder("third").withAliases("t")) .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("numberswithfollowingargument").argument(IntegerArgument.of("num")) .argument(BooleanArgument.of("another_argument"))); @@ -211,6 +224,55 @@ public class CommandSuggestionsTest { final String input6 = "flags2 -f -s"; final List suggestions6 = manager.suggest(new TestCommandSender(), input6); 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 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 suggestions = manager.suggest(new TestCommandSender(), input); + Assertions.assertEquals(Arrays.asList("--compound", "--presence", "--single", "-p"), suggestions); + + final String input2 = "flags3 --c"; + final List suggestions2 = manager.suggest(new TestCommandSender(), input2); + Assertions.assertEquals(Arrays.asList("--compound"), suggestions2); + + final String input3 = "flags3 --compound "; + final List 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 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 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 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 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 suggestions8 = manager.suggest(new TestCommandSender(), input8); + Assertions.assertEquals(Arrays.asList("--presence"), suggestions8); + + final String input9 = "flags3 --compound 22 33 44 --presence "; + final List suggestions9 = manager.suggest(new TestCommandSender(), input9); + Assertions.assertEquals(Arrays.asList("--single"), suggestions9); + + final String input10 = "flags3 --compound 22 33 44 --single "; + final List suggestions10 = manager.suggest(new TestCommandSender(), input10); + Assertions.assertEquals(Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"), suggestions10); } @Test