diff --git a/core/src/main/java/co/aikar/commands/BaseCommand.java b/core/src/main/java/co/aikar/commands/BaseCommand.java index b60e437df..75f72635b 100644 --- a/core/src/main/java/co/aikar/commands/BaseCommand.java +++ b/core/src/main/java/co/aikar/commands/BaseCommand.java @@ -710,7 +710,6 @@ public List tabComplete(CommandIssuer issuer, String commandLabel, Strin final CommandSearch search = findSubCommand(args, true); - final List cmds = new ArrayList<>(); if (search != null) { @@ -772,7 +771,7 @@ List getCommandsForCompletion(CommandIssuer issuer, String[] args) { * @return All results to complete the command. */ private List completeCommand(CommandIssuer issuer, RegisteredCommand cmd, String[] args, String commandLabel, boolean isAsync) { - if (!cmd.hasPermission(issuer) || args.length > cmd.consumeInputResolvers || args.length == 0 || cmd.complete == null) { + if (!cmd.hasPermission(issuer) || args.length == 0 || cmd.complete == null) { return Collections.emptyList(); } diff --git a/core/src/main/java/co/aikar/commands/CommandCompletions.java b/core/src/main/java/co/aikar/commands/CommandCompletions.java index f901fa715..37e7bca02 100644 --- a/core/src/main/java/co/aikar/commands/CommandCompletions.java +++ b/core/src/main/java/co/aikar/commands/CommandCompletions.java @@ -170,20 +170,21 @@ CommandCompletionHandler setDefaultCompletion(String id, Class... classes) { @NotNull List of(RegisteredCommand cmd, CommandIssuer sender, String[] args, boolean isAsync) { - String[] completions = ACFPatterns.SPACE.split(cmd.complete); - final int argIndex = args.length - 1; + RegisteredCommand.CommandData commandData = cmd.parseArguments(args); - String input = args[argIndex]; + final int argIndex = commandData.args.size() - 1; + String input = argIndex >= 0 ? commandData.args.get(argIndex) : ""; - String completion = argIndex < completions.length ? completions[argIndex] : null; - if (completion == null && completions.length > 0) { - completion = completions[completions.length - 1]; + // Check if its a switch + if (commandData.isSwitch) { + return commandData.switches; } - if (completion == null) { + + if (commandData.complete == null) { return Collections.singletonList(input); } - return getCompletionValues(cmd, sender, completion, args, isAsync); + return getCompletionValues(cmd, sender, commandData.complete, args, isAsync); } List getCompletionValues(RegisteredCommand command, CommandIssuer sender, String completion, String[] args, boolean isAsync) { diff --git a/core/src/main/java/co/aikar/commands/CommandParameter.java b/core/src/main/java/co/aikar/commands/CommandParameter.java index 029ff776f..823c09d34 100644 --- a/core/src/main/java/co/aikar/commands/CommandParameter.java +++ b/core/src/main/java/co/aikar/commands/CommandParameter.java @@ -23,11 +23,13 @@ package co.aikar.commands; +import co.aikar.commands.annotation.CommandCompletion; import co.aikar.commands.annotation.Conditions; import co.aikar.commands.annotation.Default; import co.aikar.commands.annotation.Description; import co.aikar.commands.annotation.Flags; import co.aikar.commands.annotation.Optional; +import co.aikar.commands.annotation.Switch; import co.aikar.commands.annotation.Syntax; import co.aikar.commands.annotation.Values; import co.aikar.commands.contexts.ContextResolver; @@ -36,6 +38,7 @@ import co.aikar.commands.contexts.OptionalContextResolver; import java.lang.reflect.Parameter; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -52,6 +55,8 @@ public class CommandParameter command, Parameter param, int par this.defaultValue = annotations.getAnnotationValue(param, Default.class, Annotations.REPLACEMENTS | (type != String.class ? Annotations.NO_EMPTY : 0)); this.description = annotations.getAnnotationValue(param, Description.class, Annotations.REPLACEMENTS | Annotations.DEFAULT_EMPTY); this.conditions = annotations.getAnnotationValue(param, Conditions.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); + this.complete = annotations.getAnnotationValue(param, CommandCompletion.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); + this.switches = annotations.getAnnotationValue(param, Switch.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); //noinspection unchecked this.resolver = manager.getCommandContexts().getResolver(type); @@ -94,9 +101,17 @@ public CommandParameter(RegisteredCommand command, Parameter param, int par this.syntax = annotations.getAnnotationValue(param, Syntax.class); if (syntax == null) { if (!requiresInput && canConsumeInput) { - this.syntax = "[" + name + "]"; + if (switches != null) { + this.syntax = "[-" + switches.split(",")[0] + " <" + name + ">]"; + } else { + this.syntax = "[" + name + "]"; + } } else if (requiresInput) { - this.syntax = "<" + name + ">"; + if (switches != null) { + this.syntax = "[-" + switches.split(",")[0] + "] " + "<" + name + ">"; + } else { + this.syntax = "<" + name + ">"; + } } } } @@ -254,4 +269,24 @@ public String getConditions() { public void setConditions(String conditions) { this.conditions = conditions; } + + public String getComplete() { + return complete; + } + + public void setComplete(String complete) { + this.complete = complete; + } + + public String getSwitches() { + return switches; + } + + public void setSwitches(String value) { + this.switches = value; + } + + public boolean hasSwitch(String value) { + return switches != null && Arrays.asList(switches.split(",")).contains(value); + } } diff --git a/core/src/main/java/co/aikar/commands/RegisteredCommand.java b/core/src/main/java/co/aikar/commands/RegisteredCommand.java index 0439db875..0034311b0 100644 --- a/core/src/main/java/co/aikar/commands/RegisteredCommand.java +++ b/core/src/main/java/co/aikar/commands/RegisteredCommand.java @@ -142,9 +142,12 @@ void invoke(CommandIssuer sender, List args, CommandOperationContext con try { this.manager.conditions.validateConditions(context); Map passedArgs = resolveContexts(sender, args); + if (passedArgs == null) return; - method.invoke(scope, passedArgs.values().toArray()); + method.invoke(scope, Arrays.stream(parameters) + .map(p -> passedArgs.getOrDefault(p.getName(), null)) + .toArray()); } catch (Exception e) { handleException(sender, args, e); } @@ -196,15 +199,17 @@ Map resolveContexts(CommandIssuer sender, List args) thr } @Nullable Map resolveContexts(CommandIssuer sender, List args, int argLimit) throws InvalidCommandArgument { - args = new ArrayList<>(args); - String[] origArgs = args.toArray(new String[args.size()]); + CommandData commandData = parseArguments(args.toArray(new String[0])); + List arguments = new ArrayList<>(commandData.args); + + String[] origArgs = arguments.toArray(new String[0]); Map passedArgs = new LinkedHashMap<>(); int remainingRequired = requiredResolvers; CommandOperationContext opContext = CommandManager.getCurrentCommandOperationContext(); - for (int i = 0; i < parameters.length && i < argLimit; i++) { - boolean isLast = i == parameters.length - 1; + for (int i = 0; i < commandData.parameters.size() && i < argLimit; i++) { + boolean isLast = i == commandData.parameters.size() - 1; boolean allowOptional = remainingRequired == 0; - final CommandParameter parameter = parameters[i]; + final CommandParameter parameter = commandData.parameters.get(i); if (parameter.isCommandIssuer()) { argLimit++; } @@ -213,14 +218,14 @@ Map resolveContexts(CommandIssuer sender, List args, int //noinspection unchecked final ContextResolver resolver = parameter.getResolver(); //noinspection unchecked - CEC context = (CEC) this.manager.createCommandContext(this, parameter, sender, args, i, passedArgs); + CEC context = (CEC) this.manager.createCommandContext(this, parameter, sender, commandData.args, i, passedArgs); boolean requiresInput = parameter.requiresInput(); if (requiresInput && remainingRequired > 0) { remainingRequired--; } - if (args.isEmpty() && !(isLast && type == String[].class)) { + if (arguments.isEmpty() && !(isLast && type == String[].class)) { if (allowOptional && parameter.getDefaultValue() != null) { - args.add(parameter.getDefaultValue()); + arguments.add(parameter.getDefaultValue()); } else if (allowOptional && parameter.isOptional()) { Object value = parameter.isOptionalResolver() ? resolver.getContext(context) : null; if (value == null && parameter.getClass().isPrimitive()) { @@ -237,7 +242,7 @@ Map resolveContexts(CommandIssuer sender, List args, int } } if (parameter.getValues() != null) { - String arg = !args.isEmpty() ? args.get(0) : ""; + String arg = !arguments.isEmpty() ? arguments.get(0) : ""; Set possible = new HashSet<>(); CommandCompletions commandCompletions = this.manager.getCommandCompletions(); @@ -318,4 +323,130 @@ public void addSubcommand(String cmd) { public void addSubcommands(Collection cmd) { this.registeredSubcommands.addAll(cmd); } + + /** + * Resolve arguments and parameters with respect to positional and named parameters + */ + public CommandData parseArguments(String[] args) { + + // Command completions + List completions = new ArrayList<>(); + if (complete != null) { + completions.addAll(Arrays.asList(ACFPatterns.SPACE.split(complete))); + } + + // List of all parameters + List parameters = Arrays.stream(this.parameters).collect(Collectors.toList()); + + // Return data + CommandData data = new CommandData(); + + // All Arguments + List arguments = Arrays.stream(args).collect(Collectors.toList()); + boolean isSwitch = false; + + // Current Parameter + CommandParameter currentParameter = null; + + // Current Argument + String currentArg = null; + + while (arguments.size() > 0 && parameters.size() > 0) { + currentArg = arguments.remove(0); + currentParameter = null; + + // Check if its a switch + if (currentArg.startsWith("-")) { + isSwitch = true; + + String finalArg = currentArg; + currentParameter = parameters.stream() + .filter(CommandParameter::canConsumeInput) + .filter(p -> p.hasSwitch(finalArg.substring(1))) + .findFirst() + .orElse(null); + + if (currentParameter != null) { + + parameters.remove(currentParameter); + + // Arguments consumed + if (arguments.size() > 0) { + isSwitch = false; + currentArg = arguments.remove(0); + } else { + currentArg = null; + } + + if (currentParameter.getComplete() != null) { + data.complete = currentParameter.getComplete(); + } + } + } + + // Deal with positional parameter + if (currentParameter == null) { + while(parameters.size() > 0) { + currentParameter = parameters.remove(0); + if (!currentParameter.canConsumeInput()) { + data.parameters.add(currentParameter); + currentParameter = null; + } else { + break; + } + } + + if (currentParameter == null) { + break; + } + + // Save completions + if (currentParameter.getComplete() != null) { + data.complete = currentParameter.getComplete(); + } else { + data.complete = completions.size() > 0 ? completions.remove(0) : null; + } + } + + // Save switch result + if (arguments.size() < 1 && currentParameter.getSwitches() != null) { + data.switches.add("-" + currentParameter.getSwitches().split(",")[0]); + } + + // Save arg + if (currentArg != null) { + data.args.add(currentArg); + } + + // Save Parameter + data.parameters.add(currentParameter); + } + + // Add rest of data + data.switches.addAll(parameters.stream() + .filter(p -> p.getSwitches() != null) + .map(p -> "-" + p.getSwitches().split(",")[0]) + .collect(Collectors.toList())); + + if (arguments.size() > 0) { + isSwitch = false; + data.args.addAll(arguments); + data.complete = null; + } + data.parameters.addAll(parameters); + + data.isSwitch = isSwitch; + + return data; + } + + public static class CommandData { + + public List parameters = new ArrayList<>(); + public List args = new ArrayList<>(); + public String complete = null; + public List switches = new ArrayList<>(); + public boolean isSwitch = true; + } + } diff --git a/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java b/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java index 8ad0142ea..2f1223885 100644 --- a/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java +++ b/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java @@ -36,10 +36,13 @@ * or special @codes that let you define Completion Handlers to dynamically * populate completion values. * + * If used on a parameter it provides completion(s) for that parameter and will not + * consume any provided on the Method + * * @see {@link co.aikar.commands.CommandCompletions} */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) +@Target({ElementType.METHOD, ElementType.PARAMETER}) public @interface CommandCompletion { String value(); } diff --git a/core/src/main/java/co/aikar/commands/annotation/Switch.java b/core/src/main/java/co/aikar/commands/annotation/Switch.java new file mode 100644 index 000000000..1dc38b0b9 --- /dev/null +++ b/core/src/main/java/co/aikar/commands/annotation/Switch.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-2018 Daniel Ennis (Aikar) - MIT License + * + * 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 co.aikar.commands.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Sets the name that the parameter can be accessed with + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.PARAMETER}) +public @interface Switch { + String value(); +}