Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions cloud-core/src/main/java/cloud/commandframework/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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<C>) ca)
.findFirst()
.orElse(null);

// Enforce ordering of command arguments
boolean foundOptional = false;
for (final CommandArgument<C, ?> argument : this.arguments) {
Expand Down Expand Up @@ -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<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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}


Expand Down
77 changes: 66 additions & 11 deletions cloud-core/src/main/java/cloud/commandframework/CommandTree.java
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,8 @@ private CommandTree(final @NonNull CommandManager<C> 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()) {
Expand Down Expand Up @@ -603,14 +604,14 @@ private CommandTree(final @NonNull CommandManager<C> 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<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();
Expand All @@ -628,8 +629,7 @@ private CommandTree(final @NonNull CommandManager<C> 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<String>) commandQueue).getLast();
Expand All @@ -638,8 +638,7 @@ private CommandTree(final @NonNull CommandManager<C> 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());
Pablete1234 marked this conversation as resolved.
Show resolved Hide resolved
}

// Store original input command queue before the parsers below modify it
Expand Down Expand Up @@ -670,8 +669,7 @@ private CommandTree(final @NonNull CommandManager<C> 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) {
Expand All @@ -681,6 +679,31 @@ private CommandTree(final @NonNull CommandManager<C> commandManager) {
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
*
Expand All @@ -690,7 +713,15 @@ private CommandTree(final @NonNull CommandManager<C> commandManager) {
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);
Expand All @@ -704,7 +735,12 @@ public void insertCommand(final @NonNull Command<C> 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(
Expand All @@ -719,6 +755,25 @@ public void insertCommand(final @NonNull Command<C> command) {
}
}

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Pablete1234 marked this conversation as resolved.
Show resolved Hide resolved
@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;
Expand Down Expand Up @@ -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();
}
}

/*
Expand Down Expand Up @@ -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<CommandFlag<?>> parsedFlags = new HashSet<>();
Set<CommandFlag<?>> parsedFlags = commandContext.computeIfAbsent(PARSED_FLAGS, k -> new HashSet());

CommandFlag<?> currentFlag = null;
String currentFlagName = null;

Expand All @@ -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();

Expand Down Expand Up @@ -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.");
}
}
}
Expand Down
Loading