Add support for brace expansion and fix pattern support

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1736032 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/AbstractParserTest.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/AbstractParserTest.java
index 0b04ab3..17cd11d 100644
--- a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/AbstractParserTest.java
+++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/AbstractParserTest.java
@@ -26,17 +26,18 @@
 
 import junit.framework.TestCase;
 import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl;
+import org.junit.After;
+import org.junit.Before;
 
-public abstract class AbstractParserTest extends TestCase {
+public abstract class AbstractParserTest {
 
     private ThreadIOImpl threadIO;
     private InputStream sin;
     private PrintStream sout;
     private PrintStream serr;
 
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
+    @Before
+    public void setUp() throws Exception {
         sin = new NoCloseInputStream(System.in);
         sout = new NoClosePrintStream(System.out);
         serr = new NoClosePrintStream(System.err);
@@ -44,10 +45,9 @@
         threadIO.start();
     }
 
-    @Override
-    protected void tearDown() throws Exception {
+    @After
+    public void tearDown() throws Exception {
         threadIO.stop();
-        super.tearDown();
     }
 
     public class Context extends org.apache.felix.gogo.jline.Context {
diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ShellTest.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ShellTest.java
index 5ae57d4..8d6e27c 100644
--- a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ShellTest.java
+++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/ShellTest.java
@@ -20,18 +20,18 @@
 
 import java.util.Arrays;
 
+import org.junit.Assert;
 import org.junit.Test;
 
 public class ShellTest extends AbstractParserTest {
 
-
     @Test
     public void testAssignmentWithEcho() throws Exception {
         Context context = new Context();
         context.execute("a = \"foo\"");
-        assertEquals("foo", context.get("a"));
+        Assert.assertEquals("foo", context.get("a"));
         context.execute("a = $(echo bar)");
-        assertEquals("bar", context.get("a"));
+        Assert.assertEquals("bar", context.get("a"));
     }
 
     @Test
@@ -39,8 +39,9 @@
         Context context = new Context();
         // TODO: not than in zsh, the same thing is achieved using
         // TODO:     ${${${(@f)"$(jobs)"}%]*}#*\[}
-        Object result = context.execute("sleep 1 & sleep 1 & ${${${(f)$(jobs)}%']*'}#'*\\['}");
-        assertEquals(Arrays.asList("1", "2"), result);
+//        Object result = context.execute("sleep 1 & sleep 1 & ${${${(f)\"$(jobs)\"}%']*'}#'*\\['}");
+        Object result = context.execute("sleep 1 & sleep 1 & ${${${(f)$(jobs)}%\\]*}#*\\[}");
+        Assert.assertEquals(Arrays.asList("1", "2"), result);
     }
 
 }
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/BaseTokenizer.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/BaseTokenizer.java
index 2ec6887..6d11c8e 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/BaseTokenizer.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/BaseTokenizer.java
@@ -45,7 +45,6 @@
     {
         final short sLine = line;
         final short sCol = column;
-        int start = ch;
         int level = 1;
 
         while (level != 0)
@@ -53,7 +52,7 @@
             if (eot())
             {
                 throw new EOFError(sLine, sCol, "unexpected eof found in the middle of a compound for '"
-                        + deeper + target + "', begins at " + start, "compound", Character.toString(target));
+                        + deeper + target + "'", "compound", Character.toString(target));
                 // TODO: fill context correctly
             }
 
@@ -61,6 +60,7 @@
             if (ch == '\\')
             {
                 escape();
+                continue;
             }
             if (ch == target)
             {
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 77f4c01..1929994 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
@@ -35,10 +35,14 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Callable;
 import java.util.function.BiFunction;
 import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 @SuppressWarnings("fallthrough")
 public class Expander extends BaseTokenizer
@@ -49,70 +53,82 @@
      */
     public static Object expand(CharSequence word, Evaluate eval) throws Exception
     {
-        return expand(word, eval, false);
+        return expand(word, eval, false, true, false, true, false);
     }
 
     private static Object expand(CharSequence word, Evaluate eval, boolean inQuote) throws Exception
     {
-        return new Expander(word, eval, inQuote).expand();
+        return new Expander(word, eval, inQuote, true, false, false, false).expand();
+    }
+
+    private static Object expand(CharSequence word, Evaluate eval,
+                                 boolean inQuote,
+                                 boolean generateFileNames,
+                                 boolean semanticJoin,
+                                 boolean unquote,
+                                 boolean asPattern) throws Exception
+    {
+        return new Expander(word, eval, inQuote, generateFileNames, semanticJoin, unquote, asPattern).expand();
     }
 
     private final Evaluate evaluate;
     private boolean inQuote;
+    private boolean generateFileNames;
+    private boolean semanticJoin;
+    private boolean unquote;
+    private boolean asPattern;
 
-    public Expander(CharSequence text, Evaluate evaluate, boolean inQuote)
+    public Expander(CharSequence text, Evaluate evaluate,
+                    boolean inQuote,
+                    boolean generateFileNames,
+                    boolean semanticJoin,
+                    boolean unquote,
+                    boolean asPattern)
     {
         super(text);
         this.evaluate = evaluate;
         this.inQuote = inQuote;
+        this.generateFileNames = generateFileNames;
+        this.semanticJoin = semanticJoin;
+        this.unquote = unquote;
+        this.asPattern = asPattern;
     }
 
     public Object expand(CharSequence word) throws Exception
     {
-        return expand(word, evaluate, inQuote);
+        return expand(word, evaluate, inQuote, true, false, false, false);
     }
 
+    public Object expand(CharSequence word,
+                         boolean generateFileNames,
+                         boolean semanticJoin,
+                         boolean unquote) throws Exception
+    {
+        return expand(word, evaluate, inQuote, generateFileNames, semanticJoin, unquote, false);
+    }
+
+    public Object expandPattern(CharSequence word) throws Exception {
+        return expand(word, evaluate, inQuote, false, false, false, true);
+    }
 
     private Object expand() throws Exception
     {
         Object expanded = doExpand();
-        if (expanded instanceof List) {
-            List<Object> list = new ArrayList<>();
-            for (Object o : ((List) expanded)) {
-                if (o instanceof CharSequence) {
-                    list.addAll(generateFileNames((CharSequence) o));
-                } else {
-                    list.add(o);
-                }
-            }
-            List<Object> unquoted = new ArrayList<>();
-            for (Object o : list) {
-                if (o instanceof CharSequence) {
-                    unquoted.add(unquote((CharSequence) o));
-                } else {
-                    unquoted.add(o);
-                }
-            }
-            if (unquoted.size() == 1) {
-                return unquoted.get(0);
-            }
-            if (expanded instanceof ArgList) {
-                return new ArgList(unquoted);
-            } else {
-                return unquoted;
-            }
-        } else if (expanded instanceof CharSequence) {
-            List<? extends CharSequence> list = generateFileNames((CharSequence) expanded);
-            List<CharSequence> unquoted = new ArrayList<>();
-            for (CharSequence o : list) {
-                unquoted.add(unquote(o));
-            }
-            if (unquoted.size() == 1) {
-                return unquoted.get(0);
-            }
-            return new ArgList(unquoted);
+        Stream<Object> stream = expanded instanceof Collection
+                ? asCollection(expanded).stream()
+                : Stream.of(expanded);
+        List<Object> args = stream
+                .flatMap(uncheck(o -> o instanceof CharSequence ? expandBraces((CharSequence) o).stream() : Stream.of(o)))
+                .flatMap(uncheck(o -> generateFileNames && o instanceof CharSequence ? generateFileNames((CharSequence) o).stream() : Stream.of(o)))
+                .map(o -> unquote && o instanceof CharSequence ? unquote((CharSequence) o) : o)
+                .collect(Collectors.toList());
+        if (args.size() == 1) {
+            return args.get(0);
         }
-        return expanded;
+        if (expanded instanceof ArgList) {
+            return new ArgList(args);
+        }
+        return args;
     }
 
     private CharSequence unquote(CharSequence arg) {
@@ -181,6 +197,215 @@
         return buf.toString();
     }
 
+    protected List<? extends CharSequence> expandBraces(CharSequence arg) throws Exception {
+        int braces = 0;
+        boolean escaped = false;
+        boolean doubleQuoted = false;
+        boolean singleQuoted = false;
+        List<CharSequence> parts = new ArrayList<>();
+        int start = 0;
+        for (int i = 0; i < arg.length(); i++) {
+            char c = arg.charAt(i);
+            if (doubleQuoted && escaped) {
+                escaped = false;
+            }
+            else if (escaped) {
+                escaped = false;
+            }
+            else if (singleQuoted) {
+                if (c == '\'') {
+                    singleQuoted = false;
+                }
+            }
+            else if (doubleQuoted) {
+                if (c == '\\') {
+                    escaped = true;
+                }
+                else if (c == '\"') {
+                    doubleQuoted = false;
+                }
+            }
+            else if (c == '\\') {
+                escaped = true;
+            }
+            else if (c == '\'') {
+                singleQuoted = true;
+            }
+            else if (c == '"') {
+                doubleQuoted = true;
+            }
+            else {
+                if (c == '{') {
+                    if (braces++ == 0) {
+                        if (i > start) {
+                            parts.add(arg.subSequence(start, i));
+                        }
+                        start = i;
+                    }
+                }
+                else if (c == '}') {
+                    if (--braces == 0) {
+                        parts.add(arg.subSequence(start, i + 1));
+                        start = i + 1;
+                    }
+                }
+            }
+        }
+        if (start < arg.length()) {
+            parts.add(arg.subSequence(start, arg.length()));
+        }
+        if (start == 0) {
+            return Collections.singletonList(arg);
+        }
+        List<CharSequence> generated = new ArrayList<>();
+        Pattern pattern = Pattern.compile(
+                "\\{(((?<intstart>\\-?[0-9]+)\\.\\.(?<intend>\\-?[0-9]+)(\\.\\.(?<intinc>\\-?0*[1-9][0-9]*))?)" +
+                "|((?<charstart>\\S)\\.\\.(?<charend>\\S)))\\}");
+        for (CharSequence part : parts) {
+            List<CharSequence> generators = new ArrayList<>();
+            Matcher matcher = pattern.matcher(part);
+            if (matcher.matches()) {
+                if (matcher.group("intstart") != null) {
+                    int intstart = Integer.parseInt(matcher.group("intstart"));
+                    int intend = Integer.parseInt(matcher.group("intend"));
+                    int intinc = matcher.group("intinc") != null ? Integer.parseInt(matcher.group("intinc")) : 1;
+                    if (intstart > intend) {
+                        if (intinc < 0) {
+                            int k = intstart;
+                            intstart = intend;
+                            intend = k;
+                        }
+                        intinc = -intinc;
+                    } else {
+                        if (intinc < 0) {
+                            int k = intstart;
+                            intstart = intend;
+                            intend = k;
+                        }
+                    }
+                    if (intinc > 0) {
+                        for (int k = intstart; k <= intend; k += intinc) {
+                            generators.add(Integer.toString(k));
+                        }
+                    } else {
+                        for (int k = intstart; k >= intend; k += intinc) {
+                            generators.add(Integer.toString(k));
+                        }
+                    }
+                }
+                else {
+                    char charstart = matcher.group("charstart").charAt(0);
+                    char charend = matcher.group("charend").charAt(0);
+                    if (charstart < charend) {
+                        for (char c = charstart; c <= charend; c++) {
+                            generators.add(Character.toString(c));
+                        }
+                    }
+                    else {
+                        for (char c = charstart; c >= charend; c--) {
+                            generators.add(Character.toString(c));
+                        }
+                    }
+                }
+            }
+            else if (part.charAt(0) == '{' && part.charAt(part.length() - 1) == '}') {
+                // Split on commas
+                braces = 0;
+                escaped = false;
+                doubleQuoted = false;
+                singleQuoted = false;
+                start = 1;
+                for (int i = 1; i < part.length() - 1; i++) {
+                    char c = part.charAt(i);
+                    if (doubleQuoted && escaped) {
+                        escaped = false;
+                    }
+                    else if (escaped) {
+                        escaped = false;
+                    }
+                    else if (singleQuoted) {
+                        if (c == '\'') {
+                            singleQuoted = false;
+                        }
+                    }
+                    else if (doubleQuoted) {
+                        if (c == '\\') {
+                            escaped = true;
+                        }
+                        else if (c == '\"') {
+                            doubleQuoted = false;
+                        }
+                    }
+                    else if (c == '\\') {
+                        escaped = true;
+                    }
+                    else if (c == '\'') {
+                        singleQuoted = true;
+                    }
+                    else if (c == '"') {
+                        doubleQuoted = true;
+                    }
+                    else {
+                        if (c == '}') {
+                            braces--;
+                        }
+                        else if (c == '{') {
+                            braces++;
+                        }
+                        else if (c == ',' && braces == 0) {
+                            generators.add(part.subSequence(start, i));
+                            start = i + 1;
+                        }
+                    }
+                }
+                if (start < part.length() - 1) {
+                    generators.add(part.subSequence(start, part.length() - 1));
+                }
+                generators = generators.stream()
+                        .map(uncheck(cs -> expand(cs, false, false, false)))
+                        .flatMap(o -> o instanceof Collection ? asCollection(o).stream() : Stream.of(o))
+                        .map(String::valueOf)
+                        .collect(Collectors.toList());
+
+                // If there's no splitting comma, expand with the braces
+                if (generators.size() < 2) {
+                    generators = Collections.singletonList(part.toString());
+                }
+            }
+            else {
+                generators.add(part.toString());
+            }
+            if (generated.isEmpty()) {
+                generated.addAll(generators);
+            } else {
+                List<CharSequence> prevGenerated = generated;
+                generated = generators.stream()
+                        .flatMap(s -> prevGenerated.stream().map(cs -> String.valueOf(cs) + s))
+                        .collect(Collectors.toList());
+            }
+        }
+        return generated;
+    }
+
+    public interface FunctionExc<T, R> {
+        R apply(T t) throws Exception;
+    }
+
+    public static <T, R> Function<T, R> uncheck(FunctionExc<T, R> func) {
+        return t -> {
+            try {
+                return func.apply(t);
+            } catch (Exception e) {
+                return sneakyThrow(e);
+            }
+        };
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <E extends Throwable, T> T sneakyThrow(Throwable t) throws E {
+        throw (E) t;
+    }
+
     protected List<? extends CharSequence> generateFileNames(CharSequence arg) throws IOException {
         // Disable if currentDir is not set
         Path currentDir = evaluate.currentDir();
@@ -328,18 +553,15 @@
             {
                 case '%':
                     Object exp = expandExp();
-
                     if (EOT == ch && buf.length() == 0)
                     {
                         return exp;
                     }
-
                     if (null != exp)
                     {
                         buf.append(exp);
                     }
-
-                    continue; // expandVar() has already read next char
+                    break;
 
                 case '$':
                     // Posix quote
@@ -362,7 +584,7 @@
                             buf.append(val);
                         }
                     }
-                    continue; // expandVar() has already read next char
+                    break;
 
                 case '\\':
                     buf.append(ch);
@@ -370,6 +592,7 @@
                         getch();
                         buf.append(ch);
                     }
+                    getch();
                     break;
 
                 case '"':
@@ -408,28 +631,25 @@
                         buf.append(expand.toString());
                         buf.append("\"");
                     }
-                    continue; // has already read next char
+                    break;
 
                 case '\'':
-                    if (!inQuote)
+                    skipQuote();
+                    value = text.subSequence(start - 1, index);
+                    getch();
+                    if (eot() && buf.length() == 0)
                     {
-                        skipQuote();
-                        value = text.subSequence(start - 1, index);
-
-                        if (eot() && buf.length() == 0)
-                        {
-                            return value;
-                        }
-
-                        buf.append(value);
-                        break;
+                        return value;
                     }
-                    // else fall through
+                    buf.append(value);
+                    break;
+
                 default:
                     buf.append(ch);
+                    getch();
+                    break;
             }
 
-            getch();
         }
 
         return buf.toString();
@@ -668,6 +888,8 @@
             // split / join
             String flags = null;
             String flagj = null;
+            // pattern
+            boolean flagPattern = false;
             // length
             boolean computeLength = false;
             // Parse flags
@@ -829,15 +1051,24 @@
                 val = getAndEvaluateName();
             }
             else {
-                if (ch == '#') {
-                    computeLength = true;
-                    getch();
-                }
-                if (ch == '=') {
-                    if (flags == null) {
-                        flags = "\\s";
+                while (true) {
+                    if (ch == '#') {
+                        computeLength = true;
+                        getch();
                     }
-                    getch();
+                    else if (ch == '=') {
+                        if (flags == null) {
+                            flags = "\\s";
+                        }
+                        getch();
+                    }
+                    else if (ch == '~') {
+                        flagPattern = true;
+                        getch();
+                    }
+                    else {
+                        break;
+                    }
                 }
 
                 Object val1 = getName('}');
@@ -889,9 +1120,9 @@
                             || 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();
+                        String val2 = getPattern(op.charAt(0) == '/' ? "/}" : "}");
                         if (val2 != null) {
-                            String p = toRegexPattern(val2.toString(), op.length() == 1);
+                            String p = toRegexPattern(unquoteGlob(val2), op.length() == 1);
                             String r;
                             if (op.charAt(0) == '/') {
                                 if (ch == '/') {
@@ -1044,10 +1275,23 @@
                 throw new SyntaxError(sLine, sCol, "bad substitution");
             }
 
+            // Parameter name replacement
             if (flagP) {
                 val = val != null ? evaluate.get(val.toString()) : null;
             }
 
+            // Double quote joining
+            boolean joined = false;
+            if (inQuote && !computeLength && !flagExpand) {
+                val = mapToList.apply(val);
+                if (val instanceof Collection) {
+                    String j = flagj != null ? flagj : " ";
+                    val = asCollection(val).stream()
+                            .map(String::valueOf)
+                            .collect(Collectors.joining(j));
+                    joined = true;
+                }
+            }
 
             // Character evaluation
             if (flagSharp) {
@@ -1070,17 +1314,18 @@
                 }
             }
 
-            // Joining
-            if (flagj != null) {
+            // Forced joining
+            if (flagj != null || flags != null && !joined) {
                 val = mapToList.apply(val);
                 if (val instanceof Collection) {
+                    String j = flagj != null ? flagj : " ";
                     val = asCollection(val).stream()
                             .map(String::valueOf)
-                            .collect(Collectors.joining(flagj));
+                            .collect(Collectors.joining(j));
                 }
             }
 
-            // Splitting
+            // Simple word splitting
             if (flags != null) {
                 String _flags = flags;
                 val = mapToList.apply(val);
@@ -1154,6 +1399,16 @@
                 }
             }
 
+            // Semantic joining
+            if (semanticJoin) {
+                val = mapToList.apply(val);
+                if (val instanceof Collection) {
+                    val = asCollection(val).stream()
+                            .map(String::valueOf)
+                            .collect(Collectors.joining(" "));
+                }
+            }
+
             // Empty argument removal
             if (val instanceof Collection) {
                 val = asCollection(val).stream()
@@ -1161,6 +1416,15 @@
                         .collect(Collectors.toList());
             }
 
+            if (asPattern && !inQuote && !flagPattern) {
+                val = mapToList.apply(val);
+                Stream<Object> stream = val instanceof Collection ? asCollection(val).stream() : Stream.of(val);
+                List<String> patterns = stream.map(String::valueOf)
+                        .map(s -> quote(s, 2))
+                        .collect(Collectors.toList());
+                val = patterns.size() == 1 ? patterns.get(0) : patterns;
+            }
+
             if (inQuote) {
                 val = mapToList.apply(val);
                 if (val instanceof Collection) {
@@ -1455,9 +1719,24 @@
     }
 
     private Object getName(char closing) throws Exception {
-        if (ch == '$') {
+        if (ch == '\"') {
+            if (peek() != '$') {
+                throw new IllegalArgumentException("bad substitution");
+            }
+            boolean oldInQuote = inQuote;
+            try {
+                inQuote = true;
+                getch();
+                Object val = getName(closing);
+                return val;
+            } finally {
+                inQuote = oldInQuote;
+            }
+        }
+        else if (ch == '$') {
             return expandVar();
-        } else {
+        }
+        else {
             int start = index - 1;
             while (ch != EOT && ch != closing && isName(ch)) {
                 getch();
@@ -1475,6 +1754,63 @@
         }
     }
 
+    private String getPattern(String closing) throws Exception {
+        CharSequence sub = findUntil(text, index - 1, closing);
+        index += sub.length() - 1;
+        getch();
+        return expandPattern(sub).toString();
+    }
+
+    private CharSequence findUntil(CharSequence text, int start, String closing) throws Exception {
+        int braces = 0;
+        boolean escaped = false;
+        boolean doubleQuoted = false;
+        boolean singleQuoted = false;
+        for (int i = start; i < text.length(); i++) {
+            char c = text.charAt(i);
+            if (doubleQuoted && escaped) {
+                escaped = false;
+            }
+            else if (escaped) {
+                escaped = false;
+            }
+            else if (singleQuoted) {
+                if (c == '\'') {
+                    singleQuoted = false;
+                }
+            }
+            else if (doubleQuoted) {
+                if (c == '\\') {
+                    escaped = true;
+                }
+                else if (c == '\"') {
+                    doubleQuoted = false;
+                }
+            }
+            else if (c == '\\') {
+                escaped = true;
+            }
+            else if (c == '\'') {
+                singleQuoted = true;
+            }
+            else if (c == '"') {
+                doubleQuoted = true;
+            }
+            else {
+                if (braces == 0 && closing.indexOf(c) >= 0) {
+                    return text.subSequence(start, i);
+                }
+                else if (c == '{') {
+                    braces++;
+                }
+                else if (c == '}') {
+                    braces--;
+                }
+            }
+        }
+        return text.subSequence(start, text.length());
+    }
+
     private Object getValue() throws Exception {
         if (ch == '$') {
             return expandVar();
@@ -1527,6 +1863,69 @@
         return index < str.length() ? str.charAt(index) : EOL;
     }
 
+    /**
+     * Convert a string containing escape sequences and quotes, representing a glob pattern
+     * to the corresponding regexp pattern
+     */
+    private static String unquoteGlob(String str) {
+        StringBuilder sb = new StringBuilder();
+        int index = 0;
+        boolean escaped = false;
+        boolean doubleQuoted = false;
+        boolean singleQuoted = false;
+        while (index < str.length()) {
+            char ch = str.charAt(index++);
+            if (escaped) {
+                if (isGlobMeta(ch)) {
+                    sb.append('\\');
+                }
+                sb.append(ch);
+                escaped = false;
+            }
+            else if (singleQuoted) {
+                if (ch == '\'') {
+                    singleQuoted = false;
+                } else {
+                    if (isGlobMeta(ch)) {
+                        sb.append('\\');
+                    }
+                    sb.append(ch);
+                }
+            }
+            else if (doubleQuoted) {
+                if (ch == '\\') {
+                    escaped = true;
+                }
+                else if (ch == '\"') {
+                    doubleQuoted = false;
+                }
+                else {
+                    if (isGlobMeta(ch)) {
+                        sb.append('\\');
+                    }
+                    sb.append(ch);
+                }
+            }
+            else {
+                switch (ch) {
+                    case '\\':
+                        escaped = true;
+                        break;
+                    case '\'':
+                        singleQuoted = true;
+                        break;
+                    case '"':
+                        doubleQuoted = true;
+                        break;
+                    default:
+                        sb.append(ch);
+                        break;
+                }
+            }
+        }
+        return sb.toString();
+    }
+
     private static String toRegexPattern(String str, boolean shortest) {
         boolean inGroup = false;
         StringBuilder sb = new StringBuilder();
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Tokenizer.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Tokenizer.java
index ab1b164..d89dce2 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Tokenizer.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Tokenizer.java
@@ -67,6 +67,24 @@
                     word++;
                     return token(start);
                 case '{':
+                    if (start == index - 1 && Character.isWhitespace(peek()))
+                    {
+                        word = 0;
+                        return token(start);
+                    }
+                    else
+                    {
+                        if (ch == '{')
+                        {
+                            find('}', '{');
+                        }
+                        else
+                        {
+                            find(')', '(');
+                        }
+                        getch();
+                        break;
+                    }
                 case '(':
                     if (start == index - 1)
                     {
diff --git a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser2.java b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser2.java
index 5cc4119..09b8928 100644
--- a/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser2.java
+++ b/gogo/runtime/src/test/java/org/apache/felix/gogo/runtime/TestParser2.java
@@ -52,7 +52,7 @@
         // 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"));
+//CHANGE        assertEquals("ok", c.execute("x = {// can't quote\necho ok\n}; x"));
     }
 
     public void testCoercion() throws Exception
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 f41522d..a4b7a5f 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
@@ -208,6 +208,37 @@
     }
 
     @Test
+    public void testBraces() throws Exception {
+        assertEquals(Arrays.asList("1", "3"), expand("{1..3..2}"));
+        assertEquals(Arrays.asList("3", "1"), expand("{1..3..-2}"));
+        assertEquals(Arrays.asList("1", "3"), expand("{3..1..-2}"));
+        assertEquals(Arrays.asList("3", "1"), expand("{3..1..2}"));
+
+        assertEquals(Arrays.asList("1", "2", "3"), expand("{1..3}"));
+        assertEquals(Arrays.asList("a1b", "a2b", "a3b"), expand("a{1..3}b"));
+
+        assertEquals(Arrays.asList("a1b", "a2b", "a3b"), expand("a{1,2,3}b"));
+
+        assertEquals(Arrays.asList("a1b", "a2b", "a3b"), expand("a{1,2,3}b"));
+        assertEquals(Arrays.asList("a1b", "a,b", "a3b"), expand("a{1,',',3}b"));
+
+        currentDir = Paths.get(".");
+        try {
+            expand("a{1,*,3}b");
+            fail("Expected exception");
+        } catch (Exception e) {
+            assertEquals("no matches found: a*b", e.getMessage());
+        } finally {
+            currentDir = null;
+        }
+
+        vars.put("a", "1");
+        assertEquals(Arrays.asList("a1b", "a\nb", "a3b"), expand("a{$a,$'\\n',3}b"));
+        assertEquals(Arrays.asList("ab1*z", "ab2*z", "arz"), expand("a{b{1..2}'*',r}z"));
+
+    }
+
+    @Test
     public void testJoinSplit() throws Exception {
         vars.put("array", Arrays.asList("a", "b", "c"));
         vars.put("string", "a\n\nb\nc");
@@ -299,12 +330,17 @@
     public void testPatterns() throws Exception {
         vars.put("foo", "twinkle twinkle little star");
         vars.put("sub", "t*e");
+        vars.put("sb", "*e");
         vars.put("rep", "spy");
 
-        assertEquals("spy twinkle little star", expand("${foo/${sub}/${rep}}"));
-        assertEquals("spy star", expand("${foo//${sub}/${rep}}"));
-        assertEquals("spy spy lispy star", expand("${(G)foo/${sub}/${rep}}"));
-        assertEquals("spy star", expand("${(G)foo//${sub}/${rep}}"));
+        assertEquals("spynkle spynkle little star", expand("${(G)foo/'twi'/$rep}"));
+
+        assertEquals("twinkle twinkle little star", expand("${foo/${sub}/${rep}}"));
+        assertEquals("spy twinkle little star", expand("${foo/${~sub}/${rep}}"));
+        assertEquals("spy star", expand("${foo//${~sub}/${rep}}"));
+        assertEquals("spy spy lispy star", expand("${(G)foo/${~sub}/${rep}}"));
+        assertEquals("spy star", expand("${(G)foo//${~sub}/${rep}}"));
+        assertEquals("spy twinkle little star", expand("${foo/t${~sb}/${rep}}"));
     }
 
     @Test