diff --git a/CHANGELOG.md b/CHANGELOG.md
index 608975ba..c97ee3ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Core: Add `DurationArgument` for parsing `java.time.Duration` ([#330](https://github.com/Incendo/cloud/pull/330))
- Core: Add delegating command execution handlers ([#363](https://github.com/Incendo/cloud/pull/363))
- Core: Add `builder()` getter to `Command.Builder` ([#363](https://github.com/Incendo/cloud/pull/363))
+- Core: Add flag yielding modes to `StringArgument` and `StringArrayArgument` ([#367](https://github.com/Incendo/cloud/pull/367))
- Annotations: Annotation string processors ([#353](https://github.com/Incendo/cloud/pull/353))
- Annotations: `@CommandContainer` annotation processing ([#364](https://github.com/Incendo/cloud/pull/364))
- Annotations: `@CommandMethod` annotation processing for compile-time validation ([#365](https://github.com/Incendo/cloud/pull/365))
diff --git a/cloud-core/src/main/java/cloud/commandframework/annotations/specifier/FlagYielding.java b/cloud-core/src/main/java/cloud/commandframework/annotations/specifier/FlagYielding.java
new file mode 100644
index 00000000..64da0fbc
--- /dev/null
+++ b/cloud-core/src/main/java/cloud/commandframework/annotations/specifier/FlagYielding.java
@@ -0,0 +1,43 @@
+//
+// MIT License
+//
+// Copyright (c) 2021 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.annotations.specifier;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that the argument should stop parsing when encountering what
+ * could potentially be a flag.
+ *
+ * This only has an effect on greedy arguments that consume all remaining input.
+ *
+ * @since 1.7.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface FlagYielding {
+
+}
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 021a1293..a5bd60b3 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
@@ -57,8 +57,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
*/
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]+))");
+ private static final Pattern FLAG_ALIAS_PATTERN = Pattern.compile(" -(?([A-Za-z]+))");
/**
* Dummy object that indicates that flags were parsed successfully
@@ -125,13 +125,13 @@ public final class FlagArgument extends CommandArgument {
/**
* Parse command input to figure out what flag is currently being
* typed at the end of the input queue. If no flag value is being
- * inputed, returns {@link Optional#empty()}.
+ * inputted, returns {@link Optional#empty()}.
*
* Will consume all but the last element from the input queue.
*
* @param commandContext Command context
* @param inputQueue The input queue of arguments
- * @return current flag being typed, or empty() if none is
+ * @return current flag being typed, or {@code empty()} if none is
*/
public @NonNull Optional parseCurrentFlag(
final @NonNull CommandContext<@NonNull C> commandContext,
diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParameters.java b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParameters.java
index 5d7eb178..170cec8a 100644
--- a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParameters.java
+++ b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParameters.java
@@ -59,6 +59,16 @@ public final class StandardParameters {
* Indicates that a string argument should be greedy
*/
public static final ParserParameter GREEDY = create("greedy", TypeToken.get(Boolean.class));
+ /**
+ * Indicates that an argument should stop parsing when encountering a potential flag.
+ *
+ * @since 1.7.0
+ * @see cloud.commandframework.annotations.specifier.FlagYielding
+ */
+ public static final ParserParameter FLAG_YIELDING = create(
+ "flag_yielding",
+ TypeToken.get(Boolean.class)
+ );
/**
* Indicates that a string argument should be quoted.
*
diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java
index 89ccc3a6..cdf48ec8 100644
--- a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java
+++ b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java
@@ -23,6 +23,7 @@
//
package cloud.commandframework.arguments.parser;
+import cloud.commandframework.annotations.specifier.FlagYielding;
import cloud.commandframework.annotations.specifier.Greedy;
import cloud.commandframework.annotations.specifier.Liberal;
import cloud.commandframework.annotations.specifier.Quoted;
@@ -104,6 +105,10 @@ public final class StandardParserRegistry implements ParserRegistry {
Liberal.class,
(liberal, typeToken) -> ParserParameters.single(StandardParameters.LIBERAL, true)
);
+ this.registerAnnotationMapper(
+ FlagYielding.class,
+ (flagYielding, typeToken) -> ParserParameters.single(StandardParameters.FLAG_YIELDING, true)
+ );
/* Register standard types */
this.registerParserSupplier(TypeToken.get(Byte.class), options ->
@@ -137,10 +142,13 @@ public final class StandardParserRegistry implements ParserRegistry {
(double) options.get(StandardParameters.RANGE_MAX, Double.POSITIVE_INFINITY)
));
this.registerParserSupplier(TypeToken.get(Character.class), options -> new CharArgument.CharacterParser<>());
- this.registerParserSupplier(TypeToken.get(String[].class), options -> new StringArrayArgument.StringArrayParser<>());
+ this.registerParserSupplier(TypeToken.get(String[].class), options ->
+ new StringArrayArgument.StringArrayParser<>(options.get(StandardParameters.FLAG_YIELDING, false))
+ );
/* Make this one less awful */
this.registerParserSupplier(TypeToken.get(String.class), options -> {
final boolean greedy = options.get(StandardParameters.GREEDY, false);
+ final boolean greedyFlagAware = options.get(StandardParameters.FLAG_YIELDING, false);
final boolean quoted = options.get(StandardParameters.QUOTED, false);
if (greedy && quoted) {
throw new IllegalArgumentException(
@@ -150,6 +158,8 @@ public final class StandardParserRegistry implements ParserRegistry {
final StringArgument.StringMode stringMode;
if (greedy) {
stringMode = StringArgument.StringMode.GREEDY;
+ } else if (greedyFlagAware) {
+ stringMode = StringArgument.StringMode.GREEDY_FLAG_YIELDING;
} else if (quoted) {
stringMode = StringArgument.StringMode.QUOTED;
} else {
diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArgument.java b/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArgument.java
index 023bb5ab..0574f46c 100644
--- a/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArgument.java
+++ b/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArgument.java
@@ -166,6 +166,18 @@ public final class StringArgument extends CommandArgument {
return of(name, StringMode.GREEDY);
}
+ /**
+ * Create a new required command argument with the 'greedy flag yielding' parsing mode
+ *
+ * @param name Argument name
+ * @param Command sender type
+ * @return Created argument
+ * @since 1.7.0
+ */
+ public static @NonNull CommandArgument greedyFlagYielding(final @NonNull String name) {
+ return of(name, StringMode.GREEDY_FLAG_YIELDING);
+ }
+
/**
* Create a new required command argument with the 'quoted' parsing mode
*
@@ -189,8 +201,14 @@ public final class StringArgument extends CommandArgument {
public enum StringMode {
SINGLE,
+ QUOTED,
GREEDY,
- QUOTED
+ /**
+ * Greedy string that will consume the input until a flag is present.
+ *
+ * @since 1.7.0
+ */
+ GREEDY_FLAG_YIELDING
}
@@ -224,6 +242,17 @@ public final class StringArgument extends CommandArgument {
return this;
}
+ /**
+ * Greedy string that will consume the input until a flag is present.
+ *
+ * @return Builder instance
+ * @since 1.7.0
+ */
+ public @NonNull @This Builder greedyFlagYielding() {
+ this.stringMode = StringMode.GREEDY_FLAG_YIELDING;
+ return this;
+ }
+
/**
* Set the string mode to single
*
@@ -274,8 +303,11 @@ public final class StringArgument extends CommandArgument {
}
+ @SuppressWarnings("UnnecessaryLambda")
public static final class StringParser implements ArgumentParser {
+ private static final Pattern FLAG_PATTERN = Pattern.compile("(-[A-Za-z_\\-0-9])|(--[A-Za-z_\\-0-9]*)");
+
private final StringMode stringMode;
private final BiFunction, String, List> suggestionsProvider;
@@ -390,6 +422,13 @@ public final class StringArgument extends CommandArgument {
break;
}
+ if (this.stringMode == StringMode.GREEDY_FLAG_YIELDING) {
+ // The pattern requires a leading space.
+ if (FLAG_PATTERN.matcher(string).matches()) {
+ break;
+ }
+ }
+
sj.add(string);
inputQueue.remove();
}
diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArrayArgument.java b/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArrayArgument.java
index caf326a7..968d965b 100644
--- a/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArrayArgument.java
+++ b/cloud-core/src/main/java/cloud/commandframework/arguments/standard/StringArrayArgument.java
@@ -29,9 +29,11 @@ import cloud.commandframework.arguments.parser.ArgumentParseResult;
import cloud.commandframework.arguments.parser.ArgumentParser;
import cloud.commandframework.context.CommandContext;
import io.leangen.geantyref.TypeToken;
+import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.function.BiFunction;
+import java.util.regex.Pattern;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -48,12 +50,13 @@ public final class StringArrayArgument extends CommandArgument {
final boolean required,
final @NonNull String name,
final @Nullable BiFunction, String, List> suggestionsProvider,
- final @NonNull ArgumentDescription defaultDescription
+ final @NonNull ArgumentDescription defaultDescription,
+ final boolean flagYielding
) {
super(
required,
name,
- new StringArrayParser<>(),
+ new StringArrayParser<>(flagYielding),
"",
TypeToken.get(String[].class),
suggestionsProvider,
@@ -74,10 +77,35 @@ public final class StringArrayArgument extends CommandArgument {
final @NonNull BiFunction, String, List> suggestionsProvider
) {
return new StringArrayArgument<>(
- true,
+ true /* required */,
name,
suggestionsProvider,
- ArgumentDescription.empty()
+ ArgumentDescription.empty(),
+ false /* flagYielding */
+ );
+ }
+
+ /**
+ * Create a new required string array argument
+ *
+ * @param name Argument name
+ * @param flagYielding Whether the parser should stop parsing when encountering a potential flag
+ * @param suggestionsProvider Suggestions provider
+ * @param Command sender type
+ * @return Created argument
+ * @since 1.7.0
+ */
+ public static @NonNull StringArrayArgument of(
+ final @NonNull String name,
+ final boolean flagYielding,
+ final @NonNull BiFunction, String, List> suggestionsProvider
+ ) {
+ return new StringArrayArgument<>(
+ true /* required */,
+ name,
+ suggestionsProvider,
+ ArgumentDescription.empty(),
+ flagYielding
);
}
@@ -94,10 +122,35 @@ public final class StringArrayArgument extends CommandArgument {
final @NonNull BiFunction, String, List> suggestionsProvider
) {
return new StringArrayArgument<>(
- false,
+ false /* required */,
name,
suggestionsProvider,
- ArgumentDescription.empty()
+ ArgumentDescription.empty(),
+ false /* flagYielding */
+ );
+ }
+
+ /**
+ * Create a new optional string array argument
+ *
+ * @param name Argument name
+ * @param flagYielding Whether the parser should stop parsing when encountering a potential flag
+ * @param suggestionsProvider Suggestions provider
+ * @param Command sender type
+ * @return Created argument
+ * @since 1.7.0
+ */
+ public static @NonNull StringArrayArgument optional(
+ final @NonNull String name,
+ final boolean flagYielding,
+ final @NonNull BiFunction, String, List> suggestionsProvider
+ ) {
+ return new StringArrayArgument<>(
+ false /* required */,
+ name,
+ suggestionsProvider,
+ ArgumentDescription.empty(),
+ flagYielding
);
}
@@ -109,16 +162,54 @@ public final class StringArrayArgument extends CommandArgument {
*/
public static final class StringArrayParser implements ArgumentParser {
+ private static final Pattern FLAG_PATTERN = Pattern.compile("(-[A-Za-z_\\-0-9])|(--[A-Za-z_\\-0-9]*)");
+
+ private final boolean flagYielding;
+
+ /**
+ * Construct a new string array parser.
+ */
+ public StringArrayParser() {
+ this.flagYielding = false;
+ }
+
+ /**
+ * Construct a new string array parser.
+ *
+ * @param flagYielding Whether the parser should stop parsing when encountering a potential flag
+ * @since 1.7.0
+ */
+ public StringArrayParser(final boolean flagYielding) {
+ this.flagYielding = flagYielding;
+ }
+
@Override
public @NonNull ArgumentParseResult parse(
final @NonNull CommandContext<@NonNull C> commandContext,
final @NonNull Queue<@NonNull String> inputQueue
) {
- final String[] result = new String[inputQueue.size()];
- for (int i = 0; i < result.length; i++) {
- result[i] = inputQueue.remove();
+ if (this.flagYielding) {
+ final List result = new LinkedList<>();
+ final int size = inputQueue.size();
+
+ for (int i = 0; i < size; i++) {
+ final String string = inputQueue.peek();
+ if (string == null || FLAG_PATTERN.matcher(string).matches()) {
+ break;
+ }
+ inputQueue.remove();
+
+ result.add(string);
+ }
+
+ return ArgumentParseResult.success(result.toArray(new String[0]));
+ } else {
+ final String[] result = new String[inputQueue.size()];
+ for (int i = 0; i < result.length; i++) {
+ result[i] = inputQueue.remove();
+ }
+ return ArgumentParseResult.success(result);
}
- return ArgumentParseResult.success(result);
}
}
diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java
index 5a0d09d8..bd492669 100644
--- a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java
+++ b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java
@@ -28,6 +28,7 @@ import cloud.commandframework.arguments.standard.BooleanArgument;
import cloud.commandframework.arguments.standard.EnumArgument;
import cloud.commandframework.arguments.standard.IntegerArgument;
import cloud.commandframework.arguments.standard.StringArgument;
+import cloud.commandframework.arguments.standard.StringArrayArgument;
import cloud.commandframework.types.tuples.Pair;
import cloud.commandframework.types.tuples.Triplet;
import java.util.Arrays;
@@ -38,6 +39,7 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static cloud.commandframework.util.TestUtils.createManager;
+import static com.google.common.truth.Truth.assertThat;
public class CommandSuggestionsTest {
@@ -394,6 +396,107 @@ public class CommandSuggestionsTest {
Assertions.assertEquals(Collections.singletonList("later"), suggestions6);
}
+ @Test
+ void testFlagYieldingGreedyStringFollowedByFlagArgument() {
+ // Arrange
+ final CommandManager manager = createManager();
+ manager.command(
+ manager.commandBuilder("command")
+ .argument(
+ StringArgument.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 suggestions1 = manager.suggest(
+ new TestCommandSender(),
+ "command "
+ );
+ final List suggestions2 = manager.suggest(
+ new TestCommandSender(),
+ "command hel"
+ );
+ final List suggestions3 = manager.suggest(
+ new TestCommandSender(),
+ "command hello --"
+ );
+ final List suggestions4 = manager.suggest(
+ new TestCommandSender(),
+ "command hello --f"
+ );
+ final List suggestions5 = manager.suggest(
+ new TestCommandSender(),
+ "command hello -f"
+ );
+ final List suggestions6 = manager.suggest(
+ new TestCommandSender(),
+ "command hello -"
+ );
+
+ // Assert
+ assertThat(suggestions1).containsExactly("hello");
+ assertThat(suggestions2).containsExactly("hello");
+ assertThat(suggestions3).containsExactly("--flag", "--flag2");
+ assertThat(suggestions4).containsExactly("--flag", "--flag2");
+ assertThat(suggestions5).containsExactly("-f");
+ assertThat(suggestions6).containsExactly("hello");
+ }
+
+ @Test
+ void testFlagYieldingStringArrayFollowedByFlagArgument() {
+ // Arrange
+ final CommandManager manager = createManager();
+ 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 suggestions1 = manager.suggest(
+ new TestCommandSender(),
+ "command "
+ );
+ final List suggestions2 = manager.suggest(
+ new TestCommandSender(),
+ "command hello"
+ );
+ final List suggestions3 = manager.suggest(
+ new TestCommandSender(),
+ "command hello --"
+ );
+ final List suggestions4 = manager.suggest(
+ new TestCommandSender(),
+ "command hello --f"
+ );
+ final List suggestions5 = manager.suggest(
+ new TestCommandSender(),
+ "command hello -f"
+ );
+ final List suggestions6 = manager.suggest(
+ new TestCommandSender(),
+ "command hello -"
+ );
+
+ // Assert
+ assertThat(suggestions1).isEmpty();
+ assertThat(suggestions2).isEmpty();
+ assertThat(suggestions3).containsExactly("--flag", "--flag2");
+ assertThat(suggestions4).containsExactly("--flag", "--flag2");
+ assertThat(suggestions5).containsExactly("-f");
+ assertThat(suggestions6).isEmpty();
+ }
+
public enum TestEnum {
FOO,
BAR
diff --git a/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringArrayParserTest.java b/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringArrayParserTest.java
index 27dfd0c9..ab7c9c21 100644
--- a/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringArrayParserTest.java
+++ b/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringArrayParserTest.java
@@ -26,6 +26,7 @@ package cloud.commandframework.arguments.standard;
import cloud.commandframework.TestCommandSender;
import cloud.commandframework.arguments.parser.ArgumentParseResult;
import cloud.commandframework.context.CommandContext;
+import java.util.Collections;
import java.util.LinkedList;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
@@ -78,4 +79,60 @@ class StringArrayParserTest {
assertThat(input).isEmpty();
}
+
+ @Test
+ void Parse_GreedyFlagAwareLongFormFlag_EndsAfterFlag() {
+ // Arrange
+ final StringArrayArgument.StringArrayParser parser = new StringArrayArgument.StringArrayParser<>(true);
+ final LinkedList input = ArgumentTestHelper.linkedListOf(
+ "this",
+ "is",
+ "a",
+ "string",
+ "--flag",
+ "more",
+ "flag",
+ "content"
+ );
+
+ // Act
+ final ArgumentParseResult result = parser.parse(
+ this.context,
+ input
+ );
+
+ // Assert
+ assertThat(result.getFailure()).isEmpty();
+ assertThat(result.getParsedValue()).hasValue(new String[] {"this", "is", "a", "string"});
+
+ assertThat(input).containsExactly("--flag", "more", "flag", "content");
+ }
+
+ @Test
+ void Parse_GreedyFlagAwareShortFormFlag_EndsAfterFlag() {
+ // Arrange
+ final StringArrayArgument.StringArrayParser parser = new StringArrayArgument.StringArrayParser<>(true);
+ final LinkedList input = ArgumentTestHelper.linkedListOf(
+ "this",
+ "is",
+ "a",
+ "string",
+ "-f",
+ "-l",
+ "-a",
+ "-g"
+ );
+
+ // Act
+ final ArgumentParseResult result = parser.parse(
+ this.context,
+ input
+ );
+
+ // Assert
+ assertThat(result.getFailure()).isEmpty();
+ assertThat(result.getParsedValue()).hasValue(new String[] {"this", "is", "a", "string"});
+
+ assertThat(input).containsExactly("-f", "-l", "-a", "-g");
+ }
}
diff --git a/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringParserTest.java b/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringParserTest.java
new file mode 100644
index 00000000..5b3df330
--- /dev/null
+++ b/cloud-core/src/test/java/cloud/commandframework/arguments/standard/StringParserTest.java
@@ -0,0 +1,106 @@
+//
+// MIT License
+//
+// Copyright (c) 2021 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.arguments.standard;
+
+import cloud.commandframework.TestCommandSender;
+import cloud.commandframework.arguments.parser.ArgumentParseResult;
+import cloud.commandframework.context.CommandContext;
+import java.util.Collections;
+import java.util.LinkedList;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
+
+@ExtendWith(MockitoExtension.class)
+class StringParserTest {
+
+ @Mock
+ private CommandContext context;
+
+ @Test
+ void Parse_GreedyFlagAwareLongFormFlag_EndsAfterFlag() {
+ // Arrange
+ final StringArgument.StringParser parser = new StringArgument.StringParser<>(
+ StringArgument.StringMode.GREEDY_FLAG_YIELDING,
+ (context, input) -> Collections.emptyList()
+ );
+ final LinkedList input = ArgumentTestHelper.linkedListOf(
+ "this",
+ "is",
+ "a",
+ "string",
+ "--flag",
+ "more",
+ "flag",
+ "content"
+ );
+
+ // Act
+ final ArgumentParseResult result = parser.parse(
+ this.context,
+ input
+ );
+
+ // Assert
+ assertThat(result.getFailure()).isEmpty();
+ assertThat(result.getParsedValue()).hasValue("this is a string");
+
+ assertThat(input).containsExactly("--flag", "more", "flag", "content");
+ }
+
+ @Test
+ void Parse_GreedyFlagAwareShortFormFlag_EndsAfterFlag() {
+ // Arrange
+ final StringArgument.StringParser parser = new StringArgument.StringParser<>(
+ StringArgument.StringMode.GREEDY_FLAG_YIELDING,
+ (context, input) -> Collections.emptyList()
+ );
+ final LinkedList input = ArgumentTestHelper.linkedListOf(
+ "this",
+ "is",
+ "a",
+ "string",
+ "-f",
+ "-l",
+ "-a",
+ "-g"
+ );
+
+ // Act
+ final ArgumentParseResult result = parser.parse(
+ this.context,
+ input
+ );
+
+ // Assert
+ assertThat(result.getFailure()).isEmpty();
+ assertThat(result.getParsedValue()).hasValue("this is a string");
+
+ assertThat(input).containsExactly("-f", "-l", "-a", "-g");
+ }
+}
diff --git a/cloud-core/src/test/java/cloud/commandframework/issue/Issue321.java b/cloud-core/src/test/java/cloud/commandframework/issue/Issue321.java
new file mode 100644
index 00000000..459cb191
--- /dev/null
+++ b/cloud-core/src/test/java/cloud/commandframework/issue/Issue321.java
@@ -0,0 +1,83 @@
+//
+// MIT License
+//
+// Copyright (c) 2021 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.issue;
+
+import cloud.commandframework.CommandManager;
+import cloud.commandframework.TestCommandSender;
+import cloud.commandframework.arguments.flags.FlagContext;
+import cloud.commandframework.arguments.standard.StringArrayArgument;
+import cloud.commandframework.context.CommandContext;
+import cloud.commandframework.execution.CommandResult;
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+import static cloud.commandframework.util.TestUtils.createManager;
+import static com.google.common.truth.Truth8.assertThat;
+
+/**
+ * Test for https://github.com/Incendo/cloud/issues/321.
+ */
+class Issue321 {
+
+ @Test
+ void flagSucceedingFlagWithStringArrayArgument() {
+ // Arrange
+ final CommandManager commandManager = createManager();
+ commandManager.command(
+ commandManager.commandBuilder("command")
+ .flag(
+ commandManager.flagBuilder("flag1")
+ .withArgument(
+ StringArrayArgument.of(
+ "array",
+ true /* flagYielding */,
+ (context, input) -> Collections.emptyList()
+ )
+ )
+ ).flag(
+ commandManager.flagBuilder("flag2")
+ .withArgument(
+ StringArrayArgument.of(
+ "array",
+ true /* flagYielding */,
+ (context, input) -> Collections.emptyList()
+ )
+ )
+ )
+ );
+
+ // Act
+ final CommandResult result = commandManager.executeCommand(
+ new TestCommandSender(),
+ "command --flag1 one two three --flag2 1 2 3"
+ ).join();
+
+ // Assert
+ final CommandContext context = result.getCommandContext();
+ final FlagContext flags = context.flags();
+
+ assertThat(flags.getValue("flag1")).hasValue(new String[] {"one", "two", "three"});
+ assertThat(flags.getValue("flag2")).hasValue(new String[] {"1", "2", "3"});
+ }
+}