Improve parameter expansion

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1736016 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/ArgList.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/ArgList.java
new file mode 100644
index 0000000..f61717c
--- /dev/null
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/ArgList.java
@@ -0,0 +1,69 @@
+/*
+ * 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;
+
+import java.util.AbstractList;
+import java.util.List;
+
+/**
+ * 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]
+ */
+public 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/Closure.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java
index 2b121cc..b4f0e5a 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Closure.java
@@ -23,7 +23,6 @@
 import java.io.PipedOutputStream;
 import java.nio.channels.Channel;
 import java.nio.channels.Channels;
-import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -392,9 +391,9 @@
 //                return v;
 //            }
 
-            if (parms == v && parms != null)
+            if (v instanceof ArgList)
             {
-                values.addAll(parms); // explode $args array
+                values.addAll((ArgList) v); // explode $args array
             }
             else
             {
@@ -672,51 +671,4 @@
             "([^\\\\{}(\\[])[\\s\n]*\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/CommandProcessorImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java
index 3243429..e12a2d0 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandProcessorImpl.java
@@ -134,7 +134,7 @@
         return Collections.unmodifiableSet(commands.keySet());
     }
 
-    Function getCommand(String name, final Object path)
+    protected Function getCommand(String name, final Object path)
     {
         int colon = name.indexOf(':');
 
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java
index 08a233f..b248562 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Expander.java
@@ -18,6 +18,15 @@
  */
 package org.apache.felix.gogo.runtime;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Collectors;
+
 @SuppressWarnings("fallthrough")
 public class Expander extends BaseTokenizer
 {
@@ -50,6 +59,7 @@
         return expand(word, evaluate, inQuote);
     }
 
+
     private Object expand() throws Exception
     {
         final String special = "%$\\\"'";
@@ -112,17 +122,34 @@
                 case '"':
                     skipQuote();
                     value = text.subSequence(start, index - 1);
+                    getch();
                     Object expand = expand(value, evaluate, true);
-                    if (eot() && buf.length() == 0 && value == expand)
+                    if (eot() && buf.length() == 0)
                     {
-                        // FELIX-2468 avoid returning CharSequence implementation
-                        return value.toString();
+                        if (expand instanceof ArgList) {
+                            return new ArgList((ArgList) expand).stream().map(String::valueOf).collect(Collectors.toList());
+                        } else if (expand instanceof Collection) {
+                            return ((Collection) expand).stream().map(String::valueOf).collect(Collectors.joining(" "));
+                        } else if (expand != null) {
+                            return expand.toString();
+                        } else {
+                            return "";
+                        }
                     }
-                    if (null != expand)
-                    {
+                    if (expand instanceof Collection) {
+                        boolean first = true;
+                        for (Object o : ((Collection) expand)) {
+                            if (!first) {
+                                buf.append(" ");
+                            }
+                            first = false;
+                            buf.append(o);
+                        }
+                    }
+                    else if (expand != null) {
                         buf.append(expand.toString());
                     }
-                    break;
+                    continue; // has already read next char
 
                 case '\'':
                     if (!inQuote)
@@ -240,7 +267,11 @@
     private Object expandVar() throws Exception
     {
         assert '$' == ch;
-        Object val;
+
+        Object val = null;
+
+        short sLine = line;
+        short sCol = column;
 
         if (getch() != '{')
         {
@@ -273,6 +304,455 @@
         }
         else
         {
+            getch();
+
+            boolean flagk = false;
+            boolean flagv = false;
+            boolean flagP = false;
+            boolean flagC = false;
+            boolean flagL = false;
+            boolean flagU = false;
+            boolean flagExpand = false;
+            if (ch == '(') {
+                getch();
+                while (ch != EOT && ch != ')') {
+                    switch (ch) {
+                        case 'P':
+                            flagP = true;
+                            break;
+                        case '@':
+                            flagExpand = true;
+                            break;
+                        case 'k':
+                            flagk = true;
+                            break;
+                        case 'v':
+                            flagv = true;
+                            break;
+                        case 'C':
+                            flagC = true;
+                            flagL = false;
+                            flagU = false;
+                            break;
+                        case 'L':
+                            flagC = false;
+                            flagL = true;
+                            flagU = false;
+                            break;
+                        case 'U':
+                            flagC = false;
+                            flagL = false;
+                            flagU = true;
+                            break;
+                        default:
+                            throw new SyntaxError(line, column, "unsupported flag: " + ch);
+                    }
+                    getch();
+                }
+                getch();
+            }
+
+            if (ch == '+') {
+                getch();
+                val = getAndEvaluateName();
+            }
+            else {
+                boolean computeLength = false;
+                boolean wordSplit = false;
+                if (ch == '#') {
+                    computeLength = true;
+                    getch();
+                }
+                if (ch == '=') {
+                    wordSplit = true;
+                    getch();
+                }
+
+                Object val1 = getName('}');
+
+                if (ch == '}' || ch == '[') {
+                    val = val1 instanceof Token ? evaluate.get(expand((Token) val1).toString()) : val1;
+                }
+                else {
+                    int start = index - 1;
+                    while (ch != EOT && ch != '}' && ":-+=?#%/^|*?".indexOf(ch) >= 0) {
+                        getch();
+                    }
+                    Token op = text.subSequence(start, index - 1);
+                    if (Token.eq("-", op) || Token.eq(":-", op)) {
+                        val1 = val1 instanceof Token ? evaluate.get(expand((Token) val1).toString()) : val1;
+                        Object val2 = getValue();
+                        val = val1 == null ? val2 : val1;
+                    }
+                    else if (Token.eq("+", op) || Token.eq(":+", op)) {
+                        val1 = val1 instanceof Token ? evaluate.get(expand((Token) val1).toString()) : val1;
+                        Object val2 = getValue();
+                        val = val1 != null ? val2 : null;
+                    }
+                    else if (Token.eq("=", op) || Token.eq(":=", op) || Token.eq("::=", op)) {
+                        if (!(val1 instanceof Token)) {
+                            throw new SyntaxError(line, column, "not an identifier");
+                        }
+                        String name = expand((Token) val1).toString();
+                        val1 = evaluate.get(name);
+                        val = getValue();
+                        if (Token.eq("::=", op) || val1 == null) {
+                            evaluate.put(name, val);
+                        }
+                    }
+                    else if (Token.eq("?", op) || Token.eq(":?", op)) {
+                        String name;
+                        if (val1 instanceof Token) {
+                            name = expand((Token) val1).toString();
+                            val = evaluate.get(name);
+                        } else {
+                            name = "";
+                            val = val1;
+                        }
+                        if (val == null || val.toString().length() == 0) {
+                            throw new IllegalArgumentException(name + ": parameter not set");
+                        }
+                    }
+                    else if (Token.eq("#", op) || Token.eq("##", op) || Token.eq("%", op) || Token.eq("%%", op)) {
+                        val1 = val1 instanceof Token ? evaluate.get(expand((Token) val1).toString()) : val1;
+                        Object val2 = getValue();
+                        if (val2 != null) {
+                            String p = toRegexPattern(val2.toString(), op.length() == 1);
+                            String m = op.charAt(0) == '#' ? "^" + p : p + "$";
+                            if (val1 instanceof Map) {
+                                val1 = toList((Map) val1, flagk, flagv);
+                            }
+                            if (val1 instanceof Collection) {
+                                List<String> l = new ArrayList<>();
+                                for (Object o : ((Collection) val1)) {
+                                    l.add(o.toString().replaceFirst(m, ""));
+                                }
+                                val = l;
+                            } else if (val1 != null) {
+                                val = val1.toString().replaceFirst(m, "");
+                            }
+                        } else {
+                            val = val1;
+                        }
+                    }
+                }
+                if (computeLength) {
+                    if (val instanceof Collection) {
+                        val = ((Collection) val).size();
+                    }
+                    else if (val instanceof Map) {
+                        val = ((Map) val).size();
+                    }
+                    else if (val != null) {
+                        val = val.toString().length();
+                    }
+                    else {
+                        val = 0;
+                    }
+                }
+                if (wordSplit) {
+                    if (val instanceof Map) {
+                        List<Object> c = toList((Map) val, flagk, flagv);
+                        val = new ArgList(c);
+                    }
+                    else if (val instanceof Collection) {
+                        if (!(val instanceof ArgList)) {
+                            List<Object> l = val instanceof List ? (List) val : new ArrayList<>((Collection<?>) val);
+                            val = new ArgList(l);
+                        }
+                    }
+                    else if (val != null) {
+                        val = new ArgList(Arrays.asList(val.toString().split("\\s")));
+                    }
+                }
+            }
+
+            while (ch == '[') {
+//                Token leftParam;
+                Object left;
+//                Token rightParam;
+                Object right;
+                getch();
+//                if (ch == '(') {
+//                    int start = index;
+//                    findClosing();
+//                    leftParam = text.subSequence(start, index - 1);
+//                    getch();
+//                } else {
+//                    leftParam = null;
+//                }
+                if (ch == '@') {
+                    left = text.subSequence(index - 1, index);
+                    getch();
+                } else {
+                    left = getName(']');
+                }
+                if (ch == ',') {
+                    getch();
+//                    if (ch == '(') {
+//                        int start = index;
+//                        findClosing();
+//                        rightParam = text.subSequence(start, index - 1);
+//                        getch();
+//                    } else {
+//                        rightParam = null;
+//                    }
+                    right = getName(']');
+                } else {
+//                    rightParam = null;
+                    right = null;
+                }
+                if (ch != ']') {
+                    throw new SyntaxError(line, column, "invalid subscript");
+                }
+                getch();
+                if (right == null) {
+                    left = left instanceof Token ? expand((Token) left) : left;
+                    if (val instanceof Map) {
+                        if (left.toString().equals("@")) {
+                            val = new ArgList(toList((Map) val, flagk, flagv));
+                        }
+                        else {
+                            val = ((Map) val).get(left.toString());
+                        }
+                    }
+                    else if (val instanceof List) {
+                        if (left.toString().equals("@")) {
+                            val = new ArgList((List) val);
+                        }
+                        else {
+                            val = ((List) val).get(Integer.parseInt(left.toString()));
+                        }
+                    }
+                    else {
+                        if (left.toString().equals("@")) {
+                            val = val.toString();
+                        } else {
+                            val = val.toString().charAt(Integer.parseInt(left.toString()));
+                        }
+                    }
+                }
+                else {
+                    left = left instanceof Token ? expand((Token) left) : left;
+                    right = right instanceof Token ? expand((Token) right) : right;
+                    if (val instanceof Map) {
+                        val = null;
+                    }
+                    else if (val instanceof List) {
+                        val = ((List) val).subList(Integer.parseInt(left.toString()), Integer.parseInt(right.toString()));
+                    }
+                    else {
+                        val = val.toString().substring(Integer.parseInt(left.toString()), Integer.parseInt(right.toString()));
+                    }
+                }
+            }
+
+            if (ch != '}') {
+                throw new SyntaxError(sLine, sCol, "bad substitution");
+            }
+
+            if (flagP) {
+                val = val != null ? evaluate.get(val.toString()) : null;
+            }
+            if (flagC || flagL || flagU) {
+                Function<String, String> cnv;
+                if (flagC)
+                    cnv = s -> s.isEmpty() ? s : s.substring(0, 1).toUpperCase() + s.substring(1);
+                else if (flagL)
+                    cnv = String::toLowerCase;
+                else
+                    cnv = String::toUpperCase;
+                if (val instanceof Map) {
+                    val = toList((Map) val, flagk, flagv);
+                }
+                if (val instanceof Collection) {
+                    List<String> list = new ArrayList<>();
+                    for (Object o : ((Collection) val)) {
+                        list.add(o != null ? cnv.apply(o.toString()) : null);
+                    }
+                    val = list;
+                } else if (val != null) {
+                    val = cnv.apply(val.toString());
+                }
+            }
+
+            if (inQuote) {
+                if (val instanceof Map) {
+                    val = toList((Map) val, flagk, flagv);
+                }
+                if (val instanceof Collection) {
+                    List<Object> l = val instanceof List ? (List) val : new ArrayList<>((Collection) val);
+                    if (flagExpand) {
+                        val = new ArgList(l);
+                    } else {
+                        val = l;
+                    }
+                }
+            }
+            else {
+                if (flagExpand && val instanceof List) {
+                    val = new ArgList((List) val);
+                }
+            }
+
+            getch();
+
+            /*
+            Token pre;
+            if ("#^~=+".indexOf(ch) >= 0) {
+                pre = text.subSequence(index, index + 1);
+                getch();
+            } else {
+                pre = null;
+            }
+
+            Token name1;
+            Object val1;
+            if (ch == '$') {
+                name1 = null;
+                val1 = expandVar();
+            } else {
+                int start = index - 1;
+                while (ch != EOT && ch != '}' && (isName(ch) || ch == '\\')) {
+                    getch();
+                    if (ch == '{' || ch == '(' || ch == '[') {
+                        findClosing();
+                    }
+                }
+                if (ch == EOT) {
+                    throw new EOFError(sLine, sCol, "unexpected EOT looking for matching '}'", "compound", Character.toString('}'));
+                }
+                name1 = text.subSequence(start, index - 1);
+                val1 = null;
+            }
+
+            Token op;
+            if (ch != '}') {
+                int start = index - 1;
+                while (ch != EOT && ch != '}' && ":-+=?#%/^|*?".indexOf(ch) >= 0) {
+                    getch();
+                }
+                op = text.subSequence(start, index - 1);
+            } else {
+                op = null;
+            }
+
+            Token name2;
+            Object val2;
+            if (ch == '}') {
+                name2 = null;
+                val2 = null;
+            }
+            else if (ch == '$') {
+                name2 = null;
+                val2 = expandVar();
+            }
+            else {
+                int start = index - 1;
+                while (ch != EOT && ch != '}') {
+                    getch();
+                    if (ch == '\\') {
+                        escape();
+                    }
+                    else if (ch == '{' || ch == '(' || ch == '[') {
+                        findClosing();
+                    }
+                }
+                if (ch == EOT) {
+                    throw new EOFError(sLine, sCol, "unexpected EOT looking for matching '}'", "compound", Character.toString('}'));
+                }
+                name2 = text.subSequence(start, index - 1);
+                val2 = null;
+            }
+
+            if (ch != '}') {
+                throw new SyntaxError(sLine, sCol, "bad substitution");
+            }
+
+            if (pre == null && op == null) {
+                if (name1 != null) {
+                    val1 = evaluate.get(expand(name1).toString());
+                }
+            }
+            else if (pre != null && Token.eq(pre, "+") && op == null) {
+                if (name1 != null) {
+                    val1 = evaluate.get(expand(name1).toString()) != null;
+                } else {
+                    throw new SyntaxError(sLine, sCol, "bad substitution");
+                }
+            }
+            else if (pre != null) {
+                throw new SyntaxError(sLine, sCol, "bad substitution");
+            }
+            else if (Token.eq("-", op) || Token.eq(":-", op)) {
+                if (name1 != null) {
+                    val1 = evaluate.get(expand(name1).toString());
+                }
+                if (val1 == null) {
+                    if (name2 != null) {
+                        val2 = expand(expand(name2).toString(), evaluate, inQuote);
+                    }
+                    val1 = val2;
+                }
+                if (val1 instanceof Token) {
+                    val1 = val1.toString();
+                }
+            }
+            else if (Token.eq("=", op) || Token.eq(":=", op) || Token.eq("::=", op)) {
+                assert name1 != null;
+                val1 = evaluate.get(expand(name1).toString());
+                if (val1 == null && name2 != null) {
+                    val1 = expand(name2);
+                    if (val1 instanceof Token) {
+                        val1 = val1.toString();
+                    }
+                    evaluate.put(name1.toString(), val1);
+                }
+            }
+            else if (Token.eq("+", op) || Token.eq(":+", op)) {
+                assert name1 != null;
+                val1 = evaluate.get(expand(name1).toString());
+                if (val1 != null && name2 != null) {
+                    val1 = expand(name2);
+                    if (val1 instanceof Token) {
+                        val1 = val1.toString();
+                    }
+                }
+            }
+            else if (Token.eq("?", op) || Token.eq(":?", op)) {
+                assert name1 != null;
+                val1 = evaluate.get(expand(name1).toString());
+                if (val1 == null) {
+                    if (name2 != null) {
+                        val1 = expand(name2);
+                        if (val1 instanceof Token) {
+                            val1 = val1.toString();
+                        }
+                    }
+                    if (val1 == null || val1.toString().length() == 0) {
+                        val1 = "parameter not set";
+                    }
+                    throw new IllegalArgumentException(name1 + ": " + val1);
+                }
+            }
+            else {
+
+            }
+            val = val1;
+            if (flagP) {
+                val = val != null ? evaluate.get(val.toString()) : null;
+            }
+
+            /*
+            while (true) {
+                if (ch == EOT) {
+                    throw new EOFError(sLine, sCol, "unexpected EOT looking for matching '}'", "compound", Character.toString('}'));
+                }
+                if (ch == '\\') {
+                    escape();
+                }
+                if (ch == '{')
+            }
             // ${NAME[[:]-+=?]WORD}
             short sLine = line;
             short sCol = column;
@@ -289,6 +769,7 @@
                     case '+':
                     case '=':
                     case '?':
+                    case '#':
                         break;
 
                     default:
@@ -367,16 +848,230 @@
                         }
                         break;
 
+                    case '#':
+                        if (null != val)
+                        {
+
+                        }
+                        break;
+
                     default:
                         throw new SyntaxError(sLine, sCol, "bad substitution: ${" + group + "}");
                 }
             }
             getch();
+            */
         }
 
         return val;
     }
 
+    private List<Object> toList(Map<Object, Object> val1, boolean flagk, boolean flagv) {
+        List<Object> l = new ArrayList<>();
+        if (flagk && flagv) {
+            for (Map.Entry<Object, Object> entry : val1.entrySet()) {
+                l.add(entry.getKey());
+                l.add(entry.getValue());
+            }
+        } else if (flagk) {
+            l.addAll(val1.keySet());
+        } else {
+            l.addAll(val1.values());
+        }
+        return l;
+    }
+
+    private Object getAndEvaluateName() throws Exception {
+        Object r = getName('}');
+        if (r instanceof Token) {
+            return evaluate.get(expand((Token) r).toString());
+        } else {
+            return r;
+        }
+    }
+
+    private Object getName(char closing) throws Exception {
+        if (ch == '$') {
+            return expandVar();
+        } else {
+            int start = index - 1;
+            while (ch != EOT && ch != closing && isName(ch)) {
+                getch();
+                if (ch == '\\') {
+                    escape();
+                }
+                else if (ch == '{') {
+                    findClosing();
+                }
+            }
+            if (ch == EOT) {
+                throw new EOFError(line, column, "unexpected EOT looking for matching '}'", "compound", Character.toString('}'));
+            }
+            return text.subSequence(start, index - 1);
+        }
+    }
+
+    private Object getValue() throws Exception {
+        if (ch == '$') {
+            return expandVar();
+        } else {
+            int start = index - 1;
+            while (ch != EOT && ch != '}') {
+                if (ch == '\\') {
+                    escape();
+                }
+                else if (ch == '{' || ch == '(' || ch == '[') {
+                    findClosing();
+                }
+                else {
+                    getch();
+                }
+            }
+            if (ch == EOT) {
+                throw new EOFError(line, column, "unexpected EOT looking for matching '}'", "compound", Character.toString('}'));
+            }
+            Token name = text.subSequence(start, index - 1);
+            return expand(name).toString();
+        }
+    }
+
+    private void findClosing() {
+        char start = ch;
+        while (getch() != EOT) {
+            if (ch == '(' || ch == '{' || ch == '[') {
+                findClosing();
+            } else if (start == '(' && ch == ')'
+                    || start == '{' && ch == '}'
+                    || start == '[' && ch == ']') {
+                return;
+            }
+        }
+    }
+
+    private static final char EOL = 0;
+
+    private static boolean isRegexMeta(char ch) {
+        return ".^$+{[]|()".indexOf(ch) != -1;
+    }
+
+    private static boolean isGlobMeta(char ch) {
+        return "\\*?[{".indexOf(ch) != -1;
+    }
+
+    private static char next(String str, int index) {
+        return index < str.length() ? str.charAt(index) : EOL;
+    }
+
+    private static String toRegexPattern(String str, boolean shortest) {
+        boolean inGroup = false;
+        StringBuilder sb = new StringBuilder();
+        int index = 0;
+        while (true) {
+            while (index < str.length()) {
+                char ch = str.charAt(index++);
+                switch (ch) {
+                    case '*':
+                        sb.append(shortest ? ".*?" : ".*");
+                        break;
+                    case ',':
+                        if (inGroup) {
+                            sb.append(")|(?:");
+                        } else {
+                            sb.append(',');
+                        }
+                        break;
+                    case '?':
+                        sb.append(".");
+                        break;
+                    case '[':
+                        sb.append("[");
+                        if (next(str, index) == '^') {
+                            sb.append("\\^");
+                            ++index;
+                        } else {
+                            if (next(str, index) == '!') {
+                                sb.append('^');
+                                ++index;
+                            }
+                            if (next(str, index) == '-') {
+                                sb.append('-');
+                                ++index;
+                            }
+                        }
+                        boolean inLeft = false;
+                        char left = 0;
+                        while (index < str.length()) {
+                            ch = str.charAt(index++);
+                            if (ch == ']') {
+                                break;
+                            }
+                            if (ch == '\\' || ch == '[' || ch == '&' && next(str, index) == '&') {
+                                sb.append('\\');
+                            }
+                            sb.append(ch);
+                            if (ch == '-') {
+                                if (!inLeft) {
+                                    throw new PatternSyntaxException("Invalid range", str, index - 1);
+                                }
+                                if ((ch = next(str, index++)) == EOL || ch == ']') {
+                                    break;
+                                }
+                                if (ch < left) {
+                                    throw new PatternSyntaxException("Invalid range", str, index - 3);
+                                }
+                                sb.append(ch);
+                                inLeft = false;
+                            } else {
+                                inLeft = true;
+                                left = ch;
+                            }
+                        }
+                        if (ch != ']') {
+                            throw new PatternSyntaxException("Missing \']", str, index - 1);
+                        }
+                        sb.append("]");
+                        break;
+                    case '\\':
+                        if (index == str.length()) {
+                            throw new PatternSyntaxException("No character to escape", str, index - 1);
+                        }
+                        char ch2 = str.charAt(index++);
+                        if (isGlobMeta(ch2) || isRegexMeta(ch2)) {
+                            sb.append('\\');
+                        }
+                        sb.append(ch2);
+                        break;
+                    case '{':
+                        if (inGroup) {
+                            throw new PatternSyntaxException("Cannot nest groups", str, index - 1);
+                        }
+                        sb.append("(?:(?:");
+                        inGroup = true;
+                        break;
+                    case '}':
+                        if (inGroup) {
+                            sb.append("))");
+                            inGroup = false;
+                        } else {
+                            sb.append('}');
+                        }
+                        break;
+                    default:
+                        if (isRegexMeta(ch)) {
+                            sb.append('\\');
+                        }
+                        sb.append(ch);
+                        break;
+                }
+            }
+            if (inGroup) {
+                throw new PatternSyntaxException("Missing \'}", str, index - 1);
+            }
+            return sb.toString();
+        }
+    }
+
+
     private boolean isName(char ch)
     {
         return Character.isJavaIdentifierPart(ch) && (ch != '$') || ('.' == ch);
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Pipe.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Pipe.java
index a11f45f..492a198 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Pipe.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Pipe.java
@@ -288,7 +288,19 @@
 
             Pipe previous = setCurrentPipe(this);
             try {
-                Object result = closure.execute(statement);
+                Object result;
+                // Very special case for empty statements with redirection
+                if (statement.tokens().isEmpty() && toclose[0]) {
+                    ByteBuffer bb = ByteBuffer.allocate(1024);
+                    while (((ReadableByteChannel) streams[0]).read(bb) >= 0 || bb.position() != 0) {
+                        bb.flip();
+                        ((WritableByteChannel) streams[1]).write(bb);
+                        bb.compact();
+                    }
+                    result = null;
+                } else {
+                    result = closure.execute(statement);
+                }
                 // If an error has been set
                 if (error != 0) {
                     return new Result(error);
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Reflective.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Reflective.java
index 442505a..b619ee7 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Reflective.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Reflective.java
@@ -24,8 +24,10 @@
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import org.apache.felix.service.command.CommandSession;
@@ -392,7 +394,7 @@
             return converted;
         }
 
-        String string = arg.toString();
+        String string = toString(arg);
 
         if (type.isAssignableFrom(String.class))
         {
@@ -431,6 +433,70 @@
         return NO_MATCH;
     }
 
+    private static String toString(Object arg)
+    {
+        if (arg instanceof Map)
+        {
+            StringBuilder sb = new StringBuilder();
+            sb.append("[");
+            boolean first = true;
+            for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) arg).entrySet())
+            {
+                if (!first) {
+                    sb.append(" ");
+                }
+                first = false;
+                writeValue(sb, entry.getKey());
+                sb.append("=");
+                writeValue(sb, entry.getValue());
+            }
+            sb.append("]");
+            return sb.toString();
+        }
+        else if (arg instanceof Collection)
+        {
+            StringBuilder sb = new StringBuilder();
+            sb.append("[");
+            boolean first = true;
+            for (Object o : ((Collection) arg))
+            {
+                if (!first) {
+                    sb.append(" ");
+                }
+                first = false;
+                writeValue(sb, o);
+            }
+            sb.append("]");
+            return sb.toString();
+        }
+        else
+        {
+            return arg.toString();
+        }
+    }
+
+    private static void writeValue(StringBuilder sb, Object o) {
+        if (o == null || o instanceof Boolean || o instanceof Number)
+        {
+            sb.append(o);
+        }
+        else
+        {
+            String s = o.toString();
+            sb.append("\"");
+            for (int i = 0; i < s.length(); i++)
+            {
+                char c = s.charAt(i);
+                if (c == '\"' || c == '=')
+                {
+                    sb.append("\\");
+                }
+                sb.append(c);
+            }
+            sb.append("\"");
+        }
+    }
+
     private static Class<?> primitiveToObject(Class<?> type)
     {
         if (type == boolean.class)
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestTokenizer.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestTokenizer.java
index f7b818e..9a7cc03 100644
--- a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestTokenizer.java
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestTokenizer.java
@@ -22,7 +22,9 @@
 import java.io.File;
 import java.io.PrintStream;
 import java.net.URI;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl;
@@ -39,7 +41,7 @@
 
 public class TestTokenizer
 {
-    private final Map<String, Object> vars = new HashMap<String, Object>();
+    private final Map<String, Object> vars = new HashMap<>();
     private final Evaluate evaluate;
 
     public TestTokenizer()
@@ -163,6 +165,31 @@
     }
 
     @Test
+    public void testSubscripts() throws Exception
+    {
+        Map<String, String> map = new LinkedHashMap<>();
+        map.put("a1", "baz");
+        map.put("a2", "bar");
+        map.put("b1", "foo");
+
+        vars.clear();
+        vars.put("key", "a1");
+        vars.put("map", map);
+
+        assertEquals("baz", expand("${map[a1]}"));
+        assertEquals("baz", expand("${map[$key]}"));
+        assertEquals("az", expand("${map[a1][1,3]}"));
+        assertEquals("AZ", expand("${(U)map[a1][1,3]}"));
+        assertEquals(map, expand("${map}"));
+        assertEquals("baz bar foo", expand("\"${map}\""));
+        assertEquals(Arrays.asList("baz", "bar", "foo"), expand("\"${map[@]}\""));
+        assertEquals(Arrays.asList("a1", "a2", "b1"), expand("\"${(k)map[@]}\""));
+        assertEquals(Arrays.asList("a1", "baz", "a2", "bar", "b1", "foo"), expand("\"${(kv)map[@]}\""));
+        assertEquals(Arrays.asList("a2", "bar"), expand("${${(kv)map[@]}[2,4]}"));
+        assertEquals(Arrays.asList("a2", "bar"), expand("${${(kv)=map}[2,4]}"));
+    }
+
+    @Test
     public void testExpand() throws Exception
     {
         final URI home = new URI("/home/derek");
@@ -270,12 +297,22 @@
             // expected
         }
 
-        assertEquals(user, expand("${USER\\\n:?}"));
+        try {
+            expand("${USER\\\n:?}");
+        }
+        catch (SyntaxError e)
+        {
+            // expected
+        }
         assertEquals(user, expand("${US\\u0045R:?}"));
 
         // bash doesn't supported nested expansions
         // gogo only supports them in the ${} syntax
-        assertEquals("Derek Baum", expand("${$USER}"));
+        assertEquals("Derek Baum", expand("${(P)$USER}"));
+        assertEquals("Derek Baum", expand("${${(P)USER}:-Derek Baum}"));
+        assertEquals("Derek Baum", expand("${${(P)USR}:-$derek}"));
+        assertEquals("derek", expand("${${USER}}"));
+        assertEquals("derek", expand("${${USER:-d}}"));
         assertEquals("x", expand("${$USR:-x}"));
         assertEquals("$" + user, expand("$$USER"));
     }