diff --git a/.gitignore b/.gitignore index 3c1c2aa0..6189b59e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ *.ipr *.iws target +/.settings +/.classpath +/.project diff --git a/pom.xml b/pom.xml index 0da489dc..1bb9f56e 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ jline jline JLine - 2.11-SNAPSHOT + 2.12-SNAPSHOT diff --git a/src/main/java/jline/UnixTerminal.java b/src/main/java/jline/UnixTerminal.java index f332d572..cb722fe8 100644 --- a/src/main/java/jline/UnixTerminal.java +++ b/src/main/java/jline/UnixTerminal.java @@ -48,9 +48,10 @@ public void init() throws Exception { setAnsiSupported(true); - // set the console to be character-buffered instead of line-buffered - // also make sure we're distinguishing carriage return from newline - settings.set("-icanon min 1 -icrnl -inlcr"); + // Set the console to be character-buffered instead of line-buffered. + // Make sure we're distinguishing carriage return from newline. + // Allow ctrl-s keypress to be used (as forward search) + settings.set("-icanon min 1 -icrnl -inlcr -ixon"); setEchoEnabled(false); } diff --git a/src/main/java/jline/console/ConsoleReader.java b/src/main/java/jline/console/ConsoleReader.java index a229f839..c2425e79 100644 --- a/src/main/java/jline/console/ConsoleReader.java +++ b/src/main/java/jline/console/ConsoleReader.java @@ -161,10 +161,10 @@ public class ConsoleReader private String commentBegin = null; private boolean skipLF = false; - + /** * Set to true if the reader should attempt to detect copy-n-paste. The - * effect of this that an attempt is made to detect if tab is quickly + * effect of this that an attempt is made to detect if tab is quickly * followed by another character, then it is assumed that the tab was * a literal tab as part of a copy-and-paste operation and is inserted as * such. @@ -188,6 +188,7 @@ private static enum State { * In the middle of a emacs seach */ SEARCH, + FORWARD_SEARCH, /** * VI "yank-to" operation ("y" during move mode) */ @@ -340,7 +341,7 @@ public boolean getExpandEvents() { public void setCopyPasteDetection(final boolean onoff) { copyPasteDetection = onoff; } - + /** * @return true if copy and paste detection is enabled. */ @@ -601,7 +602,10 @@ final String finishBuffer() throws IOException { // FIXME: Package protected bec if (expandEvents) { str = expandEvents(str); - historyLine = str.replaceAll("\\!", "\\\\!"); + // all post-expansion occurrences of '!' must have been escaped, so re-add escape to each + historyLine = str.replace("!", "\\!"); + // only leading '^' results in expansion, so only re-add escape for that case + historyLine = historyLine.replaceAll("^\\^", "\\\\^"); } // we only add it to the history if the buffer is not empty @@ -630,20 +634,22 @@ final String finishBuffer() throws IOException { // FIXME: Package protected bec */ protected String expandEvents(String str) throws IOException { StringBuilder sb = new StringBuilder(); - boolean escaped = false; for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); - if (escaped) { - sb.append(c); - escaped = false; - continue; - } else if (c == '\\') { - escaped = true; - continue; - } else { - escaped = false; - } switch (c) { + case '\\': + // any '\!' should be considered an expansion escape, so skip expansion and strip the escape character + // a leading '\^' should be considered an expansion escape, so skip expansion and strip the escape character + // otherwise, add the escape + if (i + 1 < str.length()) { + char nextChar = str.charAt(i+1); + if (nextChar == '!' || (nextChar == '^' && i == 0)) { + c = nextChar; + i++; + } + } + sb.append(c); + break; case '!': if (i + 1 < str.length()) { c = str.charAt(++i); @@ -707,14 +713,14 @@ protected String expandEvents(String str) throws IOException { throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); } if (neg) { - if (idx < history.size()) { + if (idx > 0 && idx <= history.size()) { rep = (history.get(history.index() - idx)).toString(); } else { throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); } } else { - if (idx >= history.index() - history.size() && idx < history.index()) { - rep = (history.get(idx)).toString(); + if (idx > history.index() - history.size() && idx <= history.index()) { + rep = (history.get(idx - 1)).toString(); } else { throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); } @@ -761,9 +767,6 @@ protected String expandEvents(String str) throws IOException { break; } } - if (escaped) { - sb.append('\\'); - } String result = sb.toString(); if (!str.equals(result)) { print(result); @@ -1910,10 +1913,10 @@ public String accept() throws IOException { } private void abort() throws IOException { - beep(); - buf.clear(); - println(); - redrawLine(); + beep(); + buf.clear(); + println(); + redrawLine(); } /** @@ -2348,36 +2351,57 @@ public String readLine(String prompt, final Character mask) throws IOException { // Note that we have to do this first, because if there is a command // not linked to a search command, we leave the search mode and fall // through to the normal state. - if (state == State.SEARCH) { + if (state == State.SEARCH || state == State.FORWARD_SEARCH) { int cursorDest = -1; switch ( ((Operation) o )) { case ABORT: state = State.NORMAL; + buf.clear(); + buf.buffer.append(searchTerm); break; case REVERSE_SEARCH_HISTORY: case HISTORY_SEARCH_BACKWARD: + state = State.SEARCH; if (searchTerm.length() == 0) { searchTerm.append(previousSearchTerm); } - if (searchIndex == -1) { - searchIndex = searchBackwards(searchTerm.toString()); - } else { + if (searchIndex > 0) { searchIndex = searchBackwards(searchTerm.toString(), searchIndex); } break; + case FORWARD_SEARCH_HISTORY: + case HISTORY_SEARCH_FORWARD: + state = State.FORWARD_SEARCH; + if (searchTerm.length() == 0) { + searchTerm.append(previousSearchTerm); + } + + if (searchIndex > -1 && searchIndex < history.size() - 1) { + searchIndex = searchForwards(searchTerm.toString(), searchIndex); + } + break; + case BACKWARD_DELETE_CHAR: if (searchTerm.length() > 0) { searchTerm.deleteCharAt(searchTerm.length() - 1); - searchIndex = searchBackwards(searchTerm.toString()); + if (state == State.SEARCH) { + searchIndex = searchBackwards(searchTerm.toString()); + } else { + searchIndex = searchForwards(searchTerm.toString()); + } } break; case SELF_INSERT: searchTerm.appendCodePoint(c); - searchIndex = searchBackwards(searchTerm.toString()); + if (state == State.SEARCH) { + searchIndex = searchBackwards(searchTerm.toString()); + } else { + searchIndex = searchForwards(searchTerm.toString()); + } break; default: @@ -2392,15 +2416,22 @@ public String readLine(String prompt, final Character mask) throws IOException { } // if we're still in search mode, print the search status - if (state == State.SEARCH) { + if (state == State.SEARCH || state == State.FORWARD_SEARCH) { if (searchTerm.length() == 0) { - printSearchStatus("", ""); + if (state == State.SEARCH) { + printSearchStatus("", ""); + } else { + printForwardSearchStatus("", ""); + } searchIndex = -1; } else { if (searchIndex == -1) { beep(); - } else { + printSearchStatus(searchTerm.toString(), ""); + } else if (state == State.SEARCH) { printSearchStatus(searchTerm.toString(), history.get(searchIndex).toString()); + } else { + printForwardSearchStatus(searchTerm.toString(), history.get(searchIndex).toString()); } } } @@ -2409,7 +2440,7 @@ public String readLine(String prompt, final Character mask) throws IOException { restoreLine(originalPrompt, cursorDest); } } - if (state != State.SEARCH) { + if (state != State.SEARCH && state != State.FORWARD_SEARCH) { /* * If this is still false at the end of the switch, then * we reset our repeatCount to 0. @@ -2460,12 +2491,12 @@ public String readLine(String prompt, final Character mask) throws IOException { // follows *immediately*, we assume it is a tab literal. boolean isTabLiteral = false; if (copyPasteDetection - && c == 9 - && (!pushBackChar.isEmpty() + && c == 9 + && (!pushBackChar.isEmpty() || (in.isNonBlockingEnabled() && in.peek(escapeTimeout) != -2))) { isTabLiteral = true; } - + if (! isTabLiteral) { success = complete(); } @@ -2506,7 +2537,9 @@ public String readLine(String prompt, final Character mask) throws IOException { return accept(); case ABORT: - abort(); + if (searchTerm == null) { + abort(); + } break; case INTERRUPT: @@ -2638,6 +2671,26 @@ public String readLine(String prompt, final Character mask) throws IOException { } break; + case FORWARD_SEARCH_HISTORY: + case HISTORY_SEARCH_FORWARD: + if (searchTerm != null) { + previousSearchTerm = searchTerm.toString(); + } + searchTerm = new StringBuffer(buf.buffer); + state = State.FORWARD_SEARCH; + if (searchTerm.length() > 0) { + searchIndex = searchForwards(searchTerm.toString()); + if (searchIndex == -1) { + beep(); + } + printForwardSearchStatus(searchTerm.toString(), + searchIndex > -1 ? history.get(searchIndex).toString() : ""); + } else { + searchIndex = -1; + printForwardSearchStatus("", ""); + } + break; + case CAPITALIZE_WORD: success = capitalizeWord(); break; @@ -2897,6 +2950,12 @@ else if (origState == State.VI_YANK_TO) { */ repeatCount = 0; } + + if (state != State.SEARCH && state != State.FORWARD_SEARCH) { + previousSearchTerm = ""; + searchTerm = null; + searchIndex = -1; + } } } if (!success) { @@ -3548,7 +3607,12 @@ public void resetPromptLine(String prompt, String buffer, int cursorDest) throws // backspace all text, including prompt buf.buffer.append(this.prompt); - buf.cursor += this.prompt.length(); + int promptLength = 0; + if (this.prompt != null) { + promptLength = this.prompt.length(); + } + + buf.cursor += promptLength; setPrompt(""); backspaceAll(); @@ -3564,10 +3628,17 @@ public void resetPromptLine(String prompt, String buffer, int cursorDest) throws } public void printSearchStatus(String searchTerm, String match) throws IOException { - String prompt = "(reverse-i-search)`" + searchTerm + "': "; - String buffer = match; + printSearchStatus(searchTerm, match, "(reverse-i-search)`"); + } + + public void printForwardSearchStatus(String searchTerm, String match) throws IOException { + printSearchStatus(searchTerm, match, "(i-search)`"); + } + + private void printSearchStatus(String searchTerm, String match, String searchLabel) throws IOException { + String prompt = searchLabel + searchTerm + "': "; int cursorDest = match.indexOf(searchTerm); - resetPromptLine(prompt, buffer, cursorDest); + resetPromptLine(prompt, match, cursorDest); } public void restoreLine(String originalPrompt, int cursorDest) throws IOException { @@ -3619,6 +3690,48 @@ public int searchBackwards(String searchTerm, int startIndex, boolean startsWith return -1; } + /** + * Search forward in history from a given position. + * + * @param searchTerm substring to search for. + * @param startIndex the index from which on to search + * @return index where this substring has been found, or -1 else. + */ + public int searchForwards(String searchTerm, int startIndex) { + return searchForwards(searchTerm, startIndex, false); + } + /** + * Search forwards in history from the current position. + * + * @param searchTerm substring to search for. + * @return index where the substring has been found, or -1 else. + */ + public int searchForwards(String searchTerm) { + return searchForwards(searchTerm, history.index()); + } + + public int searchForwards(String searchTerm, int startIndex, boolean startsWith) { + ListIterator it = history.entries(startIndex); + + if (searchIndex != -1 && it.hasNext()) { + it.next(); + } + + while (it.hasNext()) { + History.Entry e = it.next(); + if (startsWith) { + if (e.value().toString().startsWith(searchTerm)) { + return e.index(); + } + } else { + if (e.value().toString().contains(searchTerm)) { + return e.index(); + } + } + } + return -1; + } + // // Helpers // diff --git a/src/main/java/jline/console/KeyMap.java b/src/main/java/jline/console/KeyMap.java index aab2aeed..1daf87d7 100644 --- a/src/main/java/jline/console/KeyMap.java +++ b/src/main/java/jline/console/KeyMap.java @@ -298,6 +298,7 @@ public static KeyMap emacs() { public static final char CTRL_J = (char) 10; public static final char CTRL_M = (char) 13; public static final char CTRL_R = (char) 18; + public static final char CTRL_S = (char) 19; public static final char CTRL_U = (char) 21; public static final char CTRL_X = (char) 24; public static final char CTRL_Y = (char) 25; diff --git a/src/main/java/jline/console/completer/ArgumentParser.java b/src/main/java/jline/console/completer/ArgumentParser.java new file mode 100644 index 00000000..b27e6310 --- /dev/null +++ b/src/main/java/jline/console/completer/ArgumentParser.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2002-2012, the original author or authors. + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * http://www.opensource.org/licenses/bsd-license.php + */ +package jline.console.completer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * An utility class to parse arguments for the {@link CommandArgumentsCompleter} + * + * @author Baptiste Mesta + */ +public class ArgumentParser { + + private final String original; + + private List arguments; + + private String command; + + private String lastArgument; + + private int offset; + + /** + * + */ + public ArgumentParser(final String string) { + original = string; + final List list = Arrays.asList(string.split("(\\s)+")); + if (list.size() > 0) { + command = list.get(0); + if (list.size() > 1) { + arguments = list.subList(1, list.size()); + } else { + arguments = new ArrayList(); + } + } else { + arguments = new ArrayList(); + } + if (arguments.size() > 0) { + lastArgument = arguments.get(arguments.size() - 1); + offset = original.lastIndexOf(lastArgument); + } else { + offset = original.length(); + } + } + + public String getCommand() { + return command; + } + + public String getLastArgument() { + return lastArgument; + } + + public int getLastArgumentIndex() { + return arguments.size() - 1; + } + + public int getOffset() { + return offset; + } +} diff --git a/src/main/java/jline/console/completer/CommandArgumentsCompleter.java b/src/main/java/jline/console/completer/CommandArgumentsCompleter.java new file mode 100644 index 00000000..13c2adaf --- /dev/null +++ b/src/main/java/jline/console/completer/CommandArgumentsCompleter.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2002-2012, the original author or authors. + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * http://www.opensource.org/licenses/bsd-license.php + */ +package jline.console.completer; + +import static jline.internal.Preconditions.checkNotNull; + +import java.util.HashMap; +import java.util.List; + +import jline.shell.ShellContext; +import jline.shell.command.ShellCommand; + +/** + * Allow to complete a set of given commands + * Each command can have different completer + * At the start of the line the completer complete with command name + * Then it completes the line using command's completers + * + * @author Baptiste Mesta + */ +public class CommandArgumentsCompleter implements Completer { + + private final HashMap> commands; + + private final StringsCompleter commandCompleter; + + /** + * @param commands + */ + public CommandArgumentsCompleter(final HashMap> commands) { + this.commands = commands; + commandCompleter = new StringsCompleter(commands.keySet()); + } + + public int complete(final String buffer, final int cursor, final List candidates) { + checkNotNull(candidates); + final int pos = commandCompleter.complete(buffer, cursor, candidates); + if (pos != -1) { + return pos; + } + if (buffer != null) { + final ArgumentParser argumentParser = new ArgumentParser(buffer); + final String command = argumentParser.getCommand(); + if (command != null) { + final int lastArgumentIndex = Math.max(argumentParser.getLastArgumentIndex(), 0); + // complete with element from completer of the command + final ShellCommand clientCommand = commands.get(command); + if (clientCommand != null) { + final List completers = clientCommand.getCompleters(); + if (completers.size() > lastArgumentIndex) { + final Completer completer = completers.get(lastArgumentIndex); + final String lastArgument = argumentParser.getLastArgument(); + final int complete = completer.complete(lastArgument, lastArgument != null ? lastArgument.length() : 0, candidates); + return complete + argumentParser.getOffset(); + } + } + } + } + + if (candidates.size() == 1) { + candidates.set(0, candidates.get(0) + " "); + } + return candidates.isEmpty() ? -1 : cursor; + } + +} diff --git a/src/main/java/jline/console/completer/ResolvingStringsCompleter.java b/src/main/java/jline/console/completer/ResolvingStringsCompleter.java new file mode 100644 index 00000000..63ab183c --- /dev/null +++ b/src/main/java/jline/console/completer/ResolvingStringsCompleter.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2002-2012, the original author or authors. + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * http://www.opensource.org/licenses/bsd-license.php + */ +package jline.console.completer; + +import java.util.List; + +/** + * @author Baptiste Mesta + */ +public abstract class ResolvingStringsCompleter extends StringsCompleter { + + @Override + public int complete(final String buffer, final int cursor, final List candidates) { + getStrings().clear(); + final List resolveStrings = resolveStrings(); + if (resolveStrings != null) { + getStrings().addAll(resolveStrings); + } + return super.complete(buffer, cursor, candidates); + } + + /** + * @return + */ + public abstract List resolveStrings(); +} diff --git a/src/main/java/jline/console/history/MemoryHistory.java b/src/main/java/jline/console/history/MemoryHistory.java index 78a19ab1..c9b23dcf 100644 --- a/src/main/java/jline/console/history/MemoryHistory.java +++ b/src/main/java/jline/console/history/MemoryHistory.java @@ -337,5 +337,12 @@ public boolean next() { return true; } - + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (Entry e : this) { + sb.append(e.toString() + "\n"); + } + return sb.toString(); + } } diff --git a/src/main/java/jline/shell/BaseShell.java b/src/main/java/jline/shell/BaseShell.java new file mode 100644 index 00000000..3e592ebd --- /dev/null +++ b/src/main/java/jline/shell/BaseShell.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2002-2012, the original author or authors. + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * http://www.opensource.org/licenses/bsd-license.php + */ +package jline.shell; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import jline.console.ConsoleReader; +import jline.console.completer.CommandArgumentsCompleter; +import jline.shell.command.HelpCommand; +import jline.shell.command.ShellCommand; + +/** + * A basic shell + * Implement abstract methods + * to run it just execute (e.g. in a main) + * shell.run(); + * + * @author Baptiste Mesta + */ +public abstract class BaseShell { + + private HashMap> commands; + + private HelpCommand helpCommand; + + protected void init() throws Exception { + final List> commandList = initShellCommands(); + commands = new HashMap>(); + for (final ShellCommand shellCommand : commandList) { + commands.put(shellCommand.getName(), shellCommand); + } + helpCommand = getHelpCommand(); + if (helpCommand != null) { + commands.put(helpCommand.getName(), helpCommand); + } + + } + + /** + * return the help command used + * Can be overridden + */ + protected HelpCommand getHelpCommand() { + return new HelpCommand(commands); + } + + /** + * @return + * list of commands contributed to the shell + * @throws Exception + */ + protected abstract List> initShellCommands() throws Exception; + + /** + * called by {@link BaseShell} when the shell is exited + * + * @throws Exception + */ + protected void destroy() throws Exception { + } + + public void run() throws Exception { + init(); + printWelcomeMessage(); + final ConsoleReader reader = new ConsoleReader(); + reader.setBellEnabled(false); + final CommandArgumentsCompleter commandArgumentsCompleter = new CommandArgumentsCompleter(commands); + + reader.addCompleter(commandArgumentsCompleter); + + String line; + while ((line = reader.readLine("\n" + getPrompt())) != null) { + final List args = parse(line); + final String command = args.remove(0); + if (commands.containsKey(command)) { + final ShellCommand clientCommand = commands.get(command); + if (clientCommand.validate(args)) { + try { + clientCommand.execute(args, getContext()); + } catch (final Exception e) { + e.printStackTrace(); + } + } else { + clientCommand.printHelp(); + } + } else if ("exit".equals(line)) { + System.out.println("Exiting application"); + destroy(); + return; + } else { + System.out.println("Wrong argument"); + helpCommand.printHelp(); + } + } + destroy(); + } + + /** + * @return + */ + protected abstract T getContext(); + + /** + * Override this to print a welcom message + */ + protected abstract void printWelcomeMessage(); + + /** + * allow to specify the prompt used + */ + protected abstract String getPrompt(); + + /** + * used to parse arguments of the line + * + * @param line + * @return + */ + protected List parse(final String line) { + return new ArrayList(Arrays.asList(line.trim().split("(\\s)+"))); + } + +} diff --git a/src/main/java/jline/shell/ShellContext.java b/src/main/java/jline/shell/ShellContext.java new file mode 100644 index 00000000..dbb80e71 --- /dev/null +++ b/src/main/java/jline/shell/ShellContext.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2002-2012, the original author or authors. + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * http://www.opensource.org/licenses/bsd-license.php + */ +package jline.shell; + +/** + * @author Baptiste Mesta + */ +public interface ShellContext { + +} diff --git a/src/main/java/jline/shell/command/HelpCommand.java b/src/main/java/jline/shell/command/HelpCommand.java new file mode 100644 index 00000000..860a09ae --- /dev/null +++ b/src/main/java/jline/shell/command/HelpCommand.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2002-2012, the original author or authors. + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * http://www.opensource.org/licenses/bsd-license.php + */ +package jline.shell.command; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +import jline.console.completer.Completer; +import jline.console.completer.StringsCompleter; +import jline.shell.ShellContext; + +/** + * Default implementation of the help command + * + * @author Baptiste Mesta + */ +public class HelpCommand extends ShellCommand { + + private final HashMap> commands; + + /** + * @param commands + */ + public HelpCommand(final HashMap> commands) { + this.commands = commands; + } + + @Override + public boolean execute(final List args, final T context) throws Exception { + commands.get(args.get(0)).printHelp(); + return true; + } + + @Override + public void printHelp() { + printUsage(); + } + + @Override + public boolean validate(final List args) { + return args.size() == 1 && commands.containsKey(args.get(0)); + } + + @Override + public List getCompleters() { + return Arrays.asList((Completer) new StringsCompleter(commands.keySet())); + } + + private void printUsage() { + System.out.println("-----------------------------------------------"); + System.out.println("Usage: "); + System.out.println("Command can be:"); + final Set keySet = commands.keySet(); + final ArrayList list = new ArrayList(keySet); + Collections.sort(list); + for (final String entry : list) { + System.out.println(entry); + } + System.out.println(""); + System.out.println("Use 'help ' for help about a command"); + System.out.println("-----------------------------------------------"); + } + + @Override + public String getName() { + return "help"; + } +} diff --git a/src/main/java/jline/shell/command/ShellCommand.java b/src/main/java/jline/shell/command/ShellCommand.java new file mode 100644 index 00000000..9413cf34 --- /dev/null +++ b/src/main/java/jline/shell/command/ShellCommand.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2002-2012, the original author or authors. + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * http://www.opensource.org/licenses/bsd-license.php + */ +package jline.shell.command; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import jline.console.completer.Completer; +import jline.shell.ShellContext; + +/** + * A command that can be contributed to a shell + * + * @author Baptiste Mesta + */ +public abstract class ShellCommand { + + public abstract String getName(); + + /** + * @param args + * arguments to execute the command + * @param context + * a context given by the shell + * @return + * true if the command was successfully executed + * @throws Exception + */ + public abstract boolean execute(List args, T context) throws Exception; + + public List getCompleters() { + return Collections.emptyList(); + } + + /** + * Implement this to show usage help on this command + */ + public abstract void printHelp(); + + /** + * Check if given args allow the command to be executed + * + * @param args + * @return + * true if the command can be executed + */ + public abstract boolean validate(List args); + + /** + * utiliy method to get argument after an other argument: + * e.g. if '-u user' is given to the command this return the 'user' + * + * @param args + * @param key + * @return + */ + protected String getParam(final List args, final String key) { + for (final Iterator iterator = args.iterator(); iterator.hasNext();) { + final String param = iterator.next(); + if (key.equals(param)) { + return iterator.next(); + } + } + return null; + } +} diff --git a/src/test/java/jline/console/ConsoleReaderTest.java b/src/test/java/jline/console/ConsoleReaderTest.java index a035f8d0..e23ecc57 100644 --- a/src/test/java/jline/console/ConsoleReaderTest.java +++ b/src/test/java/jline/console/ConsoleReaderTest.java @@ -59,15 +59,18 @@ private void assertWindowsKeyBehavior(String expected, char[] input) throws Exce assertEquals(expected, line); } + private ConsoleReader createConsole() throws Exception { + return createConsole(""); + } + private ConsoleReader createConsole(String chars) throws Exception { - System.err.println(Configuration.getEncoding()); - System.err.println(chars); return createConsole(chars.getBytes(Configuration.getEncoding())); } private ConsoleReader createConsole(byte[] bytes) throws Exception { return createConsole(null, bytes); } + private ConsoleReader createConsole(String appName, byte[] bytes) throws Exception { InputStream in = new ByteArrayInputStream(bytes); output = new ByteArrayOutputStream(); @@ -239,7 +242,7 @@ public void testInsertOnWindowsTerminal() throws Exception { @Test public void testExpansion() throws Exception { - ConsoleReader reader = new ConsoleReader(); + ConsoleReader reader = createConsole(); MemoryHistory history = new MemoryHistory(); history.setMaxSize(3); history.add("foo"); @@ -263,6 +266,7 @@ public void testExpansion() throws Exception { assertEquals("mkdir monkey", reader.expandEvents("!mk")); try { reader.expandEvents("!mz"); + fail("expected IllegalArgumentException"); } catch (IllegalArgumentException e) { assertEquals("!mz: event not found", e.getMessage()); } @@ -272,20 +276,94 @@ public void testExpansion() throws Exception { assertEquals("mkdir monkey", reader.expandEvents("!-1")); assertEquals("cd c:\\", reader.expandEvents("!-2")); - assertEquals("cd c:\\", reader.expandEvents("!2")); - assertEquals("mkdir monkey", reader.expandEvents("!3")); + assertEquals("cd c:\\", reader.expandEvents("!3")); + assertEquals("mkdir monkey", reader.expandEvents("!4")); try { reader.expandEvents("!20"); + fail("expected IllegalArgumentException"); } catch (IllegalArgumentException e) { assertEquals("!20: event not found", e.getMessage()); } try { reader.expandEvents("!-20"); + fail("expected IllegalArgumentException"); } catch (IllegalArgumentException e) { assertEquals("!-20: event not found", e.getMessage()); } } + @Test + public void testNumericExpansions() throws Exception { + ConsoleReader reader = createConsole(); + MemoryHistory history = new MemoryHistory(); + history.setMaxSize(3); + + // Seed history with three entries: + // 1 history1 + // 2 history2 + // 3 history3 + history.add("history1"); + history.add("history2"); + history.add("history3"); + reader.setHistory(history); + + // Validate !n + assertExpansionIllegalArgumentException(reader, "!0"); + assertEquals("history1", reader.expandEvents("!1")); + assertEquals("history2", reader.expandEvents("!2")); + assertEquals("history3", reader.expandEvents("!3")); + assertExpansionIllegalArgumentException(reader, "!4"); + + // Validate !-n + assertExpansionIllegalArgumentException(reader, "!-0"); + assertEquals("history3", reader.expandEvents("!-1")); + assertEquals("history2", reader.expandEvents("!-2")); + assertEquals("history1", reader.expandEvents("!-3")); + assertExpansionIllegalArgumentException(reader, "!-4"); + + // Validate !! + assertEquals("history3", reader.expandEvents("!!")); + + // Add two new entries. Because maxSize=3, history is: + // 3 history3 + // 4 history4 + // 5 history5 + history.add("history4"); + history.add("history5"); + + // Validate !n + assertExpansionIllegalArgumentException(reader, "!0"); + assertExpansionIllegalArgumentException(reader, "!1"); + assertExpansionIllegalArgumentException(reader, "!2"); + assertEquals("history3", reader.expandEvents("!3")); + assertEquals("history4", reader.expandEvents("!4")); + assertEquals("history5", reader.expandEvents("!5")); + assertExpansionIllegalArgumentException(reader, "!6"); + + // Validate !-n + assertExpansionIllegalArgumentException(reader, "!-0"); + assertEquals("history5", reader.expandEvents("!-1")); + assertEquals("history4", reader.expandEvents("!-2")); + assertEquals("history3", reader.expandEvents("!-3")); + assertExpansionIllegalArgumentException(reader, "!-4"); + + // Validate !! + assertEquals("history5", reader.expandEvents("!!")); + } + + /** + * Validates that an 'event not found' IllegalArgumentException is thrown + * for the expansion event. + */ + protected void assertExpansionIllegalArgumentException(ConsoleReader reader, String event) throws Exception { + try { + reader.expandEvents(event); + fail("Expected IllegalArgumentException for " + event); + } catch (IllegalArgumentException e) { + assertEquals(event + ": event not found", e.getMessage()); + } + } + @Test public void testStoringHistory() throws Exception { ConsoleReader reader = createConsole("foo ! bar\r\n"); @@ -298,6 +376,106 @@ public void testStoringHistory() throws Exception { history.previous(); assertEquals("foo \\! bar", history.current()); + + reader = createConsole("cd c:\\docs\r\n"); + history = new MemoryHistory(); + reader.setHistory(history); + reader.setExpandEvents(true); + + line = reader.readLine(); + assertEquals("cd c:\\docs", line); + + history.previous(); + assertEquals("cd c:\\docs", history.current()); + } + + @Test + public void testExpansionAndHistoryWithEscapes() throws Exception { + + /* + * Tests the results of the ConsoleReader.readLine() call and the line + * stored in history. For each input, it tests the with-expansion and + * without-expansion case. + */ + + ConsoleReader reader = null; + + // \! (escaped expansion v1) + reader = createConsole("echo ab\\!ef", true, "cd"); + assertReadLine("echo ab!ef", reader); + assertHistory("echo ab\\!ef", reader); + + reader = createConsole("echo ab\\!ef", false, "cd"); + assertReadLine("echo ab\\!ef", reader); + assertHistory("echo ab\\!ef", reader); + + // \!\! (escaped expansion v2) + reader = createConsole("echo ab\\!\\!ef", true, "cd"); + assertReadLine("echo ab!!ef", reader); + assertHistory("echo ab\\!\\!ef", reader); + + reader = createConsole("echo ab\\!\\!ef", false, "cd"); + assertReadLine("echo ab\\!\\!ef", reader); + assertHistory("echo ab\\!\\!ef", reader); + + // !! (expansion) + reader = createConsole("echo ab!!ef", true, "cd"); + assertReadLine("echo abcdef", reader); + assertHistory("echo abcdef", reader); + + reader = createConsole("echo ab!!ef", false, "cd"); + assertReadLine("echo ab!!ef", reader); + assertHistory("echo ab!!ef", reader); + + // \G (backslash no expansion) + reader = createConsole("echo abc\\Gdef", true, "cd"); + assertReadLine("echo abc\\Gdef", reader); + assertHistory("echo abc\\Gdef", reader); + + reader = createConsole("echo abc\\Gdef", false, "cd"); + assertReadLine("echo abc\\Gdef", reader); + assertHistory("echo abc\\Gdef", reader); + + // \^ (escaped expansion) + reader = createConsole("\\^abc^def", true, "echo abc"); + assertReadLine("^abc^def", reader); + assertHistory("\\^abc^def", reader); + + reader = createConsole("\\^abc^def", false, "echo abc"); + assertReadLine("\\^abc^def", reader); + assertHistory("\\^abc^def", reader); + + // ^^ (expansion) + reader = createConsole("^abc^def", true, "echo abc"); + assertReadLine("echo def", reader); + assertHistory("echo def", reader); + + reader = createConsole("^abc^def", false, "echo abc"); + assertReadLine("^abc^def", reader); + assertHistory("^abc^def", reader); + } + + private ConsoleReader createConsole(String input, boolean expandEvents, String... historyItems) throws Exception { + ConsoleReader consoleReader = createConsole(input + "\r\n"); + MemoryHistory history = new MemoryHistory(); + if (historyItems != null) { + for (String historyItem : historyItems) { + history.add(historyItem); + } + } + consoleReader.setHistory(history); + consoleReader.setExpandEvents(expandEvents); + return consoleReader; + } + + private void assertReadLine(String expected, ConsoleReader consoleReader) throws Exception { + assertEquals(expected, consoleReader.readLine()); + } + + private void assertHistory(String expected, ConsoleReader consoleReader) { + History history = consoleReader.getHistory(); + history.previous(); + assertEquals(expected, history.current()); } @Test diff --git a/src/test/java/jline/console/HistorySearchTest.java b/src/test/java/jline/console/HistorySearchTest.java new file mode 100644 index 00000000..c2951f49 --- /dev/null +++ b/src/test/java/jline/console/HistorySearchTest.java @@ -0,0 +1,143 @@ +package jline.console; + +import jline.console.history.MemoryHistory; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class HistorySearchTest { + private ConsoleReader reader; + private ByteArrayOutputStream output; + + @Before + public void setUp() throws Exception { + InputStream in = new ByteArrayInputStream(new byte[]{}); + output = new ByteArrayOutputStream(); + reader = new ConsoleReader("test console reader", in, output, null); + } + + private MemoryHistory setupHistory() { + MemoryHistory history = new MemoryHistory(); + history.setMaxSize(10); + history.add("foo"); + history.add("fiddle"); + history.add("faddle"); + reader.setHistory(history); + return history; + } + + @Test + public void testReverseHistorySearch() throws Exception { + MemoryHistory history = setupHistory(); + + String readLineResult; + reader.setInput(new ByteArrayInputStream(new byte[]{KeyMap.CTRL_R, 'f', '\n'})); + readLineResult = reader.readLine(); + assertEquals("faddle", readLineResult); + assertEquals(3, history.size()); + + reader.setInput(new ByteArrayInputStream(new byte[]{ + KeyMap.CTRL_R, 'f', KeyMap.CTRL_R, KeyMap.CTRL_R, KeyMap.CTRL_R, KeyMap.CTRL_R, KeyMap.CTRL_R, '\n' + })); + readLineResult = reader.readLine(); + assertEquals("foo", readLineResult); + assertEquals(4, history.size()); + + reader.setInput(new ByteArrayInputStream(new byte[]{KeyMap.CTRL_R, 'f', KeyMap.CTRL_R, KeyMap.CTRL_R, '\n'})); + readLineResult = reader.readLine(); + assertEquals("fiddle", readLineResult); + assertEquals(5, history.size()); + } + + @Test + public void testForwardHistorySearch() throws Exception { + MemoryHistory history = setupHistory(); + + String readLineResult; + reader.setInput(new ByteArrayInputStream(new byte[]{ + KeyMap.CTRL_R, 'f', KeyMap.CTRL_R, KeyMap.CTRL_R, KeyMap.CTRL_S, '\n' + })); + readLineResult = reader.readLine(); + assertEquals("fiddle", readLineResult); + assertEquals(4, history.size()); + + reader.setInput(new ByteArrayInputStream(new byte[]{ + KeyMap.CTRL_R, 'f', KeyMap.CTRL_R, KeyMap.CTRL_R, KeyMap.CTRL_R, KeyMap.CTRL_S, KeyMap.CTRL_S, '\n' + })); + readLineResult = reader.readLine(); + assertEquals("faddle", readLineResult); + assertEquals(5, history.size()); + + reader.setInput(new ByteArrayInputStream(new byte[]{ + KeyMap.CTRL_R, 'f', KeyMap.CTRL_R, KeyMap.CTRL_R, KeyMap.CTRL_R, KeyMap.CTRL_R, KeyMap.CTRL_S, '\n' + })); + readLineResult = reader.readLine(); + assertEquals("fiddle", readLineResult); + assertEquals(6, history.size()); + } + + @Test + public void testSearchHistoryAfterHittingEnd() throws Exception { + MemoryHistory history = setupHistory(); + + String readLineResult; + reader.setInput(new ByteArrayInputStream(new byte[]{ + KeyMap.CTRL_R, 'f', KeyMap.CTRL_R, KeyMap.CTRL_R, KeyMap.CTRL_R, KeyMap.CTRL_S, '\n' + })); + readLineResult = reader.readLine(); + assertEquals("fiddle", readLineResult); + assertEquals(4, history.size()); + } + + @Test + public void testSearchHistoryWithNoMatches() throws Exception { + MemoryHistory history = setupHistory(); + + String readLineResult; + reader.setInput(new ByteArrayInputStream(new byte[]{ + 'x', KeyMap.CTRL_S, KeyMap.CTRL_S, '\n' + })); + readLineResult = reader.readLine(); + assertEquals("", readLineResult); + assertEquals(3, history.size()); + } + + @Test + public void testAbortingSearchRetainsCurrentBufferAndPrintsDetails() throws Exception { + MemoryHistory history = setupHistory(); + + String readLineResult; + reader.setInput(new ByteArrayInputStream(new byte[]{ + 'f', KeyMap.CTRL_R, 'f', KeyMap.CTRL_G + })); + readLineResult = reader.readLine(); + assertEquals(null, readLineResult); + assertTrue(output.toString().contains("(reverse-i-search)`ff':")); + assertEquals("ff", reader.getCursorBuffer().toString()); + assertEquals(3, history.size()); + } + + @Test + public void testAbortingAfterSearchingPreviousLinesGivesBlank() throws Exception { + MemoryHistory history = setupHistory(); + + String readLineResult; + reader.setInput(new ByteArrayInputStream(new byte[]{ + 'f', KeyMap.CTRL_R, 'f', '\n', + 'f', 'o', 'o', KeyMap.CTRL_G + })); + readLineResult = reader.readLine(); + assertEquals("", readLineResult); + + readLineResult = reader.readLine(); + assertEquals(null, readLineResult); + assertEquals("", reader.getCursorBuffer().toString()); + assertEquals(3, history.size()); + } +}