diff --git a/arc-core/src/arc/util/CommandHandler.java b/arc-core/src/arc/util/CommandHandler.java index a125235cc..222a7f282 100644 --- a/arc-core/src/arc/util/CommandHandler.java +++ b/arc-core/src/arc/util/CommandHandler.java @@ -1,90 +1,66 @@ package arc.util; -import arc.struct.Seq; -import arc.struct.ObjectMap; -import arc.func.Cons; +import arc.func.*; +import arc.struct.*; +import arc.util.command.*; -/** Parses command syntax. */ +/** + * Parses command syntax. + */ public class CommandHandler{ - public String prefix = ""; - private final ObjectMap commands = new ObjectMap<>(); private final Seq orderedCommands = new Seq<>(); + public String prefix = ""; - /** Creates a command handler with a specific command prefix.*/ + /** + * Creates a command handler with a specific command prefix. + */ public CommandHandler(String prefix){ this.prefix = prefix; } + public String getPrefix(){ + return prefix; + } + public void setPrefix(String prefix){ this.prefix = prefix; } - - public String getPrefix(){ - return prefix; - } - /** Handles a message with no additional parameters.*/ + /** + * Handles a message with no additional parameters. + */ public CommandResponse handleMessage(String message){ return handleMessage(message, null); } - /** Handles a message with optional extra parameters. Runs the command if successful. - * @return a response detailing whether or not the command was handled, and what went wrong, if applicable. */ + /** + * Handles a message with optional extra parameters. Runs the command if successful. + * @return a response detailing whether or not the command was handled, and what went wrong, if applicable. + */ public CommandResponse handleMessage(String message, Object params){ if(message == null || (!message.startsWith(prefix))) return new CommandResponse(ResponseType.noCommand, null, null); - message = message.substring(prefix.length()); - - String commandstr = message.contains(" ") ? message.substring(0, message.indexOf(" ")) : message; - String argstr = message.contains(" ") ? message.substring(commandstr.length() + 1) : ""; - - Seq result = new Seq<>(); - + int spaceIndex = message.indexOf(" "); + String commandstr = spaceIndex != -1 ? message.substring(prefix.length(), spaceIndex) : message.substring(prefix.length()); Command command = commands.get(commandstr); if(command != null){ - int index = 0; - boolean satisfied = false; - - while(true){ - if(index >= command.params.length && !argstr.isEmpty()){ - return new CommandResponse(ResponseType.manyArguments, command, commandstr); - }else if(argstr.isEmpty()) break; - - if(command.params[index].optional || index >= command.params.length - 1 || command.params[index + 1].optional){ - satisfied = true; - } - - if(command.params[index].variadic){ - result.add(argstr); - break; - } - - int next = argstr.indexOf(" "); - if(next == -1){ - if(!satisfied){ - return new CommandResponse(ResponseType.fewArguments, command, commandstr); - } - result.add(argstr); - break; - }else{ - String arg = argstr.substring(0, next); - argstr = argstr.substring(arg.length() + 1); - result.add(arg); - } - - index++; + CommandParamSplitter.SplitResponse splitResponse; + if(spaceIndex == -1){ + splitResponse = CommandParamSplitter.split("", 0, 0, command.params); + }else{ + splitResponse = CommandParamSplitter.split(message, spaceIndex + 1, message.length(), command.params); } - - if(!satisfied && command.params.length > 0 && !command.params[0].optional){ + if(splitResponse.many){ + return new CommandResponse(ResponseType.manyArguments, command, commandstr); + }else if(splitResponse.few){ return new CommandResponse(ResponseType.fewArguments, command, commandstr); } - command.runner.accept(result.toArray(String.class), params); - + command.runner.accept(splitResponse.args, params); return new CommandResponse(ResponseType.valid, command, commandstr); }else{ return new CommandResponse(ResponseType.unknownCommand, null, commandstr); @@ -98,19 +74,23 @@ public void removeCommand(String text){ orderedCommands.remove(c); } - /** Register a command which handles a zero-sized list of arguments and one parameter.*/ + /** + * Register a command which handles a zero-sized list of arguments and one parameter. + */ public Command register(String text, String description, CommandRunner runner){ return register(text, "", description, runner); } - /** Register a command which handles a list of arguments and one handler-specific parameter.
+ /** + * Register a command which handles a list of arguments and one handler-specific parameter.
* argeter syntax is as follows:
* <mandatory-arg-1> <mandatory-arg-2> ... <mandatory-arg-n> [optional-arg-1] [optional-arg-2]
* Angle brackets indicate mandatory arguments. Square brackets to indicate optional arguments.
* All mandatory arguments must come before optional arguments. Arg names must not have spaces in them.
- * You may also use the ... syntax after the arg name to designate that everything after it will not be split into extra arguments. + * You may also use the ... syntax after the arg name to designate that everything after it will not be split into extra arguments. * There may only be one such argument, and it must be at the end. For example, the syntax - * <arg1> [arg2...] will require a first argument, and then take any text after that and put it in the second argument, optionally.*/ + * <arg1> [arg2...] will require a first argument, and then take any text after that and put it in the second argument, optionally. + */ public Command register(String text, String params, String description, CommandRunner runner){ //remove previously registered commands orderedCommands.remove(c -> c.text.equals(text)); @@ -137,11 +117,15 @@ public enum ResponseType{ noCommand, unknownCommand, fewArguments, manyArguments, valid } + public interface CommandRunner{ + void accept(String[] args, T parameter); + } + public static class Command{ public final String text; public final String paramText; public final String description; - public final CommandParam[] params; + public final CommandParams params; final CommandRunner runner; public Command(String text, String paramText, String description, CommandRunner runner){ @@ -149,55 +133,10 @@ public Command(String text, String paramText, String description, CommandRunner this.paramText = paramText; this.runner = runner; this.description = description; - - String[] psplit = paramText.split(" "); - if(paramText.length() == 0){ - params = new CommandParam[0]; - }else{ - params = new CommandParam[psplit.length]; - - boolean hadOptional = false; - - for(int i = 0; i < params.length; i++){ - String param = psplit[i]; - - if(param.length() <= 2) throw new IllegalArgumentException("Malformed param '" + param + "'"); - - char l = param.charAt(0), r = param.charAt(param.length() - 1); - boolean optional, variadic = false; - - if(l == '<' && r == '>'){ - if(hadOptional) - throw new IllegalArgumentException("Can't have non-optional param after optional param!"); - optional = false; - }else if(l == '[' && r == ']'){ - optional = true; - }else{ - throw new IllegalArgumentException("Malformed param '" + param + "'"); - } - - if(optional) hadOptional = true; - - String fname = param.substring(1, param.length() - 1); - if(fname.endsWith("...")){ - if(i != params.length - 1) - throw new IllegalArgumentException("A variadic parameter should be the last parameter!"); - - fname = fname.substring(0, fname.length() - 3); - variadic = true; - } - - params[i] = new CommandParam(fname, optional, variadic); - - } - } + params = CommandParamParser.parse(paramText); } } - public interface CommandRunner{ - void accept(String[] args, T parameter); - } - public static class CommandParam{ public final String name; public final boolean optional; diff --git a/arc-core/src/arc/util/command/CommandParamParseException.java b/arc-core/src/arc/util/command/CommandParamParseException.java new file mode 100644 index 000000000..333b0d186 --- /dev/null +++ b/arc-core/src/arc/util/command/CommandParamParseException.java @@ -0,0 +1,44 @@ +package arc.util.command; + +public class CommandParamParseException extends RuntimeException{ + public final int startIndex; + public final int endIndex; + public final String rawText; + + public CommandParamParseException(String message, int startIndex, int endIndex, String rawText){ + super(calculateMessage(message, rawText, startIndex, endIndex)); + this.startIndex = startIndex; + this.endIndex = endIndex; + this.rawText = rawText; + + } + + + private static String calculateMessage(String message, String rawText, int startIndex, int endIndex){ + StringBuilder builder = new StringBuilder(message); + appendRanges(builder, startIndex, endIndex); + builder.append("\n").append(rawText).append("\n"); + for(int i = 0; i < startIndex; i++){ + builder.append(" "); + } + for(int i = startIndex; i < endIndex; i++){ + builder.append("^"); + } + builder.append("\n"); + for(int i = 0; i < startIndex; i++){ + builder.append(" "); + } + builder.append(message); +// appendRanges(builder, startIndex, endIndex); + return builder.toString(); + } + + private static void appendRanges(StringBuilder builder, int startIndex, int endIndex){ + builder.append(" at "); + builder.append('['); + builder.append(startIndex); + builder.append(":"); + builder.append(endIndex); + builder.append(']'); + } +} diff --git a/arc-core/src/arc/util/command/CommandParamParser.java b/arc-core/src/arc/util/command/CommandParamParser.java new file mode 100644 index 000000000..e70b02166 --- /dev/null +++ b/arc-core/src/arc/util/command/CommandParamParser.java @@ -0,0 +1,137 @@ +package arc.util.command; + +import arc.struct.*; +import arc.util.*; +import arc.util.pooling.*; + +public class CommandParamParser{ + private static final byte searchParam = 0; + private static final byte parsingRequired = 1; + private static final byte parsingOptional = 2; + private static final Seq tmpRegions = new Seq<>(); + private static final Pool textRegionPool = new Pool(){ + @Override + protected TextRegion newObject(){ + return new TextRegion(-1, -1){ + }; + } + }; + + public static CommandParams parse(String text) throws CommandParamParseException{ + byte state = searchParam; + int begin = -1; + clear(); + for(int i = 0; i < text.length(); i++){ + char c = text.charAt(i); + switch(state){ + case searchParam: + if(c != ' ' && c != '<' && c != '[') + throwException("Unexpected char '" + c + "'", i, text); + if(c == '<' || c == '['){ + state = c == '<' ? parsingRequired : parsingOptional; + begin = i; + } + break; + + case parsingRequired: + if(c == '>'){ + state = completeParam(text, begin, i + 1); + } + break; + + case parsingOptional: + if(c == ']'){ + state = completeParam(text, begin, i + 1); + } + break; + + } + } + CommandHandler.CommandParam[] params = new CommandHandler.CommandParam[tmpRegions.size]; + boolean wasVariadic = false; + for(int i = 0; i < tmpRegions.size; i++){ + TextRegion region = tmpRegions.get(i); + boolean isVariadic = false; + int nameOffset = 0; + if(region.length() > 5){ + for(int j = 0; ; j++){ + if(text.charAt(region.end - i - 1) != '.') break; + if(j == 2){ + if(wasVariadic){ + + throwException("Cannot be more than one variadic parameter!", region, text); + } + isVariadic = wasVariadic = true; + nameOffset = 3; + break; + } + } + } + params[i] = new CommandHandler.CommandParam( + text.substring(region.start + 1, region.end - 1 - nameOffset), + text.charAt(region.start) == '[', + isVariadic + ); + } + clear(); + return new CommandParams(params); + } + + private static void clear(){ + textRegionPool.freeAll(tmpRegions); + tmpRegions.clear(); + } + + private static byte completeParam(String text, int begin, int end){ + if(end - begin <= 2){ + throwException("Malformed param '" + text.substring(begin, end) + "'", + begin, end, text + + ); + } + tmpRegions.add(textRegion(begin, end)); + return searchParam; + } + + private static TextRegion textRegion(int begin, int end){ + + return textRegionPool.obtain().set(begin, end); + } + + static void throwException(String message, int startIndex, int endIndex, String rawText){ + throw new CommandParamParseException(message, startIndex, endIndex, rawText); + } + + static void throwException(String message, int symbolIndex, String rawText){ + throw new CommandParamParseException(message, symbolIndex, symbolIndex + 1, rawText); + } + + static void throwException(String message, TextRegion region, String rawText){ + throw new CommandParamParseException(message, region.start, region.end, rawText); + } + + + private static abstract class TextRegion{ + public int start; + public int end; + + private TextRegion(int start, int end){ + this.start = start; + this.end = end; + } + + public TextRegion set(int start, int end){ + this.start = start; + this.end = end; + return this; + } + + public int length(){ + return end - start; + } + + public String substring(String text){ + return text.substring(start, end); + } + } +} diff --git a/arc-core/src/arc/util/command/CommandParamSplitter.java b/arc-core/src/arc/util/command/CommandParamSplitter.java new file mode 100644 index 000000000..503106148 --- /dev/null +++ b/arc-core/src/arc/util/command/CommandParamSplitter.java @@ -0,0 +1,86 @@ +package arc.util.command; + +import arc.struct.*; + +public class CommandParamSplitter{ + private static final SplitResponse response = new SplitResponse(); + private static final String[] emptyStringArray = {}; + private static IntSeq tmpSeq = new IntSeq(); + + public static SplitResponse split(String text, CommandParams pattern){ + return split(text, 0, text.length(), pattern); + } + + public static SplitResponse split(String text, int startIndex, int endIndex, CommandParams pattern){ + if(endIndex - startIndex == 0){ + return pattern.requiredAmount == 0 ? response.args(emptyStringArray) : response.few(); + } + int spaces = 0; + tmpSeq.clear(); + tmpSeq.add(startIndex - 1); + for(int i = startIndex; i < endIndex; i++){ + if(text.charAt(i) == ' '){ + tmpSeq.add(i); + spaces++; + if(spaces % 5 == 0){ + if(spaces + 1 > pattern.params.length && pattern.variadicIndex == -1) return response.many(); + } + } + } + + if(spaces + 1 < pattern.requiredAmount) return response.few(); + + int expandVariadic = spaces + 1 - pattern.params.length; + if(expandVariadic > 0 && pattern.variadicIndex == -1) return response.many(); + int givenParams = Math.min(pattern.params.length, spaces + 1); + int optionalLeft = givenParams - pattern.requiredAmount; + String[] resultArgs = new String[givenParams]; + tmpSeq.add(endIndex); + for(int paramIndex = 0, spaceIndex = 0, argIndex = 0; paramIndex < pattern.params.length; paramIndex++){ + + if(pattern.params[paramIndex].optional){ + if(optionalLeft <= 0){ + continue; + }else optionalLeft--; + } + int begin = tmpSeq.get(spaceIndex) + 1; + if(pattern.variadicIndex == paramIndex && expandVariadic > 0){ + spaceIndex += expandVariadic; + } + int end = tmpSeq.get(spaceIndex + 1); + resultArgs[argIndex] = text.substring(begin, end); + argIndex++; + spaceIndex++; + } + return response.args(resultArgs); + } + + public static class SplitResponse{ + public boolean many; + public boolean few; + public String[] args; + + public SplitResponse many(){ + reset(); + this.many = true; + return this; + } + + private void reset(){ + few = many = false; + args = null; + } + + public SplitResponse few(){ + reset(); + this.few = true; + return this; + } + + public SplitResponse args(String[] args){ + reset(); + this.args = args; + return this; + } + } +} diff --git a/arc-core/src/arc/util/command/CommandParams.java b/arc-core/src/arc/util/command/CommandParams.java new file mode 100644 index 000000000..47a51a5c5 --- /dev/null +++ b/arc-core/src/arc/util/command/CommandParams.java @@ -0,0 +1,17 @@ +package arc.util.command; + +import arc.util.*; + +public class CommandParams{ + public final CommandHandler.CommandParam[] params; + public final int variadicIndex; + public final int requiredAmount; + + public CommandParams(CommandHandler.CommandParam[] params){ + this.params = params; + variadicIndex = Structs.indexOf(params, it -> it.variadic); + requiredAmount = Structs.count(params, it -> !it.optional); + + + } +} diff --git a/arc-core/test/utils/command/ParserTest.java b/arc-core/test/utils/command/ParserTest.java new file mode 100644 index 000000000..d446bb8af --- /dev/null +++ b/arc-core/test/utils/command/ParserTest.java @@ -0,0 +1,48 @@ +package utils.command; + +import arc.util.command.CommandParamParseException; +import arc.util.command.CommandParamParser; +import org.junit.Assert; +import org.junit.Test; + +public class ParserTest { + + static void checkError(String message, Runnable runnable) { + try { + runnable.run(); + if (message != null) Assert.fail("Expected error with message: '" + message + "'"); + } catch (CommandParamParseException e) { + Assert.assertEquals(message, e.getMessage()); + } + } + + @Test() + public void testUnexpectedChar() { + + checkError("Unexpected char 'd' at [11:12]\n" + + "[a] d[c] \n" + + " ^\n" + + " Unexpected char 'd'", () -> { + //noinspection ParameterOrder,VariadicParamPosition + CommandParamParser.parse("[a] d[c] "); + }); + checkError("Cannot be more than one variadic parameter! at [11:17]\n" + + "[a] [c...] \n" + + " ^^^^^^\n" + + " Cannot be more than one variadic parameter!", () -> { + //noinspection ParameterOrder,VariadicParamPosition + CommandParamParser.parse("[a] [c...] "); + }); + checkError("Malformed param '<>' at [15:17]\n" + + "[a] [c] <>\n" + + " ^^\n" + + " Malformed param '<>'", () -> { + //noinspection ParameterOrder,VariadicParamPosition + CommandParamParser.parse("[a] [c] <>"); + }); + checkError(null, () -> { + //noinspection ParameterOrder,VariadicParamPosition + CommandParamParser.parse("[a][c]"); + }); + } +} diff --git a/arc-core/test/utils/command/SplitterTest.java b/arc-core/test/utils/command/SplitterTest.java new file mode 100644 index 000000000..e1cfed5fd --- /dev/null +++ b/arc-core/test/utils/command/SplitterTest.java @@ -0,0 +1,49 @@ +package utils.command; + +import arc.util.command.CommandParamParser; +import arc.util.command.CommandParamSplitter; +import arc.util.command.CommandParams; +import org.junit.Assert; +import org.junit.Test; + +public class SplitterTest { + static T[] array(T... array) { + return array; + } + + @Test + public void test() { + //noinspection ParameterOrder,VariadicParamPosition + CommandParams params = CommandParamParser.parse("[a] [c] "); + Assert.assertTrue(CommandParamSplitter.split("1", params).few); + + //noinspection ParameterOrder,VariadicParamPosition + Assert.assertTrue(CommandParamSplitter.split("1 2 3 4", CommandParamParser.parse(" ")).many); + + Assert.assertTrue( + CommandParamSplitter.split("", CommandParamParser.parse("<1>")).few + ); + + + Assert.assertArrayEquals( + array("1", "2"), + CommandParamSplitter.split("1 2", params).args + ); + Assert.assertArrayEquals( + array("1", "2", "3"), + CommandParamSplitter.split("1 2 3", params).args + ); + Assert.assertArrayEquals( + array("1", "2", "3", "4"), + CommandParamSplitter.split("1 2 3 4", params).args + ); + Assert.assertArrayEquals( + array("1", "2 3", "4", "5"), + CommandParamSplitter.split("1 2 3 4 5", params).args + ); + Assert.assertArrayEquals( + array(), + CommandParamSplitter.split("", CommandParamParser.parse("")).args + ); + } +}