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());
+ }
+}