From 14b240137440aba57741ba6b848ded18f424a4b5 Mon Sep 17 00:00:00 2001 From: Eric Bottard Date: Mon, 19 Jun 2017 09:35:11 +0200 Subject: [PATCH] Reconsider how input buffer is tokenized Fixes #85 --- .../org/springframework/shell2/Shell.java | 70 +++++++++++-------- .../shell2/jline/JLineShell.java | 1 - .../shell2/samples/standard/Commands.java | 2 +- .../standard/StandardParameterResolver.java | 9 +-- 4 files changed, 45 insertions(+), 37 deletions(-) diff --git a/spring-shell2-core/src/main/java/org/springframework/shell2/Shell.java b/spring-shell2-core/src/main/java/org/springframework/shell2/Shell.java index 793f3177..880ea325 100644 --- a/spring-shell2-core/src/main/java/org/springframework/shell2/Shell.java +++ b/spring-shell2-core/src/main/java/org/springframework/shell2/Shell.java @@ -97,21 +97,19 @@ public class Shell implements CommandRegistry { resultHandler.handleResult(e); continue; } - if (input.words().isEmpty()) { + if (noInput(input)) { continue; } String line = input.words().stream().collect(Collectors.joining(" ")).trim(); - List words = input.words(); - String command = findLongestCommand(line); + List words = input.words(); Object result; if (command != null) { - int wordsUsedForCommandKey = command.split(" ").length; MethodTarget methodTarget = methodTargets.get(command); - List wordsForArgs = words.subList(wordsUsedForCommandKey, words.size()); + List wordsForArgs = wordsForArguments(command, words); Method method = methodTarget.getMethod(); try { @@ -130,6 +128,29 @@ public class Shell implements CommandRegistry { } } + /** + * Return true if the parsed input ends up being empty (e.g. hitting ENTER on an empty line or blank space) + */ + private boolean noInput(Input input) { + return input.words().isEmpty() + || (input.words().size() == 1 && input.words().get(0).trim().isEmpty()); + } + + /** + * Returns the list of words to be considered for argument resolving. Drops the first N words used for the + * command, as well as an optional empty word at the end of the list (which may be present if user added spaces + * before submitting the buffer) + */ + private List wordsForArguments(String command, List words) { + int wordsUsedForCommandKey = command.split(" ").length; + List args = words.subList(wordsUsedForCommandKey, words.size()); + int last = args.size() - 1; + if (last >= 0 && "".equals(args.get(last))) { + args.remove(last); + } + return args; + } + /** * Gather completion proposals given some (incomplete) input the user has already typed in. * When and how this method is invoked is implementation specific and decided by the actual user interface. @@ -139,39 +160,30 @@ public class Shell implements CommandRegistry { String prefix = context.upToCursor(); List candidates = new ArrayList<>(); - // Find the longest match for a command name with words in the buffer - String best = findLongestCommand(prefix); - if (best == null) { // no command found - candidates.addAll(commandsStartingWith(prefix)); - return candidates; - } // if we're here, we're either trying to complete args for command (will fall through) - // or trying to complete command whose name starts with (which also happens to be a command) - else if (prefix.equals(best)) { - candidates.addAll(commandsStartingWith(best)); - } // valid command () followed by a suffix (but not necessarily [ args*]) - else if (!prefix.startsWith(best + " ")) { - // must be an invalid command, can't do anything - return candidates; - } + candidates.addAll(commandsStartingWith(prefix)); - CompletionContext argsContext = context.drop(best.split(" ").length); - // Try to complete arguments - MethodTarget methodTarget = methodTargets.get(best); - Method method = methodTarget.getMethod(); - return Arrays.stream(method.getParameters()) - .map(Utils::createMethodParameter) - .flatMap(mp -> findResolver(mp).complete(mp, argsContext).stream()) - .collect(Collectors.toList()); + String best = findLongestCommand(prefix); + if (best != null) { + CompletionContext argsContext = context.drop(best.split(" ").length); + // Try to complete arguments + MethodTarget methodTarget = methodTargets.get(best); + Method method = methodTarget.getMethod(); + Arrays.stream(method.getParameters()) + .map(Utils::createMethodParameter) + .flatMap(mp -> findResolver(mp).complete(mp, argsContext).stream()) + .forEach(candidates::add); + } + return candidates; } private List commandsStartingWith(String prefix) { return methodTargets.entrySet().stream() .filter(e -> e.getKey().startsWith(prefix)) - .map(e -> toCompletionProposal(e.getKey(), e.getValue())) + .map(e -> toCommandProposal(e.getKey(), e.getValue())) .collect(Collectors.toList()); } - private CompletionProposal toCompletionProposal(String command, MethodTarget methodTarget) { + private CompletionProposal toCommandProposal(String command, MethodTarget methodTarget) { return new CompletionProposal(command) .dontQuote(true) .category("Available commands") diff --git a/spring-shell2-core/src/main/java/org/springframework/shell2/jline/JLineShell.java b/spring-shell2-core/src/main/java/org/springframework/shell2/jline/JLineShell.java index 642cf507..320b2183 100644 --- a/spring-shell2-core/src/main/java/org/springframework/shell2/jline/JLineShell.java +++ b/spring-shell2-core/src/main/java/org/springframework/shell2/jline/JLineShell.java @@ -132,7 +132,6 @@ public class JLineShell { words = words.stream() .map(s -> s.replaceAll("^\\n+|\\n+$", "")) // CR at beginning/end of line introduced by backslash continuation .map(s -> s.replaceAll("\\n+", " ")) // CR in middle of word introduced by return inside a quoted string - .filter(w -> w.length() > 0) // Empty word introduced when using quotes, no idea why... .collect(Collectors.toList()); return words; } diff --git a/spring-shell2-samples/src/main/java/org/springframework/shell2/samples/standard/Commands.java b/spring-shell2-samples/src/main/java/org/springframework/shell2/samples/standard/Commands.java index fe277f73..3d9c8535 100644 --- a/spring-shell2-samples/src/main/java/org/springframework/shell2/samples/standard/Commands.java +++ b/spring-shell2-samples/src/main/java/org/springframework/shell2/samples/standard/Commands.java @@ -38,7 +38,7 @@ import org.springframework.stereotype.Component; @ShellComponent("") public class Commands { - @ShellMethod(help = "a command whose name looks the same as another one") + @ShellMethod(help = "a command whose name looks the same as another one", value = "help me out") public void helpMeOut() { System.out.println("You can go"); } diff --git a/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/StandardParameterResolver.java b/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/StandardParameterResolver.java index 57939dfd..9a2e9d19 100644 --- a/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/StandardParameterResolver.java +++ b/spring-shell2-standard/src/main/java/org/springframework/shell2/standard/StandardParameterResolver.java @@ -297,7 +297,7 @@ public class StandardParameterResolver implements ParameterResolver { if (!set) { if (unfinished == null) { // case 1 above - return commandsThatStartWithContextPrefix(methodParameter, context); + return argumentKeysThatStartWithContextPrefix(methodParameter, context); } // case 2 else { return valueCompletions(methodParameter, context); @@ -306,9 +306,6 @@ public class StandardParameterResolver implements ParameterResolver { else { List result = new ArrayList<>(); - String prefix = context.currentWordUpToCursor() != null ? context.currentWordUpToCursor() : ""; - // TODO: should not look at last word only, but everything after what was used for key - Object value = convertRawValue(parameterRawValue, methodParameter); if (value instanceof Collection && ((Collection) value).size() == arity || (ObjectUtils.isArray(value) && Array.getLength(value) == arity)) { @@ -320,7 +317,7 @@ public class StandardParameterResolver implements ParameterResolver { if (parameterRawValue.positional()) { // Case 4.1: There exists "--command foo" and user has typed "--comm" which (wrongly) got resolved as a positional param - result.addAll(commandsThatStartWithContextPrefix(methodParameter, context)); + result.addAll(argumentKeysThatStartWithContextPrefix(methodParameter, context)); } return result; } @@ -333,7 +330,7 @@ public class StandardParameterResolver implements ParameterResolver { .findFirst().orElseGet(() -> Collections.emptyList()); } - private List commandsThatStartWithContextPrefix(MethodParameter methodParameter, CompletionContext context) { + private List argumentKeysThatStartWithContextPrefix(MethodParameter methodParameter, CompletionContext context) { String prefix = context.currentWordUpToCursor() != null ? context.currentWordUpToCursor() : ""; return describe(methodParameter).keys().stream() .filter(k -> k.startsWith(prefix))