From aa293ce98a7d02059bc8984e28495b11ac281183 Mon Sep 17 00:00:00 2001 From: Pablete1234 Date: Fri, 14 Oct 2022 07:02:52 +0200 Subject: [PATCH] Support flags in any position Signed-off-by: Pablete1234 --- .../java/cloud/commandframework/Command.java | 36 +++++++ .../commandframework/CommandManager.java | 12 ++- .../cloud/commandframework/CommandTree.java | 77 ++++++++++++-- .../arguments/compound/FlagArgument.java | 86 +++++++-------- .../context/CommandContext.java | 36 +++++-- .../CommandSuggestionsTest.java | 6 +- .../feature/ArbitraryPositionFlagTest.java | 100 ++++++++++++++++++ .../fabric/testmod/FabricClientExample.java | 6 ++ 8 files changed, 292 insertions(+), 67 deletions(-) create mode 100644 cloud-core/src/test/java/cloud/commandframework/feature/ArbitraryPositionFlagTest.java diff --git a/cloud-core/src/main/java/cloud/commandframework/Command.java b/cloud-core/src/main/java/cloud/commandframework/Command.java index 5cdc42020..c2ed50972 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 4f0586036..e89321824 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 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") + ALLOW_FLAGS_EVERYWHERE } diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java index 4449c85af..398ed2712 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandTree.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandTree.java @@ -339,7 +339,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()) { @@ -603,14 +604,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(); @@ -628,8 +629,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(); @@ -638,8 +638,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 @@ -670,8 +669,7 @@ private CommandTree(final @NonNull CommandManager commandManager) { commandQueue.addAll(commandQueueOriginal); // 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) { @@ -681,6 +679,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 * @@ -690,7 +713,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); @@ -704,7 +735,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( @@ -719,6 +755,25 @@ public void insertCommand(final @NonNull Command command) { } } + private int flagStartIndex(final List> arguments, final FlagArgument flags) { + // Do not append flags + if (flags == null) { + return Integer.MAX_VALUE; + } + + // Append flags before the first non-static argument + if (this.commandManager.getSetting(CommandManager.ManagerSettings.ALLOW_FLAGS_EVERYWHERE)) { + for (int i = 1; i < arguments.size(); i++) { + if (!(arguments.get(i) instanceof StaticArgument)) { + return i - 1; + } + } + } + + // 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 358a6ed26..5bcfbd03a 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 1ba10551c..5a04c047a 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 void remove(final @NonNull CloudKey key) { /** * 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 @Nullable T getOrDefault( @@ -510,9 +511,9 @@ public void remove(final @NonNull CloudKey key) { /** * 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 void remove(final @NonNull CloudKey key) { /** * 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 void remove(final @NonNull CloudKey key) { /** * 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 void remove(final @NonNull CloudKey key) { 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 42b9bdf1b..17d3848b5 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java @@ -403,6 +403,7 @@ void testLiteralWithVariable() { void testFlagYieldingGreedyStringFollowedByFlagArgument() { // Arrange final CommandManager manager = createManager(); + manager.setSetting(CommandManager.ManagerSettings.ALLOW_FLAGS_EVERYWHERE, true); manager.command( manager.commandBuilder("command") .argument( @@ -441,7 +442,7 @@ void testFlagYieldingGreedyStringFollowedByFlagArgument() { ); // Assert - assertThat(suggestions1).containsExactly("hello"); + assertThat(suggestions1).containsExactly("hello", "--flag", "--flag2", "-f"); assertThat(suggestions2).containsExactly("hello"); assertThat(suggestions3).containsExactly("--flag", "--flag2"); assertThat(suggestions4).containsExactly("--flag", "--flag2"); @@ -453,6 +454,7 @@ void testFlagYieldingGreedyStringFollowedByFlagArgument() { void testFlagYieldingStringArrayFollowedByFlagArgument() { // Arrange final CommandManager manager = createManager(); + manager.setSetting(CommandManager.ManagerSettings.ALLOW_FLAGS_EVERYWHERE, true); manager.command( manager.commandBuilder("command") .argument( @@ -492,7 +494,7 @@ void testFlagYieldingStringArrayFollowedByFlagArgument() { ); // Assert - assertThat(suggestions1).isEmpty(); + assertThat(suggestions1).containsExactly("--flag", "--flag2", "-f"); assertThat(suggestions2).isEmpty(); assertThat(suggestions3).containsExactly("--flag", "--flag2"); assertThat(suggestions4).containsExactly("--flag", "--flag2"); 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..f05feaf7e --- /dev/null +++ b/cloud-core/src/test/java/cloud/commandframework/feature/ArbitraryPositionFlagTest.java @@ -0,0 +1,100 @@ +// +// MIT License +// +// Copyright (c) 2021 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.ALLOW_FLAGS_EVERYWHERE, 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 5ffe8748e..c75a34811 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) {