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