From bd19e1be5653416ea8771867929032ad917c7ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Thu, 22 Oct 2020 06:21:31 +0200 Subject: [PATCH] :sparkles: 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 --- .../LockableCommandManager.java | 4 +- .../arguments/compound/FlagArgument.java | 136 ++++++++++++++---- .../arguments/flags/CommandFlag.java | 17 ++- .../context/CommandContext.java | 10 ++ .../CommandSuggestionsTest.java | 15 ++ .../commandframework/CommandTreeTest.java | 3 + 6 files changed, 154 insertions(+), 31 deletions(-) diff --git a/cloud-core/src/main/java/cloud/commandframework/LockableCommandManager.java b/cloud-core/src/main/java/cloud/commandframework/LockableCommandManager.java index 9f2b170f..f9c07840 100644 --- a/cloud-core/src/main/java/cloud/commandframework/LockableCommandManager.java +++ b/cloud-core/src/main/java/cloud/commandframework/LockableCommandManager.java @@ -74,7 +74,7 @@ public abstract class LockableCommandManager extends CommandManager { * else {@link IllegalStateException} will be called * * @param command Command to register - * @return + * @return The command manager instance */ @Override public final @NonNull CommandManager command(final @NonNull Command command) { @@ -95,7 +95,7 @@ public abstract class LockableCommandManager extends CommandManager { * else {@link IllegalStateException} will be called * * @param command Command to register. {@link Command.Builder#build()}} will be invoked. - * @return + * @return The command manager instance */ @Override public final @NonNull CommandManager command(final Command.@NonNull Builder command) { 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 c9186933..5b42996e 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 @@ -39,9 +39,12 @@ import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Queue; import java.util.Set; 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. @@ -51,6 +54,9 @@ import java.util.function.BiFunction; */ public final class FlagArgument extends CommandArgument { + private static final Pattern FLAG_ALIAS_PATTERN = Pattern.compile(" -(?([A-Za-z]+))"); + private static final Pattern FLAG_PRIMARY_PATTERN = Pattern.compile(" --(?([A-Za-z]+))"); + /** * Dummy object that indicates that flags were parsed successfully */ @@ -121,11 +127,45 @@ public final class FlagArgument extends CommandArgument { } } else { final String flagName = string.substring(1); - for (final CommandFlag flag : this.flags) { - for (final String alias : flag.getAliases()) { - if (alias.equalsIgnoreCase(flagName)) { - currentFlag = flag; - break; + 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 String alias : flag.getAliases()) { + if (alias.equalsIgnoreCase(flagName)) { + currentFlag = flag; + break; + } } } } @@ -196,30 +236,76 @@ public final class FlagArgument extends CommandArgument { /* Check if we have a last flag stored */ final String lastArg = commandContext.getOrDefault(FLAG_META, ""); if (lastArg.isEmpty() || !lastArg.startsWith("-")) { - /* We don't care about the last value and so we expect a flag */ - final List strings = new LinkedList<>(); - for (final CommandFlag flag : this.flags) { - final String mainFlag = String.format("--%s", flag.getName()); - final List rawInput = commandContext.getRawInput(); - if (rawInput.contains(mainFlag)) { - continue; /* Flag was already used */ - } - final List 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; + final String rawInput = commandContext.getRawInputJoined(); + /* Collection containing all used flags */ + final List> 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) { + if (flag.getName().equalsIgnoreCase(name)) { + usedFlags.add(flag); 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); - strings.addAll(flagAliases); + } + /* Suggestions */ + final List 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; } else { diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java index 32d49ad7..9ca95d48 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/flags/CommandFlag.java @@ -56,9 +56,9 @@ public final class CommandFlag { final @NonNull Description description, final @Nullable CommandArgument commandArgument ) { - this.name = name; - this.aliases = aliases; - this.description = description; + this.name = Objects.requireNonNull(name, "name cannot be null"); + this.aliases = Objects.requireNonNull(aliases, "aliases cannot be null"); + this.description = Objects.requireNonNull(description, "description cannot be null"); this.commandArgument = commandArgument; } @@ -156,7 +156,8 @@ public final class CommandFlag { } /** - * 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 * @return New builder instance @@ -167,6 +168,14 @@ public final class CommandFlag { if (alias.isEmpty()) { 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); } return new Builder<>( 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 0ff9f25e..f6dcab69 100644 --- a/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java +++ b/cloud-core/src/main/java/cloud/commandframework/context/CommandContext.java @@ -244,6 +244,16 @@ public final class CommandContext { 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 * diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java index 4aaf0e50..24747878 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java @@ -80,6 +80,12 @@ public class CommandSuggestionsTest { .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("numberswithmin") @@ -169,6 +175,15 @@ public class CommandSuggestionsTest { final String input3 = "flags 10 --enum foo "; final List suggestions3 = manager.suggest(new TestCommandSender(), input3); Assertions.assertEquals(Collections.singletonList("--static"), suggestions3); + final String input4 = "flags2 "; + final List suggestions4 = manager.suggest(new TestCommandSender(), input4); + Assertions.assertEquals(Arrays.asList("--first", "--second", "--third", "-f", "-s", "-t"), suggestions4); + final String input5 = "flags2 -f"; + final List suggestions5 = manager.suggest(new TestCommandSender(), input5); + Assertions.assertEquals(Arrays.asList("-fs", "-ft", "-f"), suggestions5); + final String input6 = "flags2 -f -s"; + final List suggestions6 = manager.suggest(new TestCommandSender(), input6); + Assertions.assertEquals(Arrays.asList("-st", "-s"), suggestions6); } @Test diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java index 1f6acfa2..8fcb7bf0 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandTreeTest.java @@ -117,6 +117,7 @@ class CommandTreeTest { .withAliases("t") .build()) .flag(manager.flagBuilder("test2") + .withAliases("f") .build()) .flag(manager.flagBuilder("num") .withArgument(IntegerArgument.of("num")).build()) @@ -124,6 +125,7 @@ class CommandTreeTest { .withArgument(EnumArgument.of(FlagEnum.class, "enum"))) .handler(c -> { 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("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 --num 500").join(); manager.executeCommand(new TestCommandSender(), "flags --num 63 --enum potato --test").join(); + manager.executeCommand(new TestCommandSender(), "flags -tf --num 63 --enum potato").join(); } @Test