Add option to allow flags anywhere after last literal argument (#395)

This commit is contained in:
Pablo Herrera 2022-11-28 21:17:25 +01:00 committed by Jason
parent adea7d5ba9
commit 6c026f994b
8 changed files with 372 additions and 113 deletions

View file

@ -62,6 +62,7 @@ public class Command<C> {
private final List<@NonNull CommandComponent<C>> components;
private final List<@NonNull CommandArgument<C, ?>> arguments;
private final @Nullable FlagArgument<C> flagArgument;
private final CommandExecutionHandler<C> commandExecutionHandler;
private final Class<? extends C> senderType;
private final CommandPermission commandPermission;
@ -78,6 +79,7 @@ public class Command<C> {
* @since 1.3.0
*/
@API(status = API.Status.STABLE, since = "1.3.0")
@SuppressWarnings("unchecked")
public Command(
final @NonNull List<@NonNull CommandComponent<C>> commandComponents,
final @NonNull CommandExecutionHandler<@NonNull C> commandExecutionHandler,
@ -90,6 +92,14 @@ public class Command<C> {
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<C>) ca)
.findFirst()
.orElse(null);
// Enforce ordering of command arguments
boolean foundOptional = false;
for (final CommandArgument<C, ?> argument : this.arguments) {
@ -318,6 +328,32 @@ public class Command<C> {
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<CommandArgument<@NonNull C, @NonNull ?>> nonFlagArguments() {
List<CommandArgument<C, ?>> 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
*

View file

@ -1417,7 +1417,17 @@ public abstract class CommandManager<C> {
* @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
}

View file

@ -340,7 +340,8 @@ public final class CommandTree<C> {
));
}
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 @@ public final class CommandTree<C> {
* 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);
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 @@ public final class CommandTree<C> {
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<String>) commandQueue).getLast();
@ -639,8 +639,7 @@ public final class CommandTree<C> {
}
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 @@ public final class CommandTree<C> {
}
// 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 @@ public final class CommandTree<C> {
return string;
}
private @NonNull List<@NonNull String> directSuggestions(
final @NonNull CommandContext<C> commandContext,
final @NonNull Node<@NonNull CommandArgument<C, ?>> current,
final @NonNull String text) {
CommandArgument<C, ?> argument = Objects.requireNonNull(current.getValue());
commandContext.setCurrentArgument(argument);
List<String> 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<CommandArgument<C, ?>> 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 @@ public final class CommandTree<C> {
public void insertCommand(final @NonNull Command<C> command) {
synchronized (this.commandLock) {
Node<CommandArgument<C, ?>> node = this.internalTree;
for (final CommandArgument<C, ?> argument : command.getArguments()) {
FlagArgument<C> flags = command.flagArgument();
List<CommandArgument<C, ?>> nonFlagArguments = command.nonFlagArguments();
int flagStartIdx = this.flagStartIndex(nonFlagArguments, flags);
for (int i = 0; i < nonFlagArguments.size(); i++) {
final CommandArgument<C, ?> argument = nonFlagArguments.get(i);
Node<CommandArgument<C, ?>> tempNode = node.getChild(argument);
if (tempNode == null) {
tempNode = node.addChild(argument);
@ -725,7 +756,12 @@ public final class CommandTree<C> {
}
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 final class CommandTree<C> {
}
}
private int flagStartIndex(final @NonNull List<CommandArgument<C, ?>> arguments, final @Nullable FlagArgument<C> 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<C, ?>> node

View file

@ -79,6 +79,14 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
*/
public static final CloudKey<String> 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<Set<CommandFlag<?>>> PARSED_FLAGS = SimpleCloudKey.of("__parsed_flags__",
new TypeToken<Set<CommandFlag<?>>>(){});
private static final String FLAG_ARGUMENT_NAME = "flags";
private final Collection<@NonNull CommandFlag<?>> flags;
@ -157,16 +165,11 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
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 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
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<>();
Set<CommandFlag<?>> parsedFlags = commandContext.computeIfAbsent(PARSED_FLAGS, k -> new HashSet());
CommandFlag<?> currentFlag = null;
String currentFlagName = null;
@ -323,8 +323,12 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
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 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
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
));
}
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 {
/* 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.");
}
throw new IllegalStateException("Neither result or value were present. Panicking.");
}
}
}

View file

@ -41,6 +41,7 @@ import java.util.HashMap;
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 class CommandContext<C> {
/**
* 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 <T> Argument type
* @param <T> Value type
* @return Argument, or supplied default value
*/
public <T> T getOrDefault(
@ -510,9 +511,9 @@ public class CommandContext<C> {
/**
* 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 <T> Argument type
* @param <T> Value type
* @return Argument, or supplied default value
* @since 1.4.0
*/
@ -527,9 +528,9 @@ public class CommandContext<C> {
/**
* 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 <T> Argument type
* @param <T> Value type
* @return Argument, or supplied default value
* @since 1.2.0
*/
@ -544,9 +545,9 @@ public class CommandContext<C> {
/**
* 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 <T> Argument type
* @param <T> Value type
* @return Argument, or supplied default value
* @since 1.4.0
*/
@ -558,6 +559,25 @@ public class CommandContext<C> {
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 <T> Value type
* @return present or computed value
* @since 1.8.0
*/
@API(status = API.Status.STABLE, since = "1.8.0")
public <T> T computeIfAbsent(
final @NonNull CloudKey<T> key,
final @NonNull Function<CloudKey<T>, T> defaultFunction
) {
@SuppressWarnings("unchecked")
final T castedValue = (T) this.internalStorage.computeIfAbsent(key, k -> defaultFunction.apply((CloudKey<T>) k));
return castedValue;
}
/**
* Get the raw input.
*

View file

@ -462,30 +462,12 @@ public class CommandSuggestionsTest {
);
// Act
final List<String> suggestions1 = manager.suggest(
new TestCommandSender(),
"command "
);
final List<String> suggestions2 = manager.suggest(
new TestCommandSender(),
"command hel"
);
final List<String> suggestions3 = manager.suggest(
new TestCommandSender(),
"command hello --"
);
final List<String> suggestions4 = manager.suggest(
new TestCommandSender(),
"command hello --f"
);
final List<String> suggestions5 = manager.suggest(
new TestCommandSender(),
"command hello -f"
);
final List<String> suggestions6 = manager.suggest(
new TestCommandSender(),
"command hello -"
);
final List<String> suggestions1 = suggest(manager, "command ");
final List<String> suggestions2 = suggest(manager, "command hel");
final List<String> suggestions3 = suggest(manager, "command hello --");
final List<String> suggestions4 = suggest(manager, "command hello --f");
final List<String> suggestions5 = suggest(manager, "command hello -f");
final List<String> suggestions6 = suggest(manager, "command hello -");
// Assert
assertThat(suggestions1).containsExactly("hello");
@ -513,30 +495,12 @@ public class CommandSuggestionsTest {
);
// Act
final List<String> suggestions1 = manager.suggest(
new TestCommandSender(),
"command "
);
final List<String> suggestions2 = manager.suggest(
new TestCommandSender(),
"command hello"
);
final List<String> suggestions3 = manager.suggest(
new TestCommandSender(),
"command hello --"
);
final List<String> suggestions4 = manager.suggest(
new TestCommandSender(),
"command hello --f"
);
final List<String> suggestions5 = manager.suggest(
new TestCommandSender(),
"command hello -f"
);
final List<String> suggestions6 = manager.suggest(
new TestCommandSender(),
"command hello -"
);
final List<String> suggestions1 = suggest(manager, "command ");
final List<String> suggestions2 = suggest(manager, "command hello");
final List<String> suggestions3 = suggest(manager, "command hello --");
final List<String> suggestions4 = suggest(manager, "command hello --f");
final List<String> suggestions5 = suggest(manager, "command hello -f");
final List<String> suggestions6 = suggest(manager, "command hello -");
// Assert
assertThat(suggestions1).isEmpty();
@ -547,6 +511,78 @@ public class CommandSuggestionsTest {
assertThat(suggestions6).isEmpty();
}
@Test
void testFlagYieldingGreedyStringWithLiberalFlagArgument() {
// Arrange
final CommandManager<TestCommandSender> manager = createManager();
manager.setSetting(CommandManager.ManagerSettings.LIBERAL_FLAG_PARSING, true);
manager.command(
manager.commandBuilder("command")
.argument(
StringArgument.<TestCommandSender>newBuilder("string")
.greedyFlagYielding()
.withSuggestionsProvider((context, input) -> Collections.singletonList("hello"))
.build()
).flag(manager.flagBuilder("flag").withAliases("f").build())
.flag(manager.flagBuilder("flag2").build())
);
// Act
final List<String> suggestions1 = suggest(manager, "command ");
final List<String> suggestions2 = suggest(manager, "command hel");
final List<String> suggestions3 = suggest(manager, "command hello --");
final List<String> suggestions4 = suggest(manager, "command hello --f");
final List<String> suggestions5 = suggest(manager, "command hello -f");
final List<String> 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<TestCommandSender> 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<String> suggestions1 = suggest(manager, "command ");
final List<String> suggestions2 = suggest(manager, "command hello");
final List<String> suggestions3 = suggest(manager, "command hello --");
final List<String> suggestions4 = suggest(manager, "command hello --f");
final List<String> suggestions5 = suggest(manager, "command hello -f");
final List<String> suggestions6 = suggest(manager, "command hello -");
// Assert
assertThat(suggestions1).containsExactly("--flag", "--flag2", "-f");
assertThat(suggestions2).isEmpty();
assertThat(suggestions3).containsExactly("--flag", "--flag2");
assertThat(suggestions4).containsExactly("--flag", "--flag2");
assertThat(suggestions5).containsExactly("-f");
assertThat(suggestions6).isEmpty();
}
private List<String> suggest(CommandManager<TestCommandSender> manager, String command) {
return manager.suggest(new TestCommandSender(), command);
}
public enum TestEnum {
FOO,
BAR

View file

@ -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<TestCommandSender> 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<String> 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<TestCommandSender> result = this.commandManager.executeCommand(new TestCommandSender(), cmd).join();
assertThat(result.getCommandContext().flags().isPresent("flag")).isEqualTo(true);
}
}
@Test
void testFailBeforeLiterals() {
List<String> 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();
}
}

View file

@ -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 final class FabricClientExample implements ClientModInitializer {
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) {