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:
Alexander Söderberg 2020-10-22 06:21:31 +02:00 committed by Alexander Söderberg
parent c9b61e4275
commit bd19e1be56
6 changed files with 154 additions and 31 deletions

View file

@ -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) {

View file

@ -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,11 +127,45 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
} }
} else { } else {
final String flagName = string.substring(1); final String flagName = string.substring(1);
for (final CommandFlag<?> flag : this.flags) { if (flagName.length() > 1) {
for (final String alias : flag.getAliases()) { boolean oneAdded = false;
if (alias.equalsIgnoreCase(flagName)) { /* This is a multi-alias flag, find all flags that apply */
currentFlag = flag; for (final CommandFlag<?> flag : this.flags) {
break; 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 String alias : flag.getAliases()) {
if (alias.equalsIgnoreCase(flagName)) {
currentFlag = flag;
break;
}
} }
} }
} }
@ -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 */
for (final CommandFlag<?> flag : this.flags) { final List<CommandFlag<?>> usedFlags = new LinkedList<>();
final String mainFlag = String.format("--%s", flag.getName()); /* Find all "primary" flags, using --flag */
final List<String> rawInput = commandContext.getRawInput(); final Matcher primaryMatcher = FLAG_PRIMARY_PATTERN.matcher(rawInput);
if (rawInput.contains(mainFlag)) { while (primaryMatcher.find()) {
continue; /* Flag was already used */ final String name = primaryMatcher.group("name");
} for (final CommandFlag<?> flag : this.flags) {
final List<String> flagAliases = new LinkedList<>(); if (flag.getName().equalsIgnoreCase(name)) {
boolean flagUsed = false; usedFlags.add(flag);
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 */
final Matcher aliasMatcher = FLAG_ALIAS_PATTERN.matcher(rawInput);
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;
}
}
} }
}
strings.add(mainFlag); /* Suggestions */
strings.addAll(flagAliases); 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 {

View file

@ -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<>(

View file

@ -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
* *

View file

@ -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

View file

@ -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