✨ Make the flag parser smarter
It will now allow multiple presence flag aliases to be joined into a single flag, such that `-a -b -c <=> -abc`. This fixes #75
This commit is contained in:
parent
c9b61e4275
commit
bd19e1be56
6 changed files with 154 additions and 31 deletions
|
|
@ -74,7 +74,7 @@ public abstract class LockableCommandManager<C> extends CommandManager<C> {
|
||||||
* else {@link IllegalStateException} will be called
|
* else {@link IllegalStateException} will be called
|
||||||
*
|
*
|
||||||
* @param command Command to register
|
* @param command Command to register
|
||||||
* @return
|
* @return The command manager instance
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public final @NonNull CommandManager<C> command(final @NonNull Command<C> command) {
|
public final @NonNull CommandManager<C> command(final @NonNull Command<C> command) {
|
||||||
|
|
@ -95,7 +95,7 @@ public abstract class LockableCommandManager<C> extends CommandManager<C> {
|
||||||
* else {@link IllegalStateException} will be called
|
* else {@link IllegalStateException} will be called
|
||||||
*
|
*
|
||||||
* @param command Command to register. {@link Command.Builder#build()}} will be invoked.
|
* @param command Command to register. {@link Command.Builder#build()}} will be invoked.
|
||||||
* @return
|
* @return The command manager instance
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public final @NonNull CommandManager<C> command(final Command.@NonNull Builder<C> command) {
|
public final @NonNull CommandManager<C> command(final Command.@NonNull Builder<C> command) {
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,12 @@ import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Container for flag parsing logic. This should not be be used directly.
|
* Container for flag parsing logic. This should not be be used directly.
|
||||||
|
|
@ -51,6 +54,9 @@ import java.util.function.BiFunction;
|
||||||
*/
|
*/
|
||||||
public final class FlagArgument<C> extends CommandArgument<C, Object> {
|
public final class FlagArgument<C> extends CommandArgument<C, Object> {
|
||||||
|
|
||||||
|
private static final Pattern FLAG_ALIAS_PATTERN = Pattern.compile(" -(?<name>([A-Za-z]+))");
|
||||||
|
private static final Pattern FLAG_PRIMARY_PATTERN = Pattern.compile(" --(?<name>([A-Za-z]+))");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dummy object that indicates that flags were parsed successfully
|
* Dummy object that indicates that flags were parsed successfully
|
||||||
*/
|
*/
|
||||||
|
|
@ -121,6 +127,39 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final String flagName = string.substring(1);
|
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 CommandFlag<?> flag : this.flags) {
|
||||||
for (final String alias : flag.getAliases()) {
|
for (final String alias : flag.getAliases()) {
|
||||||
if (alias.equalsIgnoreCase(flagName)) {
|
if (alias.equalsIgnoreCase(flagName)) {
|
||||||
|
|
@ -130,6 +169,7 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (currentFlag == null) {
|
if (currentFlag == null) {
|
||||||
return ArgumentParseResult.failure(new FlagParseException(
|
return ArgumentParseResult.failure(new FlagParseException(
|
||||||
string,
|
string,
|
||||||
|
|
@ -196,30 +236,76 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
|
||||||
/* Check if we have a last flag stored */
|
/* Check if we have a last flag stored */
|
||||||
final String lastArg = commandContext.getOrDefault(FLAG_META, "");
|
final String lastArg = commandContext.getOrDefault(FLAG_META, "");
|
||||||
if (lastArg.isEmpty() || !lastArg.startsWith("-")) {
|
if (lastArg.isEmpty() || !lastArg.startsWith("-")) {
|
||||||
/* We don't care about the last value and so we expect a flag */
|
final String rawInput = commandContext.getRawInputJoined();
|
||||||
final List<String> strings = new LinkedList<>();
|
/* Collection containing all used flags */
|
||||||
|
final List<CommandFlag<?>> usedFlags = new LinkedList<>();
|
||||||
|
/* Find all "primary" flags, using --flag */
|
||||||
|
final Matcher primaryMatcher = FLAG_PRIMARY_PATTERN.matcher(rawInput);
|
||||||
|
while (primaryMatcher.find()) {
|
||||||
|
final String name = primaryMatcher.group("name");
|
||||||
for (final CommandFlag<?> flag : this.flags) {
|
for (final CommandFlag<?> flag : this.flags) {
|
||||||
final String mainFlag = String.format("--%s", flag.getName());
|
if (flag.getName().equalsIgnoreCase(name)) {
|
||||||
final List<String> rawInput = commandContext.getRawInput();
|
usedFlags.add(flag);
|
||||||
if (rawInput.contains(mainFlag)) {
|
|
||||||
continue; /* Flag was already used */
|
|
||||||
}
|
|
||||||
final List<String> flagAliases = new LinkedList<>();
|
|
||||||
boolean flagUsed = false;
|
|
||||||
for (final String alias : flag.getAliases()) {
|
|
||||||
final String aliasFlag = String.format("-%s", alias);
|
|
||||||
if (rawInput.contains(aliasFlag)) {
|
|
||||||
flagUsed = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
flagAliases.add(aliasFlag);
|
|
||||||
}
|
}
|
||||||
if (flagUsed) {
|
|
||||||
continue; /* Flag was already used via an alias */
|
|
||||||
}
|
}
|
||||||
|
/* Find all alias flags */
|
||||||
strings.add(mainFlag);
|
final Matcher aliasMatcher = FLAG_ALIAS_PATTERN.matcher(rawInput);
|
||||||
strings.addAll(flagAliases);
|
while (aliasMatcher.find()) {
|
||||||
|
final String name = aliasMatcher.group("name");
|
||||||
|
for (final CommandFlag<?> flag : this.flags) {
|
||||||
|
for (final String alias : flag.getAliases()) {
|
||||||
|
/* Aliases are single-char strings */
|
||||||
|
if (name.contains(alias)) {
|
||||||
|
usedFlags.add(flag);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Suggestions */
|
||||||
|
final List<String> strings = new LinkedList<>();
|
||||||
|
/* Recommend "primary" flags */
|
||||||
|
for (final CommandFlag<?> flag : this.flags) {
|
||||||
|
if (usedFlags.contains(flag)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
strings.add(
|
||||||
|
String.format(
|
||||||
|
"--%s",
|
||||||
|
flag.getName()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* Recommend aliases */
|
||||||
|
final boolean suggestCombined = input.length() > 1 && input.charAt(0) == '-' && input.charAt(1) != '-';
|
||||||
|
for (final CommandFlag<?> flag : this.flags) {
|
||||||
|
if (usedFlags.contains(flag)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (final String alias : flag.getAliases()) {
|
||||||
|
if (suggestCombined && flag.getCommandArgument() == null) {
|
||||||
|
strings.add(
|
||||||
|
String.format(
|
||||||
|
"%s%s",
|
||||||
|
input,
|
||||||
|
alias
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
strings.add(
|
||||||
|
String.format(
|
||||||
|
"-%s",
|
||||||
|
alias
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* If we are suggesting the combined flag, then also suggest the current input */
|
||||||
|
if (suggestCombined) {
|
||||||
|
strings.add(input);
|
||||||
}
|
}
|
||||||
return strings;
|
return strings;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,9 @@ public final class CommandFlag<T> {
|
||||||
final @NonNull Description description,
|
final @NonNull Description description,
|
||||||
final @Nullable CommandArgument<?, T> commandArgument
|
final @Nullable CommandArgument<?, T> commandArgument
|
||||||
) {
|
) {
|
||||||
this.name = name;
|
this.name = Objects.requireNonNull(name, "name cannot be null");
|
||||||
this.aliases = aliases;
|
this.aliases = Objects.requireNonNull(aliases, "aliases cannot be null");
|
||||||
this.description = description;
|
this.description = Objects.requireNonNull(description, "description cannot be null");
|
||||||
this.commandArgument = commandArgument;
|
this.commandArgument = commandArgument;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,7 +156,8 @@ public final class CommandFlag<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new builder instance using the given flag aliases
|
* Create a new builder instance using the given flag aliases.
|
||||||
|
* These may at most be one character in length
|
||||||
*
|
*
|
||||||
* @param aliases Flag aliases
|
* @param aliases Flag aliases
|
||||||
* @return New builder instance
|
* @return New builder instance
|
||||||
|
|
@ -167,6 +168,14 @@ public final class CommandFlag<T> {
|
||||||
if (alias.isEmpty()) {
|
if (alias.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (alias.length() > 1) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format(
|
||||||
|
"Alias '%s' has name longer than one character. This is not allowed",
|
||||||
|
alias
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
filteredAliases.add(alias);
|
filteredAliases.add(alias);
|
||||||
}
|
}
|
||||||
return new Builder<>(
|
return new Builder<>(
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,16 @@ public final class CommandContext<C> {
|
||||||
return this.getOrDefault("__raw_input__", new LinkedList<>());
|
return this.getOrDefault("__raw_input__", new LinkedList<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the raw input as a joined string
|
||||||
|
*
|
||||||
|
* @return {@link #getRawInput()} joined with {@code " "} as the delimiter
|
||||||
|
* @since 1.1.0
|
||||||
|
*/
|
||||||
|
public @NonNull String getRawInputJoined() {
|
||||||
|
return String.join(" ", this.getRawInput());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an argument timing for a specific argument
|
* Create an argument timing for a specific argument
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,12 @@ public class CommandSuggestionsTest {
|
||||||
.build())
|
.build())
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
|
manager.command(manager.commandBuilder("flags2")
|
||||||
|
.flag(manager.flagBuilder("first").withAliases("f"))
|
||||||
|
.flag(manager.flagBuilder("second").withAliases("s"))
|
||||||
|
.flag(manager.flagBuilder("third").withAliases("t"))
|
||||||
|
.build());
|
||||||
|
|
||||||
manager.command(manager.commandBuilder("numbers").argument(IntegerArgument.of("num")));
|
manager.command(manager.commandBuilder("numbers").argument(IntegerArgument.of("num")));
|
||||||
|
|
||||||
manager.command(manager.commandBuilder("numberswithmin")
|
manager.command(manager.commandBuilder("numberswithmin")
|
||||||
|
|
@ -169,6 +175,15 @@ public class CommandSuggestionsTest {
|
||||||
final String input3 = "flags 10 --enum foo ";
|
final String input3 = "flags 10 --enum foo ";
|
||||||
final List<String> suggestions3 = manager.suggest(new TestCommandSender(), input3);
|
final List<String> suggestions3 = manager.suggest(new TestCommandSender(), input3);
|
||||||
Assertions.assertEquals(Collections.singletonList("--static"), suggestions3);
|
Assertions.assertEquals(Collections.singletonList("--static"), suggestions3);
|
||||||
|
final String input4 = "flags2 ";
|
||||||
|
final List<String> suggestions4 = manager.suggest(new TestCommandSender(), input4);
|
||||||
|
Assertions.assertEquals(Arrays.asList("--first", "--second", "--third", "-f", "-s", "-t"), suggestions4);
|
||||||
|
final String input5 = "flags2 -f";
|
||||||
|
final List<String> suggestions5 = manager.suggest(new TestCommandSender(), input5);
|
||||||
|
Assertions.assertEquals(Arrays.asList("-fs", "-ft", "-f"), suggestions5);
|
||||||
|
final String input6 = "flags2 -f -s";
|
||||||
|
final List<String> suggestions6 = manager.suggest(new TestCommandSender(), input6);
|
||||||
|
Assertions.assertEquals(Arrays.asList("-st", "-s"), suggestions6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ class CommandTreeTest {
|
||||||
.withAliases("t")
|
.withAliases("t")
|
||||||
.build())
|
.build())
|
||||||
.flag(manager.flagBuilder("test2")
|
.flag(manager.flagBuilder("test2")
|
||||||
|
.withAliases("f")
|
||||||
.build())
|
.build())
|
||||||
.flag(manager.flagBuilder("num")
|
.flag(manager.flagBuilder("num")
|
||||||
.withArgument(IntegerArgument.of("num")).build())
|
.withArgument(IntegerArgument.of("num")).build())
|
||||||
|
|
@ -124,6 +125,7 @@ class CommandTreeTest {
|
||||||
.withArgument(EnumArgument.of(FlagEnum.class, "enum")))
|
.withArgument(EnumArgument.of(FlagEnum.class, "enum")))
|
||||||
.handler(c -> {
|
.handler(c -> {
|
||||||
System.out.println("Flag present? " + c.flags().isPresent("test"));
|
System.out.println("Flag present? " + c.flags().isPresent("test"));
|
||||||
|
System.out.println("Second flag present? " + c.flags().isPresent("test2"));
|
||||||
System.out.println("Numerical flag: " + c.flags().getValue("num", -10));
|
System.out.println("Numerical flag: " + c.flags().getValue("num", -10));
|
||||||
System.out.println("Enum: " + c.flags().getValue("enum", FlagEnum.PROXI));
|
System.out.println("Enum: " + c.flags().getValue("enum", FlagEnum.PROXI));
|
||||||
})
|
})
|
||||||
|
|
@ -283,6 +285,7 @@ class CommandTreeTest {
|
||||||
manager.executeCommand(new TestCommandSender(), "flags --test test2").join());
|
manager.executeCommand(new TestCommandSender(), "flags --test test2").join());
|
||||||
manager.executeCommand(new TestCommandSender(), "flags --num 500").join();
|
manager.executeCommand(new TestCommandSender(), "flags --num 500").join();
|
||||||
manager.executeCommand(new TestCommandSender(), "flags --num 63 --enum potato --test").join();
|
manager.executeCommand(new TestCommandSender(), "flags --num 63 --enum potato --test").join();
|
||||||
|
manager.executeCommand(new TestCommandSender(), "flags -tf --num 63 --enum potato").join();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue