FELIX-3341 Implement csh-like command line history
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1346788 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Console.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Console.java
index 953db7d..47c6d4c 100644
--- a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Console.java
+++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Console.java
@@ -29,13 +29,15 @@
private final CommandSession session;
private final InputStream in;
private final PrintStream out;
+ private final History history;
private boolean quit;
- public Console(CommandSession session)
+ public Console(CommandSession session, History history)
{
this.session = session;
in = session.getKeyboard();
out = session.getConsole();
+ this.history = history;
}
public void run()
@@ -59,6 +61,12 @@
try
{
+ if (line.charAt(0) == '!' || line.charAt(0) == '^')
+ {
+ line = history.evaluate(line);
+ System.out.println(line);
+ }
+
Object result = session.execute(line);
session.put("_", result); // set $_ to last result
@@ -80,7 +88,7 @@
out.println("gosh: " + e);
quit = true;
}
-
+
if (!quit)
{
session.put("exception", e);
@@ -95,6 +103,10 @@
+ e.getMessage());
}
}
+ finally
+ {
+ this.history.append(line);
+ }
}
}
catch (Exception e)
diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/History.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/History.java
new file mode 100644
index 0000000..c298f25
--- /dev/null
+++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/History.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.felix.gogo.shell;
+
+import java.text.CharacterIterator;
+import java.text.StringCharacterIterator;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.ListIterator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class History {
+
+ private static final int SIZE_DEFAULT = 100;
+
+ private LinkedList<String> commands;
+
+ private int limit;
+
+ public History() {
+ this.limit = SIZE_DEFAULT;
+ this.commands = new LinkedList<String>();
+ }
+
+ CharSequence evaluate(final CharSequence commandLine) {
+
+ /*
+ * <pre>
+ * hist = ( '!' spec ) | ( '^' subst ) .
+ * spec = '!' ( '!' | idx | '?' find | string ) [ ':' [ 'a' | 'g' ] 's' regex ] . idx = [ '-' ] { 0..9 } .
+ * find = string ( '?' | EOL ) .
+ * subst = pat '^' repl '^' EOL .
+ * regex = str pat str repl str EOL .
+ * </pre>
+ */
+
+ final CharacterIterator ci = new StringCharacterIterator(commandLine.toString());
+
+ String event;
+ char c = ci.current();
+ if (c == '!') {
+ c = ci.next();
+ if (c == '!') {
+ event = this.commands.getLast();
+ ci.next();
+ } else if ((c >= '0' && c <= '9') || c == '-') {
+ event = getCommand(ci);
+ } else if (c == '?') {
+ event = findContains(ci);
+ ci.next();
+ } else {
+ ci.previous();
+ event = findStartsWith(ci);
+ }
+ } else if (c == '^') {
+ event = subst(ci, c, false, this.commands.getLast());
+ } else {
+ throw new IllegalArgumentException(commandLine + ": Unsupported event");
+ }
+
+ if (ci.current() == ':') {
+ c = ci.next();
+ boolean global = (c == 'a' || c == 'g');
+ if (global) {
+ c = ci.next();
+ }
+ if (c == 's') {
+ event = subst(ci, ci.next(), global, event);
+ }
+ }
+
+ return event;
+ }
+
+ /**
+ * Returns the command history, oldest command first
+ */
+ Iterator<String> getHistory() {
+ return this.commands.iterator();
+ }
+
+ void append(final CharSequence commandLine) {
+ commands.add(commandLine.toString());
+ if (commands.size() > this.limit) {
+ commands.removeFirst();
+ }
+ }
+
+ private String getCommand(final CharacterIterator ci) {
+ final StringBuilder s = new StringBuilder();
+ char c = ci.current();
+ do {
+ s.append(c);
+ c = ci.next();
+ } while (c >= '0' && c <= '9');
+ final int n = Integer.parseInt(s.toString());
+ final int pos = ((n < 0) ? this.commands.size() : -1) + n;
+ if (pos >= 0 && pos < this.commands.size()) {
+ return this.commands.get(pos);
+ }
+ throw new IllegalArgumentException("!" + n + ": event not found");
+ }
+
+ private String findContains(final CharacterIterator ci) {
+ CharSequence part = findDelimiter(ci, '?');
+ final ListIterator<String> iter = this.commands.listIterator(this.commands.size());
+ while (iter.hasPrevious()) {
+ String value = iter.previous();
+ if (value.contains(part)) {
+ return value;
+ }
+ }
+
+ throw new IllegalArgumentException("No command containing '" + part + "' in the history");
+ }
+
+ private String findStartsWith(final CharacterIterator ci) {
+ String part = findDelimiter(ci, ':').toString();
+ final ListIterator<String> iter2 = this.commands.listIterator(this.commands.size());
+ while (iter2.hasPrevious()) {
+ String value = iter2.previous();
+ if (value.startsWith(part)) {
+ return value;
+ }
+ }
+
+ throw new IllegalArgumentException("No command containing '" + part + "' in the history");
+ }
+
+ private String subst(final CharacterIterator ci, final char delimiter, final boolean replaceAll, final String event) {
+ final String pattern = findDelimiter(ci, delimiter).toString();
+ final String repl = findDelimiter(ci, delimiter).toString();
+ if (pattern.length() == 0) {
+ throw new IllegalArgumentException(":s" + event + ": substitution failed");
+ }
+ final Pattern regex = Pattern.compile(pattern);
+ final Matcher m = regex.matcher(event);
+ final StringBuffer res = new StringBuffer();
+
+ if (!m.find()) {
+ throw new IllegalArgumentException(":s" + event + ": substitution failed");
+ }
+ do {
+ m.appendReplacement(res, repl);
+ } while (replaceAll && m.find());
+ m.appendTail(res);
+ return res.toString();
+ }
+
+ private CharSequence findDelimiter(final CharacterIterator ci, char delimiter) {
+ final StringBuilder b = new StringBuilder();
+ for (char c = ci.next(); c != CharacterIterator.DONE && c != delimiter; c = ci.next()) {
+ if (c == '\\') {
+ c = ci.next();
+ }
+ b.append(c);
+ }
+ return b;
+ }
+}
diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Shell.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Shell.java
index 073e8b2..fef5daf 100644
--- a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Shell.java
+++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Shell.java
@@ -25,6 +25,8 @@
import java.net.URI;
import java.net.URLConnection;
import java.nio.CharBuffer;
+import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.Set;
@@ -37,13 +39,14 @@
public class Shell
{
- static final String[] functions = { "gosh", "sh", "source" };
+ static final String[] functions = { "gosh", "sh", "source", "history" };
private final static URI CWD = new File(".").toURI();
private final URI baseURI;
private final BundleContext context;
private final CommandProcessor processor;
+ private final History history;
public Shell(BundleContext context, CommandProcessor processor)
{
@@ -52,6 +55,7 @@
String baseDir = context.getProperty("gosh.home");
baseDir = (baseDir == null) ? context.getProperty("user.dir") : baseDir;
baseURI = new File(baseDir).toURI();
+ this.history = new History();
}
public Object gosh(final CommandSession session, String[] argv) throws Exception
@@ -199,7 +203,7 @@
private Object console(CommandSession session)
{
- Console console = new Console(session);
+ Console console = new Console(session, history);
console.run();
return null;
}
@@ -248,4 +252,13 @@
return CWD;
}
}
+
+ public String[] history() {
+ Iterator<String> history = this.history.getHistory();
+ List<String> lines = new ArrayList<String>();
+ for (int i = 1; history.hasNext(); i++) {
+ lines.add(String.format("%5d %s", i, history.next()));
+ }
+ return lines.toArray(new String[lines.size()]);
+ }
}