refactor Parser - create explicit Tokenizer to enable fix for:

FELIX-1487: commands on multiple lines - NEWLINE can now be used instead of semiclon as a statement separator.

The Parser now tracks the exact location (line and column) when an error occurs.

also fixes:

FELIX-1473: allow method calls on Strings
FELIX-1493: add $argv for access to List rather than expanded $args
FELIX-1474: result of commands is implicitly written to pipe



git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@941549 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Closure.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Closure.java
index 6268f8a..6110be1 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Closure.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Closure.java
@@ -18,42 +18,143 @@
  */
 package org.apache.felix.gogo.runtime.shell;
 
+import java.io.EOFException;
+import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 
+import org.apache.felix.gogo.runtime.shell.Tokenizer.Type;
 import org.osgi.service.command.CommandSession;
 import org.osgi.service.command.Function;
 
-public class Closure extends Reflective implements Function
+public class Closure extends Reflective implements Function, Evaluate
 {
-    private static final long serialVersionUID = 1L;
-    final CharSequence source;
-    final Closure parent;
-    CommandSessionImpl session;
-    List<Object> parms;
+    public static final String LOCATION = ".location";
 
-    Closure(CommandSessionImpl session, Closure parent, CharSequence source)
+    private static final long serialVersionUID = 1L;
+    private static final ThreadLocal<String> location = new ThreadLocal<String>();
+
+    private final CommandSessionImpl session;
+    private final Closure parent;
+    private final CharSequence source;
+    private final List<List<List<Token>>> program;
+    private final Object script;
+
+    private Token errTok;
+    private List<Object> parms = null;
+    private List<Object> parmv = null;
+
+    public Closure(CommandSessionImpl session, Closure parent, CharSequence source) throws Exception
     {
         this.session = session;
         this.parent = parent;
         this.source = source;
+        script = session.get("0"); // by convention, $0 is script name
+
+        try
+        {
+            program = new Parser(source).program();
+        }
+        catch (Exception e)
+        {
+            throw setLocation(e);
+        }
+    }
+    
+    public CommandSessionImpl session()
+    {
+        return session;
     }
 
+    private Exception setLocation(Exception e)
+    {
+        String loc = location.get();
+        if (null == loc)
+        {
+            loc = (null == script ? "" : script + ":");
+
+            if (e instanceof SyntaxError)
+            {
+                SyntaxError se = (SyntaxError) e;
+                loc += se.line() + "." + se.column();
+
+                if (e instanceof EOFError)
+                { // map to public exception, so interactive clients can provide more input
+                    EOFException eofe = new EOFException(e.getMessage());
+                    eofe.initCause(e);
+                    e = eofe;
+                }
+            }
+            else if (null != errTok)
+            {
+                loc += errTok.line + "." + errTok.column;
+            }
+
+            location.set(loc);
+        }
+        else if (null != script && !loc.contains(":"))
+        {
+            location.set(script + ":" + loc);
+        }
+
+        session.put(LOCATION, location.get());
+
+        return e;
+    }
+
+    // implements Function interface
+    // XXX: session arg x not used?
     public Object execute(CommandSession x, List<Object> values) throws Exception
     {
-        parms = values;
-        Parser parser = new Parser(source);
-        List<List<List<CharSequence>>> program = parser.program();
-        Pipe last = null;
+        try
+        {
+            location.remove();
+            session.variables.remove(LOCATION);
+            return execute(values);
+        }
+        catch (Exception e)
+        {
+            throw setLocation(e);
+        }
+    }
 
-        for (List<List<CharSequence>> pipeline : program)
+    @SuppressWarnings("unchecked")
+    private Object execute(List<Object> values) throws Exception
+    {
+        if (null != values)
+        {
+            parmv = values;
+            parms = new ArgList(parmv);
+        }
+        else if (null != parent)
+        {
+            // inherit parent closure parameters
+            parms = parent.parms;
+            parmv = parent.parmv;
+        }
+        else
+        {
+            // inherit session parameters
+            Object args = session.get("args");
+            if (null != args && args instanceof List<?>)
+            {
+                parmv = (List<Object>) args;
+                parms = new ArgList(parmv);
+            }
+        }
+
+        Pipe last = null;
+        Object[] mark = Pipe.mark();
+
+        for (List<List<Token>> pipeline : program)
         {
             ArrayList<Pipe> pipes = new ArrayList<Pipe>();
 
-            for (List<CharSequence> statement : pipeline)
+            for (List<Token> statement : pipeline)
             {
                 Pipe current = new Pipe(this, statement);
 
@@ -97,19 +198,22 @@
                 if (pipe.exception != null)
                 {
                     // can't throw exception, as result is defined by last pipe
-                    session.err.println("pipe: " + pipe.exception);
+                    Object oloc = session.get(LOCATION);
+                    String loc = (String.valueOf(oloc).contains(":") ? oloc + ": "
+                        : "pipe: ");
+                    session.err.println(loc + pipe.exception);
                     session.put("pipe-exception", pipe.exception);
                 }
             }
 
             if (last.exception != null)
             {
-                Pipe.reset();
+                Pipe.reset(mark);
                 throw last.exception;
             }
         }
 
-        Pipe.reset(); // reset IO in case sshd uses same thread for new client
+        Pipe.reset(mark); // reset IO in case same thread used for new client
 
         if (last == null)
         {
@@ -120,110 +224,180 @@
         {
             return Arrays.asList((Object[]) last.result);
         }
+
         return last.result;
     }
 
-    Object executeStatement(List<CharSequence> statement) throws Exception
+    public Object eval(final Token t) throws Exception
+    {
+        Object v = null;
+
+        switch (t.type)
+        {
+            case WORD:
+                v = Tokenizer.expand(t, this);
+                if (t == v)
+                {
+                    String s = t.toString();
+                    if ("null".equals(s))
+                        v = null;
+                    else if ("false".equals(s))
+                        v = false;
+                    else if ("true".equals(s))
+                        v = true;
+                    else
+                        v = s;
+                }
+                break;
+
+            case CLOSURE:
+                v = new Closure(session, this, t);
+                break;
+
+            case EXECUTION:
+                v = new Closure(session, this, t).execute(session, parms);
+                break;
+
+            case ARRAY:
+                v = array(t);
+                break;
+
+            case ASSIGN:
+                v = t.type;
+                break;
+
+            default:
+                throw new SyntaxError(t.line, t.column, "unexpected token: " + t.type);
+        }
+
+        return v;
+    }
+
+    public Object executeStatement(List<Token> statement) throws Exception
     {
         // add set -x facility if echo is set
         if (Boolean.TRUE.equals(session.get("echo")))
         {
             StringBuilder buf = new StringBuilder("+");
-            for (CharSequence token : statement)
+            for (Token token : statement)
             {
                 buf.append(' ');
-                buf.append(token);
+                buf.append(token.source());
             }
             session.err.println(buf);
         }
 
-        if (statement.size() == 1 && statement.get(0).charAt(0) == '(')
-        {
-            return eval(statement.get(0));
-        }
-
-        Object result;
         List<Object> values = new ArrayList<Object>();
-        for (CharSequence token : statement)
+        errTok = statement.get(0);
+
+        for (Token t : statement)
         {
-            Object v = eval(token);
-            if (v != null && v == parms)
+            Object v = eval(t);
+
+            if ((Type.EXECUTION == t.type) && (statement.size() == 1))
             {
-                for (Object p : parms)
-                {
-                    values.add(p);
-                }
+                return v;
+            }
+
+            if (parms == v && parms != null)
+            {
+                values.addAll(parms); // explode $args array
             }
             else
             {
                 values.add(v);
             }
         }
-        result = execute(values.remove(0), values);
-        return result;
-    }
 
-    private Object execute(Object cmd, List<Object> values) throws Exception
-    {
+        Object cmd = values.remove(0);
         if (cmd == null)
         {
             if (values.isEmpty())
             {
                 return null;
             }
-            else
-            {
-                throw new IllegalArgumentException("Command name evaluates to null");
-            }
+
+            throw new RuntimeException("Command name evaluates to null: " + errTok);
         }
 
+        return execute(cmd, values);
+    }
+
+    private Object execute(Object cmd, List<Object> values) throws Exception
+    {
         // Now there are the following cases
         // <string> '=' statement // complex assignment
         // <string> statement // cmd call
         // <object> // value of <object>
         // <object> statement // method call
 
-        if (cmd instanceof CharSequence)
+        boolean dot = values.size() > 1 && ".".equals(String.valueOf(values.get(0)));
+
+        if (cmd instanceof CharSequence && !dot)
         {
             String scmd = cmd.toString();
 
-            if (values.size() > 0 && "=".equals(values.get(0)))
+            if (values.size() > 0 && Type.ASSIGN.equals(values.get(0)))
             {
+                Object value;
+
                 if (values.size() == 1)
                 {
                     return session.variables.remove(scmd);
                 }
-                else if (values.size() == 2)
+
+                if (values.size() == 2)
                 {
-                    Object value = values.get(1);
-                    if (value instanceof CharSequence)
-                    {
-                        value = eval((CharSequence) value);
-                    }
-                    return assignment(scmd, value);
+                    value = values.get(1);
                 }
                 else
                 {
-                    Object value = execute(values.get(1),
-                        values.subList(2, values.size()));
-                    return assignment(scmd, value);
+                    value = execute(values.get(1), values.subList(2, values.size()));
                 }
+
+                return assignment(scmd, value);
             }
             else
             {
                 String scopedFunction = scmd;
                 Object x = get(scmd);
+
                 if (!(x instanceof Function))
                 {
                     if (scmd.indexOf(':') < 0)
                     {
                         scopedFunction = "*:" + scmd;
                     }
+
                     x = get(scopedFunction);
+
                     if (x == null || !(x instanceof Function))
                     {
-                        throw new IllegalArgumentException("Command not found:  "
-                            + scopedFunction);
+                        // try default command handler
+                        if (session.get(".default.lock") == null)
+                        {
+                            x = get("default");
+                            if (x == null)
+                            {
+                                x = get("*:default");
+                            }
+
+                            if (x instanceof Function)
+                            {
+                                try
+                                {
+                                    session.put(".default.lock", "active");
+                                    values.add(0, scmd);
+                                    return ((Function) x).execute(session, values);
+                                }
+                                finally
+                                {
+                                    session.variables.remove(".default.lock");
+                                }
+                            }
+                        }
+
+                        throw new IllegalArgumentException("Command not found: " + scmd);
                     }
                 }
                 return ((Function) x).execute(session, values);
@@ -235,6 +409,33 @@
             {
                 return cmd;
             }
+            else if (dot)
+            {
+                // FELIX-1473 - allow methods calls on String objects
+                Object target = cmd;
+                ArrayList<Object> args = new ArrayList<Object>();
+                values.remove(0);
+
+                for (Object arg : values)
+                {
+                    if (".".equals(arg))
+                    {
+                        target = method(session, target, args.remove(0).toString(), args);
+                        args.clear();
+                    }
+                    else
+                    {
+                        args.add(arg);
+                    }
+                }
+
+                if (args.size() == 0)
+                {
+                    return target;
+                }
+
+                return method(session, target, args.remove(0).toString(), args);
+            }
             else
             {
                 return method(session, cmd, values.remove(0).toString(), values);
@@ -248,241 +449,58 @@
         return value;
     }
 
-    private Object eval(CharSequence seq) throws Exception
+    private Object array(Token array) throws Exception
     {
-        Object res = null;
-        StringBuilder sb = null;
-        Parser p = new Parser(seq);
-        int start = p.current;
-        while (!p.eof())
-        {
-            char c = p.peek();
-            if (!p.escaped)
-            {
-                if (c == '$' || c == '(' || c == '\'' || c == '"' || c == '[' || c == '{')
-                {
-                    if (start != p.current || res != null)
-                    {
-                        if (sb == null)
-                        {
-                            sb = new StringBuilder();
-                        }
-                        if (res != null)
-                        {
-                            if (res == parms)
-                            {
-                                for (int i = 0; i < parms.size(); i++)
-                                {
-                                    if (i > 0)
-                                    {
-                                        sb.append(' ');
-                                    }
-                                    sb.append(parms.get(i));
-                                }
-                            }
-                            else
-                            {
-                                sb.append(res);
-                            }
-                            res = null;
-                        }
-                        if (start != p.current)
-                        {
-                            sb.append(new Parser(p.text.subSequence(start, p.current)).unescape());
-                            start = p.current;
-                            continue;
-                        }
-                    }
-                    switch (c)
-                    {
-                        case '\'':
-                            p.next();
-                            p.quote(c);
-                            res = new Parser(p.text.subSequence(start + 1, p.current - 1)).unescape();
-                            start = p.current;
-                            continue;
-                        case '\"':
-                            p.next();
-                            p.quote(c);
-                            res = eval(p.text.subSequence(start + 1, p.current - 1));
-                            start = p.current;
-                            continue;
-                        case '[':
-                            p.next();
-                            res = array(seq.subSequence(start + 1, p.find(']', '[') - 1));
-                            start = p.current;
-                            continue;
-                        case '(':
-                            p.next();
-                            Closure cl = new Closure(session, this, p.text.subSequence(
-                                start + 1, p.find(')', '(') - 1));
-                            res = cl.execute(session, parms);
-                            start = p.current;
-                            continue;
-                        case '{':
-                            p.next();
-                            res = new Closure(session, this, p.text.subSequence(
-                                start + 1, p.find('}', '{') - 1));
-                            start = p.current;
-                            continue;
-                        case '$':
-                            p.next();
-                            res = var(p.findVar());
-                            start = p.current;
-                            continue;
-                    }
-                }
-            }
-            p.next();
-        }
-        if (start != p.current)
-        {
-            if (sb == null)
-            {
-                sb = new StringBuilder();
-            }
-            if (res != null)
-            {
-                if (res == parms)
-                {
-                    for (Object v : parms)
-                    {
-                        sb.append(v);
-                    }
-                }
-                else
-                {
-                    sb.append(res);
-                }
-                res = null;
-            }
-            sb.append(new Parser(p.text.subSequence(start, p.current)).unescape());
-        }
-        if (sb != null)
-        {
-            if (res != null)
-            {
-                if (res == parms)
-                {
-                    for (int i = 0; i < parms.size(); i++)
-                    {
-                        if (i > 0)
-                        {
-                            sb.append(' ');
-                        }
-                        sb.append(parms.get(i));
-                    }
-                }
-                else
-                {
-                    sb.append(res);
-                }
-            }
-            res = sb;
-        }
-        if (res instanceof CharSequence)
-        {
-            String r = res.toString();
-            if ("null".equals(r))
-            {
-                return null;
-            }
-            else if ("false".equals(r))
-            {
-                return false;
-            }
-            else if ("true".equals(r))
-            {
-                return true;
-            }
-            return r;
-        }
+        List<Token> list = new ArrayList<Token>();
+        Map<Token, Token> map = new LinkedHashMap<Token, Token>();
+        (new Parser(array)).array(list, map);
 
-        return res;
-    }
-
-    private Object array(CharSequence array) throws Exception
-    {
-        List<Object> list = new ArrayList<Object>();
-        Map<Object, Object> map = new LinkedHashMap<Object, Object>();
-        Parser p = new Parser(array);
-
-        while (!p.eof())
+        if (map.isEmpty())
         {
-            CharSequence token = p.value();
-
-            p.ws();
-            if (p.peek() == '=')
+            List<Object> olist = new ArrayList<Object>();
+            for (Token t : list)
             {
-                p.next();
-                p.ws();
-                if (!p.eof())
-                {
-                    CharSequence value = p.messy();
-                    map.put(eval(token), eval(value));
-                }
+                olist.add(eval(t));
             }
-            else
-            {
-                list.add(eval(token));
-            }
-
-            if (p.peek() == ',')
-            {
-                p.next();
-            }
-            p.ws();
-        }
-        p.ws();
-        if (!p.eof())
-        {
-            throw new IllegalArgumentException("Invalid array syntax: " + array);
-        }
-
-        if (map.size() != 0 && list.size() != 0)
-        {
-            throw new IllegalArgumentException("You can not mix maps and arrays: "
-                + array);
-        }
-
-        if (map.size() > 0)
-        {
-            return map;
+            return olist;
         }
         else
         {
-            return list;
+            Map<Object, Object> omap = new LinkedHashMap<Object, Object>();
+            for (Entry<Token, Token> e : map.entrySet())
+            {
+                Token key = e.getKey();
+                Object k = eval(key);
+                if (!(k instanceof String))
+                {
+                    throw new SyntaxError(key.line, key.column,
+                        "map key null or not String: " + key);
+                }
+                omap.put(k, eval(e.getValue()));
+            }
+            return omap;
         }
     }
 
-    private Object var(CharSequence var) throws Exception
-    {
-        if (var.charAt(0) == '{')
-        {
-            var = var.subSequence(1, var.length() - 1);
-        }
-        Object v = eval(var);
-        String name = v.toString();
-        return get(name);
-    }
-
-    /**
-     * @param name
-     * @return
-     */
-    private Object get(String name)
+    public Object get(String name)
     {
         if (parms != null)
         {
-            if ("it".equals(name))
-            {
-                return parms.get(0);
-            }
             if ("args".equals(name))
             {
                 return parms;
             }
 
+            if ("argv".equals(name))
+            {
+                return parmv;
+            }
+
+            if ("it".equals(name))
+            {
+                return parms.get(0);
+            }
+
             if (name.length() == 1 && Character.isDigit(name.charAt(0)))
             {
                 int i = name.charAt(0) - '0';
@@ -492,6 +510,67 @@
                 }
             }
         }
+
         return session.get(name);
     }
+
+    public Object put(String key, Object value)
+    {
+        return session.variables.put(key, value);
+    }
+
+    @Override
+    public String toString()
+    {
+        return source.toString().trim().replaceAll("\n+", "\n").replaceAll(
+            "([^\\\\{(\\[])\n", "\\1;").replaceAll("[ \\\\\t\n]+", " ");
+    }
+
+    /**
+     * List that overrides toString() for implicit $args expansion.
+     * Also checks for index out of bounds, so that $1 evaluates to null
+     * rather than throwing IndexOutOfBoundsException.
+     * e.g. x = { a$args }; x 1 2 => a1 2 and not a[1, 2]
+     */
+    class ArgList extends AbstractList<Object>
+    {
+        private List<Object> list;
+
+        public ArgList(List<Object> args)
+        {
+            this.list = args;
+        }
+
+        @Override
+        public String toString()
+        {
+            StringBuilder buf = new StringBuilder();
+            for (Object o : list)
+            {
+                if (buf.length() > 0)
+                    buf.append(' ');
+                buf.append(o);
+            }
+            return buf.toString();
+        }
+
+        @Override
+        public Object get(int index)
+        {
+            return index < list.size() ? list.get(index) : null;
+        }
+
+        @Override
+        public Object remove(int index)
+        {
+            return list.remove(index);
+        }
+
+        @Override
+        public int size()
+        {
+            return list.size();
+        }
+    }
+
 }
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandSessionImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandSessionImpl.java
index d1b76cc..1a26221 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandSessionImpl.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandSessionImpl.java
@@ -35,14 +35,14 @@
     public static final String VARIABLES = ".variables";
     public static final String COMMANDS = ".commands";
     private static final String COLUMN = "%-20s %s\n";
-    InputStream in;
-    PrintStream out;
+    protected InputStream in;
+    protected PrintStream out;
     PrintStream err;
     CommandShellImpl service;
-    final Map<Object, Object> variables = new HashMap<Object, Object>();
-    private boolean closed; // derek
+    protected final Map<String, Object> variables = new HashMap<String, Object>();
+    private boolean closed;
 
-    CommandSessionImpl(CommandShellImpl service, InputStream in, PrintStream out, PrintStream err)
+    protected CommandSessionImpl(CommandShellImpl service, InputStream in, PrintStream out, PrintStream err)
     {
         this.service = service;
         this.in = in;
@@ -52,7 +52,7 @@
 
     public void close()
     {
-        this.closed = true; // derek
+        this.closed = true;
     }
 
     public Object execute(CharSequence commandline) throws Exception
@@ -62,7 +62,7 @@
 
         if (closed)
         {
-            throw new IllegalStateException("session is closed"); // derek
+            throw new IllegalStateException("session is closed");
         }
 
         Closure impl = new Closure(this, null, commandline);
@@ -77,7 +77,6 @@
 
     public Object get(String name)
     {
-        // XXX: derek.baum@paremus.com
         // there is no API to list all variables, so overload name == null
         if (name == null || VARIABLES.equals(name))
         {
@@ -93,7 +92,7 @@
             return variables.get(name);
         }
 
-        // XXX: derek: add SCOPE support
+        // add SCOPE support
         if (name.startsWith("*:"))
         {
             String path = variables.containsKey("SCOPE") ? variables.get("SCOPE").toString()
@@ -353,4 +352,4 @@
             return "<can not format " + result + ":" + e;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandShellImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandShellImpl.java
index 13a1665..aa6f0c0 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandShellImpl.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/CommandShellImpl.java
@@ -37,7 +37,7 @@
     Set<Converter> converters = new HashSet<Converter>();
     protected ThreadIO threadIO;
     public final static Object NO_SUCH_COMMAND = new Object();
-    Map<String, Object> commands = new LinkedHashMap<String, Object>();
+    protected Map<String, Object> commands = new LinkedHashMap<String, Object>();
 
     public CommandShellImpl()
     {
@@ -219,4 +219,4 @@
         }
         return null;
     }
-}
\ No newline at end of file
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/EOFError.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/EOFError.java
new file mode 100644
index 0000000..4965e0d
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/EOFError.java
@@ -0,0 +1,30 @@
+/*
+ * 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.runtime.shell;
+
+public class EOFError extends SyntaxError
+{
+    private static final long serialVersionUID = 1L;
+    
+    public EOFError(int line, int column, String message)
+    {
+        super(line, column, message);
+    }
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Evaluate.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Evaluate.java
new file mode 100644
index 0000000..4111912
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Evaluate.java
@@ -0,0 +1,29 @@
+/*
+ * 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.runtime.shell;
+
+public interface Evaluate
+{
+    Object eval(Token t) throws Exception;
+    
+    Object get(String key);
+    
+    Object put(String key, Object value);
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Parser.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Parser.java
index 6c915ce..de11bfd 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Parser.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Parser.java
@@ -22,421 +22,155 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.gogo.runtime.shell.Tokenizer.Type;
 
 public class Parser
 {
-    int current = 0;
-    CharSequence text;
-    boolean escaped;
-    static final String SPECIAL = "<;|{[\"'$`(=";
+    private final Tokenizer tz;
 
     public Parser(CharSequence program)
     {
-        text = program;
+        tz = new Tokenizer(program);
     }
 
-    void ws()
+    public List<List<List<Token>>> program()
     {
-        // derek: BUGFIX: loop if comment  at beginning of input
-        //while (!eof() && Character.isWhitespace(peek())) {
-        while (!eof() && (!escaped && Character.isWhitespace(peek()) || current == 0))
-        {
-            if (current != 0 || !escaped && Character.isWhitespace(peek()))
-            {
-                current++;
-            }
-            if (peek() == '/' && current < text.length() - 2
-                && text.charAt(current + 1) == '/')
-            {
-                comment();
-            }
-            if (current == 0)
-            {
-                break;
-            }
-        }
-    }
-
-    private void comment()
-    {
-        while (!eof() && peek() != '\n' && peek() != '\r')
-        {
-            next();
-        }
-    }
-
-    boolean eof()
-    {
-        return current >= text.length();
-    }
-
-    char peek()
-    {
-        return peek(false);
-    }
-
-    char peek(boolean increment)
-    {
-        escaped = false;
-        if (eof())
-        {
-            return 0;
-        }
-
-        int last = current;
-        char c = text.charAt(current++);
-
-        if (c == '\\')
-        {
-            escaped = true;
-            if (eof())
-            {
-                throw new RuntimeException("Eof found after \\"); // derek
-            }
-
-            c = text.charAt(current++);
-
-            switch (c)
-            {
-                case 't':
-                    c = '\t';
-                    break;
-                case '\r':
-                case '\n':
-                    c = ' ';
-                    break;
-                case 'b':
-                    c = '\b';
-                    break;
-                case 'f':
-                    c = '\f';
-                    break;
-                case 'n':
-                    c = '\n';
-                    break;
-                case 'r':
-                    c = '\r';
-                    break;
-                case 'u':
-                    c = unicode();
-                    current += 4;
-                    break;
-                default:
-                // We just take the next character literally
-                // but have the escaped flag set, important for {},[] etc
-            }
-        }
-        if (!increment)
-        {
-            current = last;
-        }
-        return c;
-    }
-
-    public List<List<List<CharSequence>>> program()
-    {
-        List<List<List<CharSequence>>> program = new ArrayList<List<List<CharSequence>>>();
-        ws();
-        if (!eof())
+        List<List<List<Token>>> program = new ArrayList<List<List<Token>>>();
+        
+        while (tz.next() != Type.EOT)
         {
             program.add(pipeline());
-            while (peek() == ';')
+            
+            switch (tz.type())
             {
-                current++;
-                List<List<CharSequence>> pipeline = pipeline();
-                if (pipeline.get(0).get(0).length() != 0)
-                {
-                    program.add(pipeline);
-                }
+                case SEMICOLON:
+                case NEWLINE:
+                    continue;
             }
+            
+            break;
         }
-        if (!eof())
-        {
-            throw new RuntimeException("Program has trailing text: " + context(current));
-        }
+
+        if (tz.next() != Type.EOT)
+            throw new RuntimeException("Program has trailing text: " + tz.value());
 
         return program;
     }
 
-    CharSequence context(int around)
+    private List<List<Token>> pipeline()
     {
-        return text.subSequence(Math.max(0, current - 20), Math.min(text.length(),
-            current + 4));
+        List<List<Token>> pipeline = new ArrayList<List<Token>>();
+        
+        while (true)
+        {
+            pipeline.add(command());
+            switch (tz.type())
+            {
+                case PIPE:
+                    if (tz.next() == Type.EOT)
+                    {
+                        Token t = tz.token();
+                        throw new EOFError(t.line, t.column, "unexpected EOT after pipe '|'");
+                    }
+                    break;
+
+                default:
+                    return pipeline;
+            }
+        }
     }
 
-    public List<List<CharSequence>> pipeline()
+    private List<Token> command()
     {
-        List<List<CharSequence>> statements = new ArrayList<List<CharSequence>>();
-        statements.add(statement());
-        while (peek() == '|')
+        List<Token> command = new ArrayList<Token>();
+
+        while (true)
         {
-            current++;
-            ws();
-            if (!eof())
+            Token t = tz.token();
+            
+            switch (t.type)
             {
-                statements.add(statement());
+                case WORD:
+                case CLOSURE:
+                case EXECUTION:
+                case ARRAY:
+                case ASSIGN:
+                    break;
+                    
+                default:
+                    throw new SyntaxError(t.line, t.column, "unexpected token: " + t.type);
+            }
+            
+            command.add(t);
+            
+            switch (tz.next())
+            {
+                case PIPE:
+                case SEMICOLON:
+                case NEWLINE:
+                case EOT:
+                    return command;
+            }
+        }
+    }
+    
+    public void array(List<Token> list, Map<Token, Token> map) throws Exception
+    {
+        Token lt = null;
+        boolean isMap = false;
+
+        while (tz.next() != Type.EOT)
+        {
+            if (isMap)
+            {
+                Token key = lt;
+                lt = null;
+                if (null == key)
+                {
+                    key = tz.token();
+
+                    if (tz.next() != Type.ASSIGN)
+                    {
+                        Token t = tz.token();
+                        throw new SyntaxError(t.line, t.column,
+                            "map expected '=', found: " + t);
+                    }
+
+                    tz.next();
+                }
+
+                Token k = (list.isEmpty() ? key : list.remove(0));
+                Token v = tz.token();
+                map.put(k, v);
             }
             else
             {
-                throw new RuntimeException("Eof found after pipe |");
-            }
-        }
-        return statements;
-    }
-
-    public List<CharSequence> statement()
-    {
-        List<CharSequence> statement = new ArrayList<CharSequence>();
-        statement.add(value());
-        while (!eof())
-        {
-            ws();
-            if (peek() == '|' || peek() == ';')
-            {
-                break;
-            }
-
-            if (!eof())
-            {
-                statement.add(messy());
-            }
-        }
-        return statement;
-    }
-
-    public CharSequence messy()
-    {
-        char c = peek();
-        if (c > 0 && SPECIAL.indexOf(c) < 0)
-        {
-            int start = current++;
-            while (!eof())
-            {
-                c = peek();
-                if (!escaped && (c == ';' || c == '|' || Character.isWhitespace(c)))
+                switch (tz.type())
                 {
-                    break;
-                }
-                next();
-            }
-            return text.subSequence(start, current);
-        }
-        else
-        {
-            return value();
-        }
-    }
+                    case WORD:
+                    case CLOSURE:
+                    case EXECUTION:
+                    case ARRAY:
+                        lt = tz.token();
+                        list.add(lt);
+                        break;
 
-    CharSequence value()
-    {
-        ws();
-
-        int start = current;
-        char c = next();
-        if (!escaped)
-        {
-            switch (c)
-            {
-                case '{':
-                    return text.subSequence(start, find('}', '{'));
-                case '(':
-                    return text.subSequence(start, find(')', '('));
-                case '[':
-                    return text.subSequence(start, find(']', '['));
-                case '<':
-                    return text.subSequence(start, find('>', '<'));
-                case '=':
-                    return text.subSequence(start, current);
-                case '"':
-                case '\'':
-                    quote(c);
-                    break;
-            }
-        }
-
-        // Some identifier or number
-        while (!eof())
-        {
-            c = peek();
-            if (!escaped)
-            {
-                if (Character.isWhitespace(c) || c == ';' || c == '|' || c == '=')
-                {
-                    break;
-                }
-                else if (c == '{')
-                {
-                    next();
-                    find('}', '{');
-                }
-                else if (c == '(')
-                {
-                    next();
-                    find(')', '(');
-                }
-                else if (c == '<')
-                {
-                    next();
-                    find('>', '<');
-                }
-                else if (c == '[')
-                {
-                    next();
-                    find(']', '[');
-                }
-                else if (c == '\'' || c == '"')
-                {
-                    next();
-                    quote(c);
-                    next();
-                }
-                else
-                {
-                    next();
-                }
-            }
-            else
-            {
-                next();
-            }
-        }
-        return text.subSequence(start, current);
-    }
-
-    boolean escaped()
-    {
-        return escaped;
-    }
-
-    char next()
-    {
-        return peek(true);
-    }
-
-    char unicode()
-    {
-        if (current + 4 > text.length())
-        {
-            throw new IllegalArgumentException("Unicode \\u escape at eof at pos ..."
-                + context(current) + "...");
-        }
-
-        String s = text.subSequence(current, current + 4).toString();
-        int n = Integer.parseInt(s, 16);
-        return (char) n;
-    }
-
-    int find(char target, char deeper)
-    {
-        int start = current;
-        int level = 1;
-
-        while (level != 0)
-        {
-            if (eof())
-            {
-                throw new RuntimeException("Eof found in the middle of a compound for '"
-                    + target + deeper + "', begins at " + context(start));
-            }
-
-            char c = next();
-            if (!escaped)
-            {
-                if (c == target)
-                {
-                    level--;
-                }
-                else
-                {
-                    if (c == deeper)
-                    {
-                        level++;
-                    }
-                    else
-                    {
-                        if (c == '"')
+                    case ASSIGN:
+                        if (list.size() == 1)
                         {
-                            quote('"');
+                            isMap = true;
+                            break;
                         }
-                        else
-                        {
-                            if (c == '\'')
-                            {
-                                quote('\'');
-                            }
-                            else
-                            {
-                                if (c == '`')
-                                {
-                                    quote('`');
-                                }
-                            }
-                        }
-                    }
+                        // fall through
+                    default:
+                        lt = tz.token();
+                        throw new SyntaxError(lt.line, lt.column,
+                            "unexpected token in list: " + lt);
                 }
             }
         }
-        return current;
     }
 
-    int quote(char which)
-    {
-        while (!eof() && (peek() != which || escaped))
-        {
-            next();
-        }
-
-        return current++;
-    }
-
-    CharSequence findVar()
-    {
-        int start = current;
-        char c = peek();
-
-        if (c == '{')
-        {
-            next();
-            int end = find('}', '{');
-            return text.subSequence(start, end);
-        }
-        if (c == '(')
-        {
-            next();
-            int end = find(')', '(');
-            return text.subSequence(start, end);
-        }
-
-        if (Character.isJavaIdentifierPart(c))
-        {
-            while (c == '$')
-            {
-                c = next();
-            }
-            while (!eof() && (Character.isJavaIdentifierPart(c) || c == '.') && c != '$')
-            {
-                next();
-                c = peek();
-            }
-            return text.subSequence(start, current);
-        }
-        throw new IllegalArgumentException(
-            "Reference to variable does not match syntax of a variable: "
-            + context(start));
-    }
-
-    public String toString()
-    {
-        return "..." + context(current) + "...";
-    }
-
-    public String unescape()
-    {
-        StringBuilder sb = new StringBuilder();
-        while (!eof())
-        {
-            sb.append(next());
-        }
-        return sb.toString();
-    }
-}
\ No newline at end of file
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Pipe.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Pipe.java
index d25c085..237b044 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Pipe.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Pipe.java
@@ -39,16 +39,22 @@
     Closure closure;
     Exception exception;
     Object result;
-    List<CharSequence> statement;
+    List<Token> statement;
 
-    public static void reset()
+    public static Object[] mark()
     {
-        tIn.set(null);
-        tOut.set(null);
-        tErr.set(null);
+        Object[] mark = {tIn.get(), tOut.get(), tErr.get() };
+        return mark;
     }
 
-    public Pipe(Closure closure, List<CharSequence> statement)
+    public static void reset(Object[] mark)
+    {
+        tIn.set((InputStream) mark[0]);
+        tOut.set((PrintStream) mark[1]);
+        tErr.set((PrintStream) mark[2]);
+    }
+
+    public Pipe(Closure closure, List<Token> statement)
     {
         super("pipe-" + statement);
         this.closure = closure;
@@ -94,14 +100,17 @@
         tIn.set(in);
         tOut.set(out);
         tErr.set(err);
-        closure.session.service.threadIO.setStreams(in, out, err);
-
+        closure.session().service.threadIO.setStreams(in, out, err);
+        
         try
         {
             result = closure.executeStatement(statement);
             if (result != null && pout != null)
             {
-                out.println(closure.session.format(result, Converter.INSPECT));
+                if (!Boolean.FALSE.equals(closure.session().get(".FormatPipe")))
+                {
+                    out.println(closure.session().format(result, Converter.INSPECT));
+                }
             }
         }
         catch (Exception e)
@@ -111,7 +120,7 @@
         finally
         {
             out.flush();
-            closure.session.service.threadIO.close();
+            closure.session().service.threadIO.close();
 
             try
             {
@@ -130,4 +139,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/SyntaxError.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/SyntaxError.java
new file mode 100644
index 0000000..bbaea93
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/SyntaxError.java
@@ -0,0 +1,44 @@
+/*
+ * 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.runtime.shell;
+
+public class SyntaxError extends RuntimeException
+{
+    private static final long serialVersionUID = 1L;
+    private final int line;
+    private final int column;
+
+    public SyntaxError(int line, int column, String message)
+    {
+        super(message);
+        this.line = line;
+        this.column = column;
+    }
+    
+    public int column()
+    {
+        return column;
+    }
+
+    public int line()
+    {
+        return line;
+    }
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Token.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Token.java
new file mode 100644
index 0000000..097bf0b
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Token.java
@@ -0,0 +1,81 @@
+/*
+ * 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.runtime.shell;
+
+import org.apache.felix.gogo.runtime.shell.Tokenizer.Type;
+
+public class Token implements CharSequence
+{
+    Type type; 
+    CharSequence value;
+    short line;
+    short column;
+    
+    public Token(Type type, CharSequence value, short line, short column)
+    {
+        this.type = type;
+        this.value = value;
+        this.line = line;
+        this.column = column;
+    }
+
+    @Override
+    public String toString()
+    {
+        //return type + "<" + value + ">";
+        return null == value ? type.toString() : value.toString();
+    }
+    
+    public char charAt(int index)
+    {
+        return  value.charAt(index);
+    }
+
+    public int length()
+    {
+        return (null == value ? 0 : value.length());
+    }
+
+    public CharSequence subSequence(int start, int end)
+    {
+        return value.subSequence(start, end);
+    }
+    
+    public String source()
+    {
+        switch (type)
+        {
+            case WORD:
+                return value.toString();
+                
+            case CLOSURE:
+                return "{" + value + "}";
+                
+            case EXECUTION:
+                return "(" + value + ")";
+                
+            case ARRAY:
+                return "[" + value + "]";
+                
+            default:
+                return type.toString();
+        }
+    }
+}
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Tokenizer.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Tokenizer.java
new file mode 100644
index 0000000..f70cbe0
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/shell/Tokenizer.java
@@ -0,0 +1,755 @@
+/*
+ * 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.runtime.shell;
+
+
+/**
+ * Bash-like tokenizer.
+ * 
+ * Single and double quotes are just like Bash - single quotes escape everything
+ * (including backslashes), newlines are allowed in quotes.
+ * backslash-newline indicates a line continuation and is removed.
+ * 
+ * Variable expansion is just like Bash: $NAME or ${NAME[[:][-+=?WORD]},
+ * except it can yield any Object. Variables expanded within double-quotes,
+ * or adjacent to a String are converted to String.
+ * 
+ * Unlike bash, indirect variable expansion is supported using ${$NAME}.
+ * 
+ * Only a single variable assignment is recognized, with '=' being the second token.
+ * (Bash allows name1=value1 name2=value2 ... command args)
+ * 
+ * Comments can only start where white space is allowed:
+ * # or // starts a line comment, /* starts a block comment.
+ * The following common uses do NOT start comments:
+ *    ls http://example.com#anchor
+ *    ls $dir/*.java
+ * 
+ * @see http://wiki.bash-hackers.org/syntax/basicgrammar
+ */
+public class Tokenizer
+{
+    public enum Type
+    {
+        ASSIGN('='), PIPE('|'), SEMICOLON(';'), NEWLINE, ARRAY, CLOSURE, EXECUTION, WORD, EOT;
+
+        private char c;
+
+        Type()
+        {
+        }
+
+        Type(char c)
+        {
+            this.c = c;
+        }
+
+        @Override
+        public String toString()
+        {
+            return (c == 0 ? super.toString() : "'" + c + "'");
+        }
+    }
+
+    private static final boolean DEBUG = false;
+    private static final char EOT = (char) -1;
+
+    private final CharSequence text;
+    private final Evaluate evaluate;
+    private final boolean inArray;
+    private final boolean inQuote;
+
+    private Type type = Type.NEWLINE;
+    private CharSequence value;
+    private Token token;
+
+    private short line;
+    private short column;
+    private char ch;
+    private int index;
+    private boolean firstWord;
+
+    public Tokenizer(CharSequence text)
+    {
+        this(text, null, false);
+    }
+
+    public Tokenizer(CharSequence text, Evaluate evaluate, boolean inQuote)
+    {
+        this.text = text;
+        this.evaluate = evaluate;
+        this.inQuote = inQuote;
+        index = 0;
+        line = column = 1;
+
+        boolean array = false;
+
+        if (text instanceof Token)
+        {
+            Token t = (Token) text;
+            line = t.line;
+            column = t.column;
+            array = (Type.ARRAY == t.type);
+        }
+
+        inArray = array;
+        getch();
+
+        if (DEBUG)
+        {
+            if (inArray)
+                System.err.println("Tokenizer[" + text + "]");
+            else
+                System.err.println("Tokenizer<" + text + ">");
+        }
+    }
+
+    public Type type()
+    {
+        return type;
+    }
+
+    public CharSequence value()
+    {
+        return value;
+    }
+
+    public Token token()
+    {
+        return token;
+    }
+
+    public Type next()
+    {
+        final Type prevType = type;
+        token = null;
+        value = null;
+
+        short tLine;
+        short tColumn;
+
+        while (true)
+        {
+            skipSpace();
+            tLine = line;
+            tColumn = column;
+
+            switch (ch)
+            {
+                case EOT:
+                    type = Type.EOT;
+                    break;
+
+                case '\n':
+                    getch();
+                    if (inArray)
+                        continue;
+                    // only return NEWLINE once and not if not preceded by ; or |
+                    switch (prevType)
+                    {
+                        case PIPE:
+                        case SEMICOLON:
+                        case NEWLINE:
+                            continue;
+
+                        default:
+                            type = Type.NEWLINE;
+                            break;
+                    }
+                    break;
+
+                case '{':
+                case '(':
+                case '[':
+                    value = group();
+                    getch();
+                    break;
+
+                case ';':
+                    getch();
+                    type = Type.SEMICOLON;
+                    break;
+
+                case '|':
+                    getch();
+                    type = Type.PIPE;
+                    break;
+
+                case '=':
+                    if (firstWord || inArray)
+                    {
+                        getch();
+                        type = Type.ASSIGN;
+                        break;
+                    }
+                    // fall through
+                default:
+                    value = word();
+                    type = Type.WORD;
+            }
+
+            firstWord = (Type.WORD == type && (Type.WORD != prevType && Type.ASSIGN != prevType));
+            token = new Token(type, value, tLine, tColumn);
+
+            if (DEBUG)
+            {
+                System.err.print("<" + type + ">");
+                if (Type.EOT == type)
+                {
+                    System.err.println();
+                }
+            }
+
+            return type;
+        }
+    }
+
+    private CharSequence word()
+    {
+        int start = index - 1;
+        int skipCR = 0;
+
+        do
+        {
+            switch (ch)
+            {
+                case '\n':
+                    if (index >= 2 && text.charAt(index - 2) == '\r')
+                        skipCR = 1;
+                    // fall through
+                case '=':
+                    if ((Type.WORD == type || Type.ASSIGN == type) && '=' == ch
+                        && !inArray)
+                        continue;
+                    // fall through
+                case ' ':
+                case '\t':
+                case '|':
+                case ';':
+                    return text.subSequence(start, index - 1 - skipCR);
+
+                case '{':
+                    group();
+                    break;
+
+                case '\\':
+                    escape();
+                    break;
+
+                case '\'':
+                case '"':
+                    skipQuote();
+                    break;
+            }
+        }
+        while (getch() != EOT);
+
+        return text.subSequence(start, index - 1);
+    }
+
+    private CharSequence group()
+    {
+        final char push = ch;
+        final char pop;
+
+        switch (ch)
+        {
+            case '{':
+                type = Type.CLOSURE;
+                pop = '}';
+                break;
+            case '(':
+                type = Type.EXECUTION;
+                pop = ')';
+                break;
+            case '[':
+                type = Type.ARRAY;
+                pop = ']';
+                break;
+            default:
+                assert false;
+                pop = 0;
+        }
+
+        short sLine = line;
+        short sCol = column;
+        int start = index;
+        int depth = 1;
+
+        while (true)
+        {
+            boolean comment = false;
+
+            switch (ch)
+            {
+                case '{':
+                case '(':
+                case '[':
+                case '\n':
+                    comment = true;
+                    break;
+            }
+
+            if (getch() == EOT)
+            {
+                throw new EOFError(sLine, sCol, "unexpected EOT looking for matching '"
+                    + pop + "'");
+            }
+
+            // don't recognize comments that start within a word
+            if (comment || isBlank(ch))
+                skipSpace();
+
+            switch (ch)
+            {
+                case '"':
+                case '\'':
+                    skipQuote();
+                    break;
+
+                case '\\':
+                    ch = escape();
+                    break;
+
+                default:
+                    if (push == ch)
+                        depth++;
+                    else if (pop == ch && --depth == 0)
+                        return text.subSequence(start, index - 1);
+            }
+        }
+
+    }
+
+    private char escape()
+    {
+        assert '\\' == ch;
+
+        switch (getch())
+        {
+            case 'u':
+                getch();
+                getch();
+                getch();
+                getch();
+
+                if (EOT == ch)
+                {
+                    throw new EOFError(line, column, "unexpected EOT in \\u escape");
+                }
+
+                String u = text.subSequence(index - 4, index).toString();
+
+                try
+                {
+                    return (char) Integer.parseInt(u, 16);
+                }
+                catch (NumberFormatException e)
+                {
+                    throw new SyntaxError(line, column, "bad unicode escape: \\u" + u);
+                }
+
+            case EOT:
+                throw new EOFError(line, column, "unexpected EOT in \\ escape");
+
+            case '\n':
+                return '\0'; // line continuation
+
+            case '\\':
+            case '\'':
+            case '"':
+            case '$':
+                return ch;
+
+            default:
+                return ch;
+        }
+    }
+
+    private void skipQuote()
+    {
+        assert '\'' == ch || '"' == ch;
+        final char quote = ch;
+        final short sLine = line;
+        final short sCol = column;
+
+        while (getch() != EOT)
+        {
+            if (quote == ch)
+                return;
+
+            if ((quote == '"') && ('\\' == ch))
+                escape();
+        }
+
+        throw new EOFError(sLine, sCol, "unexpected EOT looking for matching quote: "
+            + quote);
+    }
+
+    private void skipSpace()
+    {
+        while (true)
+        {
+            while (isBlank(ch))
+            {
+                getch();
+            }
+
+            // skip continuation lines, but not other escapes
+            if (('\\' == ch) && (peek() == '\n'))
+            {
+                getch();
+                getch();
+                continue;
+            }
+
+            // skip comments
+            if (('/' == ch) || ('#' == ch))
+            {
+                if (('#' == ch) || (peek() == '/'))
+                {
+                    while ((getch() != EOT) && ('\n' != ch))
+                    {
+                    }
+                    continue;
+                }
+                else if ('*' == peek())
+                {
+                    short sLine = line;
+                    short sCol = column;
+                    getch();
+
+                    while ((getch() != EOT) && !(('*' == ch) && (peek() == '/')))
+                    {
+                    }
+
+                    if (EOT == ch)
+                    {
+                        throw new EOFError(sLine, sCol,
+                            "unexpected EOT looking for closing comment: */");
+                    }
+
+                    getch();
+                    getch();
+                    continue;
+                }
+            }
+
+            break;
+        }
+    }
+
+    private boolean isBlank(char ch)
+    {
+        return ' ' == ch || '\t' == ch;
+    }
+
+    private boolean isName(char ch)
+    {
+        return Character.isJavaIdentifierPart(ch) && (ch != '$') || ('.' == ch);
+    }
+
+    /**
+     * expand variables, quotes and escapes in word.
+     * @param vars
+     * @return
+     */
+    public static Object expand(CharSequence word, Evaluate eval)
+    {
+        return expand(word, eval, false);
+    }
+
+    private static Object expand(CharSequence word, Evaluate eval,
+        boolean inQuote)
+    {
+        final String special = "$\\\"'";
+        int i = word.length();
+
+        while ((--i >= 0) && (special.indexOf(word.charAt(i)) == -1))
+        {
+        }
+
+        // shortcut if word doesn't contain any special characters
+        if (i < 0)
+            return word;
+
+        return new Tokenizer(word, eval, inQuote).expand();
+    }
+
+    public Object expand(CharSequence word, short line, short column)
+    {
+        return expand(new Token(Type.WORD, word, line, column), evaluate, inQuote);
+    }
+
+    private Token word(CharSequence value)
+    {
+        return new Token(Type.WORD, value, line, column);
+    }
+
+    private Object expand()
+    {
+        StringBuilder buf = new StringBuilder();
+
+        while (ch != EOT)
+        {
+            int start = index;
+
+            switch (ch)
+            {
+                case '$':
+                    Object val = expandVar();
+
+                    if (EOT == ch && buf.length() == 0)
+                    {
+                        return val;
+                    }
+
+                    if (null != val)
+                    {
+                        buf.append(val);
+                    }
+
+                    continue; // expandVar() has already read next char
+
+                case '\\':
+                    ch = (inQuote && ("u$\\\n\"".indexOf(peek()) == -1)) ? '\\'
+                        : escape();
+
+                    if (ch != '\0') // ignore line continuation
+                    {
+                        buf.append(ch);
+                    }
+
+                    break;
+
+                case '"':
+                    Token ww = word(null);
+                    skipQuote();
+                    ww.value = text.subSequence(start, index - 1);
+                    value = ww;
+                    Object expand = expand(value, evaluate, true);
+
+                    if (eot() && buf.length() == 0 && value.equals(expand))
+                    {
+                        return ww.value;
+                    }
+
+                    if (null != expand)
+                    {
+                        buf.append(expand.toString());
+                    }
+                    break;
+
+                case '\'':
+                    if (!inQuote)
+                    {
+                        skipQuote();
+                        value = text.subSequence(start, index - 1);
+
+                        if (eot() && buf.length() == 0)
+                        {
+                            return value;
+                        }
+
+                        buf.append(value);
+                        break;
+                    }
+                    // else fall through
+                default:
+                    buf.append(ch);
+            }
+
+            getch();
+        }
+
+        return buf.toString();
+    }
+
+    private Object expandVar()
+    {
+        assert '$' == ch;
+        Object val;
+
+        if (getch() != '{')
+        {
+            int start = index - 1;
+            while (isName(ch))
+            {
+                getch();
+            }
+
+            if (index - 1 == start)
+            {
+                val = "$";
+            }
+            else
+            {
+                String name = text.subSequence(start, index - 1).toString();
+                val = evaluate.get(name);
+            }
+        }
+        else
+        {
+            // ${NAME[[:]-+=?]WORD}
+            short sLine = line;
+            short sCol = column;
+            CharSequence group = group();
+            char c;
+            int i = 0;
+
+            while (i < group.length())
+            {
+                switch (group.charAt(i))
+                {
+                    case ':':
+                    case '-':
+                    case '+':
+                    case '=':
+                    case '?':
+                        break;
+
+                    default:
+                        ++i;
+                        continue;
+                }
+                break;
+            }
+
+            sCol += i;
+
+            String name = String.valueOf(expand(group.subSequence(0, i), sLine, sCol));
+
+            for (int j = 0; j < name.length(); ++j)
+            {
+                if (!isName(name.charAt(j)))
+                {
+                    throw new SyntaxError(sLine, sCol, "bad name: ${" + group + "}");
+                }
+            }
+
+            val = evaluate.get(name);
+
+            if (i < group.length())
+            {
+                c = group.charAt(i++);
+                if (':' == c)
+                {
+                    c = (i < group.length() ? group.charAt(i++) : EOT);
+                }
+
+                CharSequence word = group.subSequence(i, group.length());
+
+                switch (c)
+                {
+                    case '-':
+                    case '=':
+                        if (null == val)
+                        {
+                            val = expand(word, evaluate, false);
+                            if ('=' == c)
+                            {
+                                evaluate.put(name, val);
+                            }
+                        }
+                        break;
+
+                    case '+':
+                        if (null != val)
+                        {
+                            val = expand(word, evaluate, false);
+                        }
+                        break;
+
+                    case '?':
+                        if (null == val)
+                        {
+                            val = expand(word, evaluate, false);
+                            if (null == val || val.toString().length() == 0)
+                            {
+                                val = "parameter not set";
+                            }
+                            throw new IllegalArgumentException(name + ": " + val);
+                        }
+                        break;
+
+                    default:
+                        throw new SyntaxError(sLine, sCol, "bad substitution: ${" + group
+                            + "}");
+                }
+            }
+            getch();
+        }
+
+        return val;
+    }
+
+    /**
+     * returns true if getch() will return EOT
+     * @return
+     */
+    private boolean eot()
+    {
+        return index >= text.length();
+    }
+
+    private char getch()
+    {
+        return ch = getch(false);
+    }
+
+    private char peek()
+    {
+        return getch(true);
+    }
+
+    private char getch(boolean peek)
+    {
+        if (eot())
+        {
+            if (!peek)
+            {
+                ++index;
+                ch = EOT;
+            }
+            return EOT;
+        }
+
+        int current = index;
+        char c = text.charAt(index++);
+
+        if (('\r' == c) && !eot() && (text.charAt(index) == '\n'))
+            c = text.charAt(index++);
+
+        if (peek)
+        {
+            index = current;
+        }
+        else if ('\n' == c)
+        {
+            ++line;
+            column = 0;
+        }
+        else
+            ++column;
+
+        return c;
+    }
+
+}
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/Context.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/Context.java
index afb8284..505713b 100644
--- a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/Context.java
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/Context.java
@@ -41,7 +41,16 @@
 
     public Object execute(CharSequence source) throws Exception
     {
-        return session.execute(source);
+        Object result = new Exception();
+        try
+        {
+            return result = session.execute(source);
+        }
+        finally
+        {
+            System.err.println("execute<" + source + "> = ("
+                + (null == result ? "Null" : result.getClass().getSimpleName()) + ")(" + result + ")\n");
+        }
     }
 
     public void addCommand(String name, Object target)
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser.java
index b75ec82..c18139d 100644
--- a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser.java
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser.java
@@ -126,14 +126,14 @@
         Context c = new Context();
         c.addCommand("echo", this);
         c.addCommand("grep", this);
-        assertEquals("a", c.execute("a = a; echo $$a"));
+        assertEquals("a", c.execute("a = a; echo ${$a}"));
 
         assertEquals("hello", c.execute("echo hello"));
         assertEquals("hello", c.execute("a = (echo hello)"));
-        assertEquals("a", c.execute("a = a; echo $(echo a)"));
+      //assertEquals("a", c.execute("a = a; echo $(echo a)")); // #p2 - no eval in var expansion
         assertEquals("3", c.execute("a=3; echo $a"));
         assertEquals("3", c.execute("a = 3; echo $a"));
-        assertEquals("a", c.execute("a = a; echo $$a"));
+        assertEquals("a", c.execute("a = a; echo ${$a}"));
     }
 
     public void testComment() throws Exception
@@ -149,12 +149,6 @@
         c.addCommand("echo", this);
         c.addCommand("capture", this);
 
-        assertEquals("http://www.aqute.biz?com=2&biz=1",
-            c.execute("['http://www.aqute.biz?com=2&biz=1'] get 0"));
-        assertEquals("{a=2, b=3}", c.execute("[a=2 b=3]").toString());
-        assertEquals("3", c.execute("[a=2 b=3] get b"));
-        assertEquals("[3, 4]", c.execute("[1 2 [3 4] 5 6] get 2").toString());
-        assertEquals(5, c.execute("[1 2 [3 4] 5 6] size"));
         assertEquals("a", c.execute("e = { echo $1 } ; e a   b"));
         assertEquals("b", c.execute("e = { echo $2 } ; e a   b"));
         assertEquals("b", c.execute("e = { $args } ; e echo  b"));
@@ -166,6 +160,7 @@
     public void testArray() throws Exception
     {
         Context c = new Context();
+        c.set("echo", true);
         assertEquals("http://www.aqute.biz?com=2&biz=1",
             c.execute("['http://www.aqute.biz?com=2&biz=1'] get 0"));
         assertEquals("{a=2, b=3}", c.execute("[a=2 b=3]").toString());
@@ -174,24 +169,15 @@
         assertEquals(5, c.execute("[1 2 [3 4] 5 6] size"));
     }
 
-    public void testEscape()
-    {
-        Parser parser = new Parser("'a|b;c'");
-        CharSequence cs = parser.messy();
-        assertEquals("'a|b;c'", cs.toString());
-        assertEquals("'a|b;c'", new Parser(cs).unescape());
-        assertEquals("$a", new Parser("\\$a").unescape());
-    }
-
     public void testParentheses()
     {
         Parser parser = new Parser("(a|b)|(d|f)");
-        List<List<List<CharSequence>>> p = parser.program();
-        assertEquals("(a|b)", p.get(0).get(0).get(0));
+        List<List<List<Token>>> p = parser.program();
+        assertEquals("a|b", p.get(0).get(0).get(0).toString());
 
         parser = new Parser("grep (d.*)|grep (d|f)");
         p = parser.program();
-        assertEquals("(d.*)", p.get(0).get(0).get(1));
+        assertEquals("d.*", p.get(0).get(0).get(1).toString());
     }
 
     public void testEcho() throws Exception
@@ -322,41 +308,42 @@
 
     public void testProgram()
     {
-        List<List<List<CharSequence>>> x = new Parser("abc def|ghi jkl;mno pqr|stu vwx").program();
-        assertEquals("abc", x.get(0).get(0).get(0));
-        assertEquals("def", x.get(0).get(0).get(1));
-        assertEquals("ghi", x.get(0).get(1).get(0));
-        assertEquals("jkl", x.get(0).get(1).get(1));
-        assertEquals("mno", x.get(1).get(0).get(0));
-        assertEquals("pqr", x.get(1).get(0).get(1));
-        assertEquals("stu", x.get(1).get(1).get(0));
-        assertEquals("vwx", x.get(1).get(1).get(1));
+        List<List<List<Token>>> x = new Parser("abc def|ghi jkl;mno pqr|stu vwx").program();
+        assertEquals("abc", x.get(0).get(0).get(0).toString());
+        assertEquals("def", x.get(0).get(0).get(1).toString());
+        assertEquals("ghi", x.get(0).get(1).get(0).toString());
+        assertEquals("jkl", x.get(0).get(1).get(1).toString());
+        assertEquals("mno", x.get(1).get(0).get(0).toString());
+        assertEquals("pqr", x.get(1).get(0).get(1).toString());
+        assertEquals("stu", x.get(1).get(1).get(0).toString());
+        assertEquals("vwx", x.get(1).get(1).get(1).toString());
     }
 
     public void testStatements()
     {
-        List<List<CharSequence>> x = new Parser("abc def|ghi jkl|mno pqr").pipeline();
-        assertEquals("abc", x.get(0).get(0));
-        assertEquals("def", x.get(0).get(1));
-        assertEquals("ghi", x.get(1).get(0));
-        assertEquals("jkl", x.get(1).get(1));
-        assertEquals("mno", x.get(2).get(0));
-        assertEquals("pqr", x.get(2).get(1));
+        List<List<Token>> x = new Parser("abc def|ghi jkl|mno pqr").program().get(0);
+        assertEquals("abc", x.get(0).get(0).toString());
+        assertEquals("def", x.get(0).get(1).toString());
+        assertEquals("ghi", x.get(1).get(0).toString());
+        assertEquals("jkl", x.get(1).get(1).toString());
+        assertEquals("mno", x.get(2).get(0).toString());
+        assertEquals("pqr", x.get(2).get(1).toString());
     }
 
     public void testSimpleValue()
     {
-        List<CharSequence> x = new Parser(
-            "abc def.ghi http://www.osgi.org?abc=&x=1 [1,2,3] {{{{{{{xyz}}}}}}} (immediate) {'{{{{{'} {\\}} 'abc{}'").statement();
-        assertEquals("abc", x.get(0));
-        assertEquals("def.ghi", x.get(1));
-        assertEquals("http://www.osgi.org?abc=&x=1", x.get(2));
-        assertEquals("[1,2,3]", x.get(3));
-        assertEquals("{{{{{{{xyz}}}}}}}", x.get(4));
-        assertEquals("(immediate)", x.get(5));
-        assertEquals("{'{{{{{'}", x.get(6));
-        assertEquals("{\\}}", x.get(7));
-        assertEquals("'abc{}'", x.get(8));
+        List<Token> x = new Parser(
+            "abc def.ghi http://www.osgi.org?abc=&x=1 [1,2,3] {{{{{{{xyz}}}}}}} (immediate) {'{{{{{'} {\\{} 'abc{}'")
+            .program().get(0).get(0);
+        assertEquals("abc", x.get(0).toString());
+        assertEquals("def.ghi", x.get(1).toString());
+        assertEquals("http://www.osgi.org?abc=&x=1", x.get(2).toString());
+        assertEquals("1,2,3", x.get(3).toString());
+        assertEquals("{{{{{{xyz}}}}}}", x.get(4).toString());
+        assertEquals("immediate", x.get(5).toString());
+        assertEquals("'{{{{{'", x.get(6).toString());
+        assertEquals("\\{", x.get(7).toString());
+        assertEquals("'abc{}'", x.get(8).toString());
     }
 
     void each(CommandSession session, Collection<Object> list, Function closure)
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser2.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser2.java
new file mode 100644
index 0000000..175a8fd
--- /dev/null
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestParser2.java
@@ -0,0 +1,78 @@
+/*
+ * 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.runtime.shell;
+
+import java.io.EOFException;
+
+import junit.framework.TestCase;
+
+/*
+ * Test features of the new parser/tokenizer, many of which are not supported
+ * by the original parser.
+ */
+public class TestParser2 extends TestCase
+{
+    public void testComment() throws Exception
+    {
+        Context c = new Context();
+        c.addCommand("echo", this);
+
+        assertEquals("file://wibble#tag", c.execute("echo file://wibble#tag"));
+        assertEquals("file:", c.execute("echo file: //wibble#tag"));
+        
+        assertEquals("PWD/*.java", c.execute("echo PWD/*.java"));
+        try
+        {
+            c.execute("echo PWD /*.java");
+            fail("expected EOFException");
+        }
+        catch (EOFException e)
+        {
+            // expected
+        }
+        
+        assertEquals("ok", c.execute("// can't quote\necho ok\n"));
+        
+        // quote in comment in closure
+        assertEquals("ok", c.execute("x = { // can't quote\necho ok\n}; x"));
+        assertEquals("ok", c.execute("x = {\n// can't quote\necho ok\n}; x"));
+        assertEquals("ok", c.execute("x = {// can't quote\necho ok\n}; x"));
+    }
+
+    public CharSequence echo(Object args[])
+    {
+        if (args == null)
+        {
+            return "";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (Object arg : args)
+        {
+            if (arg != null)
+            {
+                if (sb.length() > 0)
+                    sb.append(' ');
+                sb.append(arg);
+            }
+        }
+        return sb.toString();
+    }
+
+}
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestTokenizer.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestTokenizer.java
new file mode 100644
index 0000000..714a45c
--- /dev/null
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/shell/TestTokenizer.java
@@ -0,0 +1,277 @@
+/*
+ * 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.runtime.shell;
+
+import java.io.File;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.naming.OperationNotSupportedException;
+
+import org.apache.felix.gogo.runtime.shell.Tokenizer.Type;
+
+import junit.framework.TestCase;
+
+public class TestTokenizer extends TestCase
+{
+    private final Map<String, Object> vars = new HashMap<String, Object>();
+    private final Evaluate evaluate;
+
+    public TestTokenizer()
+    {
+        evaluate = new Evaluate()
+        {
+            public Object eval(Token t) throws Exception
+            {
+                throw new OperationNotSupportedException();
+            }
+
+            public Object get(String key)
+            {
+                return vars.get(key);
+            }
+
+            public Object put(String key, Object value)
+            {
+                return vars.put(key, value);
+            }
+        };
+    }
+
+    public void testHello() throws Exception
+    {
+        testHello("hello world\n");
+        testHello("hello world\n\n"); // multiple \n reduced to single Token.NL
+        testHello("hello world\r\n"); // \r\n -> \n
+
+        // escapes
+
+        testHello("hello \\\nworld\n");
+        try
+        {
+            testHello("hello\\u20world\n");
+            fail("bad unicode accepted");
+        }
+        catch (SyntaxError e)
+        {
+            // expected
+        }
+
+        // whitespace and comments
+
+        testHello(" hello  world    \n ");
+        testHello("hello world // comment\n\n");
+        testHello("hello world #\\ comment\n\n");
+        testHello("// comment\nhello world\n");
+        testHello("// comment ?\\ \nhello world\n");
+        testHello("hello /*\n * comment\n */ world\n");
+    }
+
+    // hello world
+    private void testHello(CharSequence text) throws Exception
+    {
+        Tokenizer t = new Tokenizer(text);
+        assertEquals(Type.WORD, t.next());
+        assertEquals("hello", t.value().toString());
+        assertEquals(Type.WORD, t.next());
+        assertEquals("world", t.value().toString());
+        assertEquals(Type.NEWLINE, t.next());
+        assertEquals(Type.EOT, t.next());
+    }
+
+    public void testString() throws Exception
+    {
+        testString("'single $quote' \"double $quote\"\n");
+    }
+
+    // 'single quote' "double quote"
+    private void testString(CharSequence text) throws Exception
+    {
+        Tokenizer t = new Tokenizer(text);
+        assertEquals(Type.WORD, t.next());
+        assertEquals("'single $quote'", t.value().toString());
+        assertEquals(Type.WORD, t.next());
+        assertEquals("\"double $quote\"", t.value().toString());
+        assertEquals(Type.NEWLINE, t.next());
+        assertEquals(Type.EOT, t.next());
+    }
+
+    public void testClosure() throws Exception
+    {
+        testClosure2("x = { echo '}' $args //comment's\n}\n");
+        testClosure2("x={ echo '}' $args //comment's\n}\n");
+        assertEquals(Type.CLOSURE, token1("{ echo \\{ $args \n}"));
+        assertEquals(Type.CLOSURE, token1("{ echo \\} $args \n}"));
+    }
+
+    /*
+     * x = {echo $args};
+     */
+    private void testClosure2(CharSequence text) throws Exception
+    {
+        Tokenizer t = new Tokenizer(text);
+        assertEquals(Type.WORD, t.next());
+        assertEquals("x", t.value().toString());
+        assertEquals(Type.ASSIGN, t.next());
+        assertEquals(Type.CLOSURE, t.next());
+        assertEquals(" echo '}' $args //comment's\n", t.value().toString());
+        assertEquals(Type.NEWLINE, t.next());
+        assertEquals(Type.EOT, t.next());
+    }
+
+    private Type token1(CharSequence text) throws Exception
+    {
+        Tokenizer t = new Tokenizer(text);
+        Type type = t.next();
+        assertEquals(Type.EOT, t.next());
+        return type;
+    }
+
+    public void testExpand() throws Exception
+    {
+        final URI home = new URI("/home/derek");
+        final File pwd = new File("/tmp");
+        final String user = "derek";
+
+        vars.clear();
+        vars.put("HOME", home);
+        vars.put("PWD", pwd);
+        vars.put("USER", user);
+        vars.put(user, "Derek Baum");
+
+        // quote removal
+        assertEquals("hello", expand("hello"));
+        assertEquals("hello", expand("'hello'"));
+        assertEquals("\"hello\"", expand("'\"hello\"'"));
+        assertEquals("hello", expand("\"hello\""));
+        assertEquals("'hello'", expand("\"'hello'\""));
+
+        // escapes
+        assertEquals("hello\\w", expand("hello\\\\w"));
+        assertEquals("hellow", expand("hello\\w"));
+        assertEquals("hello\\w", expand("\"hello\\\\w\""));
+        assertEquals("hello\\w", expand("\"hello\\w\""));
+        assertEquals("hello\\\\w", expand("'hello\\\\w'"));
+        assertEquals("hello", expand("he\\\nllo"));
+        assertEquals("he\\llo", expand("'he\\llo'"));
+        assertEquals("he'llo", expand("'he'\\''llo'"));
+        assertEquals("he\"llo", expand("\"he\\\"llo\""));
+        assertEquals("he'llo", expand("he\\'llo"));
+        assertEquals("he$llo", expand("\"he\\$llo\""));
+        assertEquals("he\\'llo", expand("\"he\\'llo\""));
+        assertEquals("hello\\w", expand("\"hello\\w\""));
+
+        // unicode
+
+        // Note: we could use literal Unicode pound '£' instead of \u00a3 in next test.
+        // if above is not UK currency symbol, then your locale is not configured for UTF-8.
+        // Java on Macs cannot handle UTF-8 unless you explicitly set '-Dfile.encoding=UTF-8'.
+        assertEquals("pound\u00a3cent\u00a2", expand("pound\\u00a3cent\\u00a2"));
+        assertEquals("euro\\u20ac", expand("'euro\\u20ac'"));
+        try
+        {
+            expand("eot\\u20a");
+            fail("EOT in unicode");
+        }
+        catch (SyntaxError e)
+        {
+            // expected
+        }
+        try
+        {
+            expand("bad\\u20ag");
+            fail("bad unicode");
+        }
+        catch (SyntaxError e)
+        {
+            // expected
+        }
+
+        // simple variable expansion - quoting or concatenation converts result to String
+        assertEquals(user, expand("$USER"));
+        assertEquals(home, expand("$HOME"));
+        assertEquals(home.toString(), expand("$HOME$W"));
+        assertEquals(pwd, expand("$PWD"));
+        assertEquals("$PWD", expand("'$PWD'"));
+        assertEquals("$PWD", expand("\\$PWD"));
+        assertEquals(pwd.toString(), expand("\"$PWD\""));
+        assertEquals("W" + pwd, expand("W$PWD"));
+        assertEquals(pwd + user, expand("$PWD$USER"));
+
+        // variable substitution  ${NAME:-WORD} etc
+        assertNull(expand("$JAVA_HOME"));
+        assertEquals(user, expand("${USER}"));
+        assertEquals(user + "W", expand("${USER}W"));
+        assertEquals("java_home", expand("${JAVA_HOME:-java_home}"));
+        assertEquals(pwd, expand("${NOTSET:-$PWD}"));
+        assertNull(vars.get("JAVA_HOME"));
+        assertEquals("java_home", expand("${JAVA_HOME:=java_home}"));
+        assertEquals("java_home", vars.get("JAVA_HOME"));
+        assertEquals("java_home", expand("$JAVA_HOME"));
+        assertEquals("yes", expand("${JAVA_HOME:+yes}"));
+        assertNull(expand("${NOTSET:+yes}"));
+        assertEquals("", expand("\"${NOTSET:+yes}\""));
+        try
+        {
+            expand("${NOTSET:?}");
+            fail("expected 'not set' exception");
+        }
+        catch (IllegalArgumentException e)
+        {
+            // expected
+        }
+
+        // bad variable names
+        assertEquals("$ W", expand("$ W"));
+        assertEquals("$ {W}", expand("$ {W}"));
+        try
+        {
+            expand("${W }");
+            fail("expected syntax error");
+        }
+        catch (SyntaxError e)
+        {
+            // expected
+        }
+
+        assertEquals(user, expand("${USER\\\n:?}"));
+        assertEquals(user, expand("${US\\u0045R:?}"));
+
+        // bash doesn't supported nested expansions
+        // gogo only supports them in the ${} syntax
+        assertEquals("Derek Baum", expand("${$USER}"));
+        assertEquals("x", expand("${$USR:-x}"));
+        assertEquals("$" + user, expand("$$USER"));
+    }
+
+    private Object expand(CharSequence word) throws Exception
+    {
+        return Tokenizer.expand(word, evaluate);
+    }
+
+    public void testParser() throws Exception
+    {
+        new Parser("// comment\n" + "a=\"who's there?\"; ps -ef;\n" + "ls | \n grep y\n").program();
+        String p1 = "a=1 \\$b=2 c={closure}\n";
+        new Parser(p1).program();
+        new Parser(new Token(Type.ARRAY, p1, (short) 0, (short) 0)).program();
+    }
+
+}