diff --git a/cloud-core/src/main/java/cloud/commandframework/Command.java b/cloud-core/src/main/java/cloud/commandframework/Command.java index 6b99550ad..97507bfd8 100644 --- a/cloud-core/src/main/java/cloud/commandframework/Command.java +++ b/cloud-core/src/main/java/cloud/commandframework/Command.java @@ -62,6 +62,7 @@ public class Command { private final List<@NonNull CommandComponent> components; private final List<@NonNull CommandArgument> arguments; + private final @Nullable FlagArgument flagArgument; private final CommandExecutionHandler commandExecutionHandler; private final Class senderType; private final CommandPermission commandPermission; @@ -78,6 +79,7 @@ public class Command { * @since 1.3.0 */ @API(status = API.Status.STABLE, since = "1.3.0") + @SuppressWarnings("unchecked") public Command( final @NonNull List<@NonNull CommandComponent> commandComponents, final @NonNull CommandExecutionHandler<@NonNull C> commandExecutionHandler, @@ -90,6 +92,14 @@ public Command( if (this.components.isEmpty()) { throw new IllegalArgumentException("At least one command component is required"); } + + this.flagArgument = + this.arguments.stream() + .filter(ca -> ca instanceof FlagArgument) + .map(ca -> (FlagArgument) ca) + .findFirst() + .orElse(null); + // Enforce ordering of command arguments boolean foundOptional = false; for (final CommandArgument argument : this.arguments) { @@ -318,6 +328,32 @@ public Command( return new ArrayList<>(this.arguments); } + /** + * Return a mutable copy of the command arguments, ignoring flag arguments. + * + * @return argument list + * @since 1.8.0 + */ + @API(status = API.Status.EXPERIMENTAL, since = "1.8.0") + public @NonNull List> nonFlagArguments() { + List> arguments = new ArrayList<>(this.arguments); + if (this.flagArgument != null) { + arguments.remove(this.flagArgument); + } + return arguments; + } + + /** + * Returns the flag argument for this command, or null if no flags are supported. + * + * @return flag argument or null + * @since 1.8.0 + */ + @API(status = API.Status.EXPERIMENTAL, since = "1.8.0") + public @Nullable FlagArgument<@NonNull C> flagArgument() { + return this.flagArgument; + } + /** * Returns a copy of the command component array * diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java index 07b2510b8..a5bbf9827 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java @@ -1417,7 +1417,17 @@ public enum ManagerSettings { * @since 1.2.0 */ @API(status = API.Status.STABLE, since = "1.2.0") - OVERRIDE_EXISTING_COMMANDS + OVERRIDE_EXISTING_COMMANDS, + + /** + * Allows parsing flags at any position after the last literal by appending flag argument nodes between each command node. + * It can have some conflicts when integrating with other command systems like Brigadier, + * and code inspecting the command tree may need to be adjusted. + * + * @since 1.8.0 + */ + @API(status = API.Status.EXPERIMENTAL, since = "1.8.0") + LIBERAL_FLAG_PARSING } diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java index 3194db03c..9742dcf17 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java @@ -340,7 +340,8 @@ private CommandTree(final @NonNull CommandManager commandManager) { )); } if (child.getValue() != null) { - if (commandQueue.isEmpty()) { + // Flag arguments need to be skipped over, so that further defaults are handled + if (commandQueue.isEmpty() && !(child.getValue() instanceof FlagArgument)) { if (child.getValue().hasDefaultValue()) { commandQueue.add(child.getValue().getDefaultValue()); } else if (!child.getValue().isRequired()) { @@ -604,14 +605,14 @@ private CommandTree(final @NonNull CommandManager commandManager) { * 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); lastFlag.ifPresent(s -> commandContext.store(FlagArgument.FLAG_META_KEY, s)); + if (!lastFlag.isPresent()) { + commandContext.remove(FlagArgument.FLAG_META_KEY); + } } else if (GenericTypeReflector.erase(child.getValue().getValueType().getType()).isArray()) { while (commandQueue.size() > 1) { commandQueue.remove(); @@ -629,8 +630,7 @@ private CommandTree(final @NonNull CommandManager commandManager) { 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()); + return this.directSuggestions(commandContext, child, commandQueue.peek()); } else if (child.isLeaf()) { if (child.getValue() instanceof CompoundArgument) { final String last = ((LinkedList) commandQueue).getLast(); @@ -639,8 +639,7 @@ private CommandTree(final @NonNull CommandManager commandManager) { } return Collections.emptyList(); } else if (commandQueue.peek().isEmpty()) { - commandContext.setCurrentArgument(child.getValue()); - return child.getValue().getSuggestionsProvider().apply(commandContext, commandQueue.remove()); + return this.directSuggestions(commandContext, child, commandQueue.peek()); } // Store original input command queue before the parsers below modify it @@ -691,8 +690,7 @@ private CommandTree(final @NonNull CommandManager commandManager) { } // Fallback: use suggestion provider of argument - commandContext.setCurrentArgument(child.getValue()); - return child.getValue().getSuggestionsProvider().apply(commandContext, this.stringOrEmpty(commandQueue.peek())); + return this.directSuggestions(commandContext, child, commandQueue.peek()); } private @NonNull String stringOrEmpty(final @Nullable String string) { @@ -702,6 +700,31 @@ private CommandTree(final @NonNull CommandManager commandManager) { return string; } + private @NonNull List<@NonNull String> directSuggestions( + final @NonNull CommandContext commandContext, + final @NonNull Node<@NonNull CommandArgument> current, + final @NonNull String text) { + CommandArgument argument = Objects.requireNonNull(current.getValue()); + + commandContext.setCurrentArgument(argument); + List suggestions = argument.getSuggestionsProvider().apply(commandContext, text); + + // When suggesting a flag, potentially suggest following nodes too + if (argument instanceof FlagArgument + && !current.getChildren().isEmpty() // Has children + && !text.startsWith("-") // Not a flag + && !commandContext.getOptional(FlagArgument.FLAG_META_KEY).isPresent()) { + suggestions = new ArrayList<>(suggestions); + for (final Node> child : current.getChildren()) { + argument = Objects.requireNonNull(child.getValue()); + commandContext.setCurrentArgument(argument); + suggestions.addAll(argument.getSuggestionsProvider().apply(commandContext, text)); + } + } + + return suggestions; + } + /** * Insert a new command into the command tree * @@ -711,7 +734,15 @@ private CommandTree(final @NonNull CommandManager commandManager) { public void insertCommand(final @NonNull Command command) { synchronized (this.commandLock) { Node> node = this.internalTree; - for (final CommandArgument argument : command.getArguments()) { + FlagArgument flags = command.flagArgument(); + + List> nonFlagArguments = command.nonFlagArguments(); + + int flagStartIdx = this.flagStartIndex(nonFlagArguments, flags); + + for (int i = 0; i < nonFlagArguments.size(); i++) { + final CommandArgument argument = nonFlagArguments.get(i); + Node> tempNode = node.getChild(argument); if (tempNode == null) { tempNode = node.addChild(argument); @@ -725,7 +756,12 @@ public void insertCommand(final @NonNull Command command) { } tempNode.setParent(node); node = tempNode; + + if (i >= flagStartIdx) { + node = node.addChild(flags); + } } + if (node.getValue() != null) { if (node.getValue().getOwningCommand() != null) { throw new IllegalStateException(String.format( @@ -740,6 +776,25 @@ public void insertCommand(final @NonNull Command command) { } } + private int flagStartIndex(final @NonNull List> arguments, final @Nullable FlagArgument flags) { + // Do not append flags + if (flags == null) { + return Integer.MAX_VALUE; + } + + // Append flags after the last static argument + if (this.commandManager.getSetting(CommandManager.ManagerSettings.LIBERAL_FLAG_PARSING)) { + for (int i = arguments.size() - 1; i >= 0; i--) { + if (arguments.get(i) instanceof StaticArgument) { + return i; + } + } + } + + // Append flags after the last argument + return arguments.size() - 1; + } + private @Nullable CommandPermission isPermitted( final @NonNull C sender, final @NonNull Node<@Nullable CommandArgument> node 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 2a5fcfe96..46a4608fa 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 @@ -79,6 +79,14 @@ public final class FlagArgument extends CommandArgument { */ public static final CloudKey FLAG_META_KEY = SimpleCloudKey.of("__last_flag__", TypeToken.get(String.class)); + /** + * Meta data for the set of parsed flags, used to detect duplicates. + * @since 1.8.0 + */ + @API(status = API.Status.EXPERIMENTAL, since = "1.8.0") + public static final CloudKey>> PARSED_FLAGS = SimpleCloudKey.of("__parsed_flags__", + new TypeToken>>(){}); + private static final String FLAG_ARGUMENT_NAME = "flags"; private final Collection<@NonNull CommandFlag> flags; @@ -157,16 +165,11 @@ private FlagArgumentParser(final @NonNull CommandFlag[] flags) { 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(); - } } /* @@ -309,11 +312,8 @@ private class FlagParser { 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<>(); + Set> parsedFlags = commandContext.computeIfAbsent(PARSED_FLAGS, k -> new HashSet()); + CommandFlag currentFlag = null; String currentFlagName = null; @@ -323,8 +323,12 @@ private class FlagParser { this.currentFlagBeingParsed = Optional.empty(); this.currentFlagNameBeingParsed = Optional.empty(); - /* Parse next flag name to set */ - if (string.startsWith("-") && currentFlag == null) { + if (!string.startsWith("-") && currentFlag == null) { + /* Not flag waiting to be parsed */ + return ArgumentParseResult.success(FLAG_PARSE_RESULT_OBJECT); + } else if (currentFlag == null) { + /* Parse next flag name to set */ + /* Remove flag argument from input queue */ inputQueue.poll(); @@ -418,43 +422,35 @@ private class FlagParser { currentFlag = null; } } else { - if (currentFlag == null) { + /* Mark this flag as the one currently being typed */ + this.currentFlagBeingParsed = Optional.of(currentFlag); + this.currentFlagNameBeingParsed = Optional.of(currentFlagName); + + // Don't attempt to parse empty strings + if (inputQueue.peek().isEmpty()) { return ArgumentParseResult.failure(new FlagParseException( - string, - FailureReason.NO_FLAG_STARTED, + currentFlag.getName(), + FailureReason.MISSING_ARGUMENT, commandContext )); - } else { - /* Mark this flag as the one currently being typed */ - this.currentFlagBeingParsed = Optional.of(currentFlag); - this.currentFlagNameBeingParsed = Optional.of(currentFlagName); - - // Don't attempt to parse empty strings - if (inputQueue.peek().isEmpty()) { - return ArgumentParseResult.failure(new FlagParseException( - currentFlag.getName(), - FailureReason.MISSING_ARGUMENT, - commandContext - )); - } + } - 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."); - } + 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."); } } } diff --git a/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java b/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java index 2d84e2917..9109b472e 100644 --- a/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java +++ b/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java @@ -41,6 +41,7 @@ import java.util.LinkedList; import java.util.Map; import java.util.Optional; +import java.util.function.Function; import java.util.function.Supplier; import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; @@ -495,9 +496,9 @@ public T getOrDefault( /** * Get a value if it exists, else return the provided default value * - * @param key Argument key + * @param key Cloud key * @param defaultValue Default value - * @param Argument type + * @param Value type * @return Argument, or supplied default value */ public T getOrDefault( @@ -510,9 +511,9 @@ public T getOrDefault( /** * Get a value if it exists, else return the provided default value * - * @param key Argument key + * @param key Cloud key * @param defaultValue Default value - * @param Argument type + * @param Value type * @return Argument, or supplied default value * @since 1.4.0 */ @@ -527,9 +528,9 @@ public T getOrDefault( /** * Get a value if it exists, else return the value supplied by the given supplier * - * @param key Argument key + * @param key Cloud key * @param defaultSupplier Supplier of default value - * @param Argument type + * @param Value type * @return Argument, or supplied default value * @since 1.2.0 */ @@ -544,9 +545,9 @@ public T getOrSupplyDefault( /** * Get a value if it exists, else return the value supplied by the given supplier * - * @param key Argument key + * @param key Cloud key * @param defaultSupplier Supplier of default value - * @param Argument type + * @param Value type * @return Argument, or supplied default value * @since 1.4.0 */ @@ -558,6 +559,25 @@ public T getOrSupplyDefault( return this.getOptional(key).orElseGet(defaultSupplier); } + /** + * Get a value if it exists, else compute and store the value returned by the function and return it. + * + * @param key Cloud key + * @param defaultFunction Default value function + * @param Value type + * @return present or computed value + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public T computeIfAbsent( + final @NonNull CloudKey key, + final @NonNull Function, T> defaultFunction + ) { + @SuppressWarnings("unchecked") + final T castedValue = (T) this.internalStorage.computeIfAbsent(key, k -> defaultFunction.apply((CloudKey) k)); + return castedValue; + } + /** * Get the raw input. * diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java index 3f711fc60..a307fc310 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java @@ -462,30 +462,12 @@ void testFlagYieldingGreedyStringFollowedByFlagArgument() { ); // Act - final List suggestions1 = manager.suggest( - new TestCommandSender(), - "command " - ); - final List suggestions2 = manager.suggest( - new TestCommandSender(), - "command hel" - ); - final List suggestions3 = manager.suggest( - new TestCommandSender(), - "command hello --" - ); - final List suggestions4 = manager.suggest( - new TestCommandSender(), - "command hello --f" - ); - final List suggestions5 = manager.suggest( - new TestCommandSender(), - "command hello -f" - ); - final List suggestions6 = manager.suggest( - new TestCommandSender(), - "command hello -" - ); + final List suggestions1 = suggest(manager, "command "); + final List suggestions2 = suggest(manager, "command hel"); + final List suggestions3 = suggest(manager, "command hello --"); + final List suggestions4 = suggest(manager, "command hello --f"); + final List suggestions5 = suggest(manager, "command hello -f"); + final List suggestions6 = suggest(manager, "command hello -"); // Assert assertThat(suggestions1).containsExactly("hello"); @@ -513,33 +495,83 @@ void testFlagYieldingStringArrayFollowedByFlagArgument() { ); // Act - final List suggestions1 = manager.suggest( - new TestCommandSender(), - "command " - ); - final List suggestions2 = manager.suggest( - new TestCommandSender(), - "command hello" - ); - final List suggestions3 = manager.suggest( - new TestCommandSender(), - "command hello --" - ); - final List suggestions4 = manager.suggest( - new TestCommandSender(), - "command hello --f" - ); - final List suggestions5 = manager.suggest( - new TestCommandSender(), - "command hello -f" + final List suggestions1 = suggest(manager, "command "); + final List suggestions2 = suggest(manager, "command hello"); + final List suggestions3 = suggest(manager, "command hello --"); + final List suggestions4 = suggest(manager, "command hello --f"); + final List suggestions5 = suggest(manager, "command hello -f"); + final List suggestions6 = suggest(manager, "command hello -"); + + // Assert + assertThat(suggestions1).isEmpty(); + assertThat(suggestions2).isEmpty(); + assertThat(suggestions3).containsExactly("--flag", "--flag2"); + assertThat(suggestions4).containsExactly("--flag", "--flag2"); + assertThat(suggestions5).containsExactly("-f"); + assertThat(suggestions6).isEmpty(); + } + + + @Test + void testFlagYieldingGreedyStringWithLiberalFlagArgument() { + // Arrange + final CommandManager manager = createManager(); + manager.setSetting(CommandManager.ManagerSettings.LIBERAL_FLAG_PARSING, true); + manager.command( + manager.commandBuilder("command") + .argument( + StringArgument.newBuilder("string") + .greedyFlagYielding() + .withSuggestionsProvider((context, input) -> Collections.singletonList("hello")) + .build() + ).flag(manager.flagBuilder("flag").withAliases("f").build()) + .flag(manager.flagBuilder("flag2").build()) ); - final List suggestions6 = manager.suggest( - new TestCommandSender(), - "command hello -" + + // Act + final List suggestions1 = suggest(manager, "command "); + final List suggestions2 = suggest(manager, "command hel"); + final List suggestions3 = suggest(manager, "command hello --"); + final List suggestions4 = suggest(manager, "command hello --f"); + final List suggestions5 = suggest(manager, "command hello -f"); + final List suggestions6 = suggest(manager, "command hello -"); + + // Assert + assertThat(suggestions1).containsExactly("hello", "--flag", "--flag2", "-f"); + assertThat(suggestions2).containsExactly("hello"); + assertThat(suggestions3).containsExactly("--flag", "--flag2"); + assertThat(suggestions4).containsExactly("--flag", "--flag2"); + assertThat(suggestions5).containsExactly("-f"); + assertThat(suggestions6).containsExactly("hello"); + } + + @Test + void testFlagYieldingStringArrayWithLiberalFlagArgument() { + // Arrange + final CommandManager manager = createManager(); + manager.setSetting(CommandManager.ManagerSettings.LIBERAL_FLAG_PARSING, true); + manager.command( + manager.commandBuilder("command") + .argument( + StringArrayArgument.of( + "array", + true, + (context, input) -> Collections.emptyList() + ) + ).flag(manager.flagBuilder("flag").withAliases("f").build()) + .flag(manager.flagBuilder("flag2").build()) ); + // Act + final List suggestions1 = suggest(manager, "command "); + final List suggestions2 = suggest(manager, "command hello"); + final List suggestions3 = suggest(manager, "command hello --"); + final List suggestions4 = suggest(manager, "command hello --f"); + final List suggestions5 = suggest(manager, "command hello -f"); + final List suggestions6 = suggest(manager, "command hello -"); + // Assert - assertThat(suggestions1).isEmpty(); + assertThat(suggestions1).containsExactly("--flag", "--flag2", "-f"); assertThat(suggestions2).isEmpty(); assertThat(suggestions3).containsExactly("--flag", "--flag2"); assertThat(suggestions4).containsExactly("--flag", "--flag2"); @@ -547,6 +579,10 @@ void testFlagYieldingStringArrayFollowedByFlagArgument() { assertThat(suggestions6).isEmpty(); } + private List suggest(CommandManager manager, String command) { + return manager.suggest(new TestCommandSender(), command); + } + public enum TestEnum { FOO, BAR diff --git a/cloud-core/src/test/java/cloud/commandframework/feature/ArbitraryPositionFlagTest.java b/cloud-core/src/test/java/cloud/commandframework/feature/ArbitraryPositionFlagTest.java new file mode 100644 index 000000000..8eb3a7b2a --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/feature/ArbitraryPositionFlagTest.java @@ -0,0 +1,100 @@ +// +// MIT License +// +// Copyright (c) 2022 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.feature; + +import cloud.commandframework.CommandManager; +import cloud.commandframework.TestCommandSender; +import cloud.commandframework.arguments.compound.FlagArgument; +import cloud.commandframework.arguments.standard.StringArgument; +import cloud.commandframework.exceptions.ArgumentParseException; +import cloud.commandframework.execution.CommandResult; +import com.google.common.truth.ThrowableSubject; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletionException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import static cloud.commandframework.util.TestUtils.createManager; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ArbitraryPositionFlagTest { + + private CommandManager commandManager; + + @BeforeEach + void setup() { + this.commandManager = createManager(); + this.commandManager.setSetting(CommandManager.ManagerSettings.LIBERAL_FLAG_PARSING, true); + + this.commandManager.command( + this.commandManager.commandBuilder("test") + .literal("literal") + .argument(StringArgument.greedyFlagYielding("text")) + .flag(this.commandManager.flagBuilder("flag").withAliases("f"))); + } + + @Test + void testParsingAllLocations() { + List passing = Arrays.asList( + "test literal -f foo bar", + "test literal foo bar -f", + "test literal --flag foo bar", + "test literal foo bar --flag"); + + for (String cmd : passing) { + CommandResult result = this.commandManager.executeCommand(new TestCommandSender(), cmd).join(); + assertThat(result.getCommandContext().flags().isPresent("flag")).isEqualTo(true); + } + } + + @Test + void testFailBeforeLiterals() { + List failing = Arrays.asList( + "test -f literal foo bar", + "test --flag literal foo bar"); + + for (String cmd : failing) { + assertThrows(CompletionException.class, commandExecutable(cmd)); + } + } + + @Test + void testMultiFlagThrows() { + final CompletionException completionException = assertThrows(CompletionException.class, + commandExecutable("test literal -f foo bar -f")); + + + ThrowableSubject argParse = assertThat(completionException).hasCauseThat(); + argParse.isInstanceOf(ArgumentParseException.class); + argParse.hasCauseThat().isInstanceOf(FlagArgument.FlagParseException.class); + } + + private Executable commandExecutable(String cmd) { + return () -> this.commandManager.executeCommand(new TestCommandSender(), cmd).join(); + } + +} diff --git a/cloud-minecraft/cloud-fabric/src/testmod/java/cloud/commandframework/fabric/testmod/FabricClientExample.java b/cloud-minecraft/cloud-fabric/src/testmod/java/cloud/commandframework/fabric/testmod/FabricClientExample.java index e1aeb7eb3..204da7a08 100644 --- a/cloud-minecraft/cloud-fabric/src/testmod/java/cloud/commandframework/fabric/testmod/FabricClientExample.java +++ b/cloud-minecraft/cloud-fabric/src/testmod/java/cloud/commandframework/fabric/testmod/FabricClientExample.java @@ -24,6 +24,7 @@ package cloud.commandframework.fabric.testmod; import cloud.commandframework.Command; +import cloud.commandframework.arguments.flags.CommandFlag; import cloud.commandframework.arguments.standard.StringArgument; import cloud.commandframework.execution.CommandExecutionCoordinator; import cloud.commandframework.fabric.FabricClientCommandManager; @@ -125,6 +126,11 @@ public void onInitializeClient() { ctx.getSender().sendError(ComponentUtils.fromMessage(ex.getRawMessage())); } })); + + commandManager.command(base.literal("flag_test") + .argument(StringArgument.optional("parameter")) + .flag(CommandFlag.newBuilder("flag").withAliases("f")) + .handler(ctx -> ctx.getSender().sendFeedback(Component.literal("Had flag: " + ctx.flags().isPresent("flag"))))); } private static void disconnectClient(final @NonNull Minecraft client) {