Better support for streams
Better ls implementation

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1736001 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/IoUtils.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/IoUtils.java
new file mode 100644
index 0000000..9f14b96
--- /dev/null
+++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/IoUtils.java
@@ -0,0 +1,105 @@
+/*
+ * 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.jline;
+
+import java.io.File;
+import java.nio.file.LinkOption;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.sshd.common.util.OsUtils;
+
+/**
+ * TODO Add javadoc
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public final class IoUtils {
+
+    public static final LinkOption[] EMPTY_LINK_OPTIONS = new LinkOption[0];
+
+    public static final List<String> WINDOWS_EXECUTABLE_EXTENSIONS = Collections.unmodifiableList(Arrays.asList(".bat", ".exe", ".cmd"));
+
+    private static final LinkOption[] NO_FOLLOW_OPTIONS = new LinkOption[]{LinkOption.NOFOLLOW_LINKS};
+
+    /**
+     * Private Constructor
+     */
+    private IoUtils() {
+        throw new UnsupportedOperationException("No instance allowed");
+    }
+
+    public static LinkOption[] getLinkOptions(boolean followLinks) {
+        if (followLinks) {
+            return EMPTY_LINK_OPTIONS;
+        } else {    // return a clone that modifications to the array will not affect others
+            return NO_FOLLOW_OPTIONS.clone();
+        }
+    }
+
+    /**
+     * @param fileName The file name to be evaluated - ignored if {@code null}/empty
+     * @return {@code true} if the file ends in one of the {@link #WINDOWS_EXECUTABLE_EXTENSIONS}
+     */
+    public static boolean isWindowsExecutable(String fileName) {
+        if ((fileName == null) || (fileName.length() <= 0)) {
+            return false;
+        }
+        for (String suffix : WINDOWS_EXECUTABLE_EXTENSIONS) {
+            if (fileName.endsWith(suffix)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @param f The {@link File} to be checked
+     * @return A {@link Set} of {@link PosixFilePermission}s based on whether
+     * the file is readable/writable/executable. If so, then <U>all</U> the
+     * relevant permissions are set (i.e., owner, group and others)
+     */
+    public static Set<PosixFilePermission> getPermissionsFromFile(File f) {
+        Set<PosixFilePermission> perms = EnumSet.noneOf(PosixFilePermission.class);
+        if (f.canRead()) {
+            perms.add(PosixFilePermission.OWNER_READ);
+            perms.add(PosixFilePermission.GROUP_READ);
+            perms.add(PosixFilePermission.OTHERS_READ);
+        }
+
+        if (f.canWrite()) {
+            perms.add(PosixFilePermission.OWNER_WRITE);
+            perms.add(PosixFilePermission.GROUP_WRITE);
+            perms.add(PosixFilePermission.OTHERS_WRITE);
+        }
+
+        if (f.canExecute() || (OsUtils.isWin32() && isWindowsExecutable(f.getName()))) {
+            perms.add(PosixFilePermission.OWNER_EXECUTE);
+            perms.add(PosixFilePermission.GROUP_EXECUTE);
+            perms.add(PosixFilePermission.OTHERS_EXECUTE);
+        }
+
+        return perms;
+    }
+
+}
diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/JLineCommands.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/JLineCommands.java
index 294011e..47784cd 100644
--- a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/JLineCommands.java
+++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/JLineCommands.java
@@ -22,6 +22,7 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.List;
@@ -78,7 +79,7 @@
 
     private void runShell(CommandSession session, Terminal terminal) {
         InputStream in = terminal.input();
-        PrintStream out = new PrintStream(terminal.output());
+        OutputStream out = terminal.output();
         CommandSession newSession = processor.createSession(in, out, out);
         newSession.put(Shell.VAR_TERMINAL, terminal);
         newSession.put(".tmux", session.get(".tmux"));
@@ -139,8 +140,8 @@
             final String cmd = String.join(" ", args);
             Runnable task = () -> {
                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                InputStream is = new ByteArrayInputStream(new byte[0]);
                 PrintStream os = new PrintStream(baos);
+                InputStream is = new ByteArrayInputStream(new byte[0]);
                 if (opt.isSet("append") || !terminal.puts(Capability.clear_screen)) {
                     terminal.writer().println();
                 }
diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Main.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Main.java
index 8d78f6a..53fc716 100644
--- a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Main.java
+++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Main.java
@@ -68,8 +68,7 @@
                 } catch (Throwable t) {
                     // ignore
                 }
-                PrintStream pout = new PrintStream(terminal.output());
-                CommandSession session = processor.createSession(terminal.input(), pout, pout);
+                CommandSession session = processor.createSession(terminal.input(), terminal.output(), terminal.output());
                 session.put(Shell.VAR_CONTEXT, context);
                 session.put(Shell.VAR_TERMINAL, terminal);
                 try {
diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java
index 4b90b7e..e40c2ef 100644
--- a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java
+++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java
@@ -19,22 +19,54 @@
 package org.apache.felix.gogo.jline;
 
 import java.io.BufferedReader;
-import java.io.File;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.PrintStream;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
-import java.util.Collection;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.EnumSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.function.Consumer;
+import java.util.function.IntBinaryOperator;
+import java.util.function.Predicate;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
+import org.apache.felix.gogo.runtime.Pipe;
 import org.apache.felix.service.command.CommandSession;
+import org.jline.builtins.Less.Source;
+import org.jline.builtins.Less.StdInSource;
+import org.jline.builtins.Less.URLSource;
 import org.jline.builtins.Options;
+import org.jline.reader.LineReader.Option;
+import org.jline.terminal.Terminal;
+import org.jline.utils.AttributedString;
+import org.jline.utils.AttributedStringBuilder;
+import org.jline.utils.AttributedStyle;
 
 /**
  * Posix-like utilities.
@@ -45,7 +77,57 @@
 public class Posix {
     static final String[] functions = {"cat", "echo", "grep", "sort", "sleep", "cd", "pwd", "ls"};
 
-    public static void sort(CommandSession session, String[] argv) throws IOException {
+    public void _main(CommandSession session, String[] argv) {
+        if (argv == null || argv.length < 1) {
+            throw new IllegalArgumentException();
+        }
+        try {
+            argv = expand(session, argv);
+            switch (argv[0]) {
+                case "cat":
+                    cat(session, argv);
+                    break;
+                case "echo":
+                    echo(session, argv);
+                    break;
+                case "grep":
+                    grep(session, argv);
+                    break;
+                case "sort":
+                    sort(session, argv);
+                    break;
+                case "sleep":
+                    sleep(session, argv);
+                    break;
+                case "cd":
+                    cd(session, argv);
+                    break;
+                case "pwd":
+                    pwd(session, argv);
+                    break;
+                case "ls":
+                    ls(session, argv);
+                    break;
+            }
+        } catch (IllegalArgumentException e) {
+            System.err.println(e.getMessage());
+            Pipe.error(2);
+        } catch (HelpException e) {
+            System.err.println(e.getMessage());
+            Pipe.error(0);
+        } catch (Exception e) {
+            System.err.println(argv[0] + ": " + e.getMessage());
+            Pipe.error(1);
+        }
+    }
+
+    protected static class HelpException extends Exception {
+        public HelpException(String message) {
+            super(message);
+        }
+    }
+
+    protected void sort(CommandSession session, String[] argv) throws Exception {
         final String[] usage = {
                 "sort -  writes sorted standard input to standard output.",
                 "Usage: sort [OPTIONS] [FILES]",
@@ -58,24 +140,16 @@
                 "     --numeric-sort            compare according to string numerical value",
                 "  -k --key=KEY                 fields to use for sorting separated by whitespaces"};
 
-        Options opt = Options.compile(usage).parse(argv);
-
-        if (opt.isSet("help")) {
-            opt.usage(System.err);
-            return;
-        }
+        Options opt = parseOptions(session, usage, argv);
 
         List<String> args = opt.args();
 
-        List<String> lines = new ArrayList<String>();
+        List<String> lines = new ArrayList<>();
         if (!args.isEmpty()) {
             for (String filename : args) {
-                BufferedReader reader = new BufferedReader(new InputStreamReader(
-                        session.currentDir().toUri().resolve(filename).toURL().openStream()));
-                try {
+                try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+                        session.currentDir().toUri().resolve(filename).toURL().openStream()))) {
                     read(reader, lines);
-                } finally {
-                    reader.close();
                 }
             }
         } else {
@@ -102,272 +176,362 @@
         }
     }
 
-    protected static void read(BufferedReader r, List<String> lines) throws IOException {
-        for (String s = r.readLine(); s != null; s = r.readLine()) {
-            lines.add(s);
-        }
-    }
-
-    public static List<String> parseSubstring(String value) {
-        List<String> pieces = new ArrayList<String>();
-        StringBuilder ss = new StringBuilder();
-        // int kind = SIMPLE; // assume until proven otherwise
-        boolean wasStar = false; // indicates last piece was a star
-        boolean leftstar = false; // track if the initial piece is a star
-        boolean rightstar = false; // track if the final piece is a star
-
-        int idx = 0;
-
-        // We assume (sub)strings can contain leading and trailing blanks
-        boolean escaped = false;
-        loop:
-        for (; ; ) {
-            if (idx >= value.length()) {
-                if (wasStar) {
-                    // insert last piece as "" to handle trailing star
-                    rightstar = true;
-                } else {
-                    pieces.add(ss.toString());
-                    // accumulate the last piece
-                    // note that in the case of
-                    // (cn=); this might be
-                    // the string "" (!=null)
-                }
-                ss.setLength(0);
-                break loop;
-            }
-
-            // Read the next character and account for escapes.
-            char c = value.charAt(idx++);
-            if (!escaped && ((c == '(') || (c == ')'))) {
-                throw new IllegalArgumentException(
-                        "Illegal value: " + value);
-            } else if (!escaped && (c == '*')) {
-                if (wasStar) {
-                    // encountered two successive stars;
-                    // I assume this is illegal
-                    throw new IllegalArgumentException("Invalid filter string: " + value);
-                }
-                if (ss.length() > 0) {
-                    pieces.add(ss.toString()); // accumulate the pieces
-                    // between '*' occurrences
-                }
-                ss.setLength(0);
-                // if this is a leading star, then track it
-                if (pieces.size() == 0) {
-                    leftstar = true;
-                }
-                wasStar = true;
-            } else if (!escaped && (c == '\\')) {
-                escaped = true;
-            } else {
-                escaped = false;
-                wasStar = false;
-                ss.append(c);
-            }
-        }
-        if (leftstar || rightstar || pieces.size() > 1) {
-            // insert leading and/or trailing "" to anchor ends
-            if (rightstar) {
-                pieces.add("");
-            }
-            if (leftstar) {
-                pieces.add(0, "");
-            }
-        }
-        return pieces;
-    }
-
-    public static boolean compareSubstring(List<String> pieces, String s) {
-        // Walk the pieces to match the string
-        // There are implicit stars between each piece,
-        // and the first and last pieces might be "" to anchor the match.
-        // assert (pieces.length > 1)
-        // minimal case is <string>*<string>
-
-        boolean result = true;
-        int len = pieces.size();
-
-        int index = 0;
-
-        loop:
-        for (int i = 0; i < len; i++) {
-            String piece = pieces.get(i);
-
-            // If this is the first piece, then make sure the
-            // string starts with it.
-            if (i == 0) {
-                if (!s.startsWith(piece)) {
-                    result = false;
-                    break loop;
-                }
-            }
-
-            // If this is the last piece, then make sure the
-            // string ends with it.
-            if (i == len - 1) {
-                if (s.endsWith(piece)) {
-                    result = true;
-                } else {
-                    result = false;
-                }
-                break loop;
-            }
-
-            // If this is neither the first or last piece, then
-            // make sure the string contains it.
-            if ((i > 0) && (i < (len - 1))) {
-                index = s.indexOf(piece, index);
-                if (index < 0) {
-                    result = false;
-                    break loop;
-                }
-            }
-
-            // Move string index beyond the matching piece.
-            index += piece.length();
-        }
-
-        return result;
-    }
-
-    private static void cat(final BufferedReader reader, boolean displayLineNumbers) throws IOException {
-        String line;
-        int lineno = 1;
-        try {
-            while ((line = reader.readLine()) != null) {
-                if (displayLineNumbers) {
-                    System.out.print(String.format("%6d  ", lineno++));
-                }
-                System.out.println(line);
-            }
-        } finally {
-            reader.close();
-        }
-    }
-
-    public Path pwd(CommandSession session, String[] argv) throws IOException {
+    protected void pwd(CommandSession session, String[] argv) throws Exception {
         final String[] usage = {
                 "pwd - get current directory",
                 "Usage: pwd [OPTIONS]",
                 "  -? --help                show help"
         };
-        Options opt = Options.compile(usage).parse(argv);
-        if (opt.isSet("help")) {
-            opt.usage(System.err);
-            return null;
-        }
+        Options opt = parseOptions(session, usage, argv);
         if (!opt.args().isEmpty()) {
-            System.err.println("usage: pwd");
-            return null;
+            throw new IllegalArgumentException("usage: pwd");
         }
-        return session.currentDir();
+        System.out.println(session.currentDir());
     }
 
-    public void cd(CommandSession session, String[] argv) throws IOException {
+    protected void cd(CommandSession session, String[] argv) throws Exception {
         final String[] usage = {
                 "cd - get current directory",
                 "Usage: cd [OPTIONS] DIRECTORY",
                 "  -? --help                show help"
         };
-        Options opt = Options.compile(usage).parse(argv);
-        if (opt.isSet("help")) {
-            opt.usage(System.err);
-            return;
-        }
+        Options opt = parseOptions(session, usage, argv);
         if (opt.args().size() != 1) {
-            System.err.println("usage: cd DIRECTORY");
-            return;
+            throw new IllegalArgumentException("usage: cd DIRECTORY");
         }
         Path cwd = session.currentDir();
         cwd = cwd.resolve(opt.args().get(0)).toAbsolutePath();
         if (!Files.exists(cwd)) {
-            throw new IOException("Directory does not exist");
+            throw new IOException("no such file or directory: " + opt.args().get(0));
         } else if (!Files.isDirectory(cwd)) {
-            throw new IOException("Target is not a directory");
+            throw new IOException("not a directory: " + opt.args().get(0));
         }
         session.currentDir(cwd);
     }
 
-    public Collection<Path> ls(CommandSession session, String[] argv) throws IOException {
+    protected void ls(CommandSession session, String[] argv) throws Exception {
         final String[] usage = {
                 "ls - list files",
-                "Usage: ls [OPTIONS] PATTERNS...",
-                "  -? --help                show help"
+                "Usage: ls [OPTIONS] [PATTERNS...]",
+                "  -? --help                show help",
+                "  -a                       list entries starting with .",
+                "  -F                       append file type indicators",
+                "  -m                       comma separated",
+                "  -l                       long listing",
+                "  -S                       sort by size",
+                "  -f                       output is not sorted",
+                "  -r                       reverse sort order",
+                "  -t                       sort by modification time",
+                "  -x                       sort horizontally",
+                "  -L                       list referenced file for links",
+                "  -h                       print sizes in human readable form"
         };
-        Options opt = Options.compile(usage).parse(argv);
-        if (opt.isSet("help")) {
-            opt.usage(System.err);
-            return null;
-        }
-        if (opt.args().isEmpty()) {
-            opt.args().add("*");
-        }
-        List<Path> files = new ArrayList<>();
-        for (String pattern : opt.args()) {
-            pattern = ((pattern == null) || (pattern.length() == 0)) ? "." : pattern;
-            pattern = ((pattern.charAt(0) != File.separatorChar) && (pattern.charAt(0) != '.'))
-                    ? "./" + pattern : pattern;
-            int idx = pattern.lastIndexOf(File.separatorChar);
-            String parent = (idx < 0) ? "." : pattern.substring(0, idx + 1);
-            String target = (idx < 0) ? pattern : pattern.substring(idx + 1);
+        Options opt = parseOptions(session, usage, argv);
+        Map<String, String> colors = getColorMap(session, "LS", "dr=34:ex=31:sl=35:ot=34;43");
 
-            Path actualParent = session.currentDir().resolve(parent).normalize();
+        class PathEntry implements Comparable<PathEntry> {
+            final Path abs;
+            final Path path;
+            final Map<String, Object> attributes;
 
-            idx = target.indexOf(File.separatorChar, idx);
-            boolean isWildcarded = (target.indexOf('*', idx) >= 0);
-            if (isWildcarded) {
-                if (!Files.exists(actualParent)) {
-                    throw new IOException("File does not exist");
+            public PathEntry(Path abs, Path root) {
+                this.abs = abs;
+                this.path = abs.startsWith(root) ? root.relativize(abs) : abs;
+                this.attributes = readAttributes(abs);
+            }
+
+            @Override
+            public int compareTo(PathEntry o) {
+                int c = doCompare(o);
+                return opt.isSet("r") ? -c : c;
+            }
+
+            private int doCompare(PathEntry o) {
+                if (opt.isSet("f")) {
+                    return -1;
                 }
-                final List<String> pieces = parseSubstring(target);
-                Files.list(actualParent)
-                        .filter(p -> compareSubstring(pieces, p.getFileName().toString()))
-                        .map(actualParent::relativize)
-                        .forEach(files::add);
-            } else {
-                Path actualTarget = actualParent.resolve(target);
-                if (!Files.exists(actualTarget)) {
-                    throw new IOException("File does not exist");
+                if (opt.isSet("S")) {
+                    long s0 = attributes.get("size") != null ? ((Number) attributes.get("size")).longValue() : 0L;
+                    long s1 = o.attributes.get("size") != null ? ((Number) o.attributes.get("size")).longValue() : 0L;
+                    return s0 > s1 ? -1 : s0 < s1 ? 1 : path.toString().compareTo(o.path.toString());
                 }
-                if (Files.isDirectory(actualTarget)) {
-                    Files.list(actualTarget)
-                            .map(actualTarget::relativize)
-                            .forEach(files::add);
+                if (opt.isSet("t")) {
+                    long t0 = attributes.get("lastModifiedTime") != null ? ((FileTime) attributes.get("lastModifiedTime")).toMillis() : 0L;
+                    long t1 = o.attributes.get("lastModifiedTime") != null ? ((FileTime) o.attributes.get("lastModifiedTime")).toMillis() : 0L;
+                    return t0 > t1 ? -1 : t0 < t1 ? 1 : path.toString().compareTo(o.path.toString());
+                }
+                return path.toString().compareTo(o.path.toString());
+            }
+
+            boolean isNotDirectory() {
+                return is("isRegularFile") || is("isSymbolicLink") || is("isOther");
+            }
+
+            boolean isDirectory() {
+                return is("isDirectory");
+            }
+
+            private boolean is(String attr) {
+                Object d = attributes.get(attr);
+                return d instanceof Boolean && (Boolean) d;
+            }
+
+            String display() {
+                String type;
+                String suffix;
+                String link = "";
+                if (is("isDirectory")) {
+                    type = "dr";
+                    suffix = "/";
+                } else if (is("isExecutable")) {
+                    type = "ex";
+                    suffix = "*";
+                } else if (is("isSymbolicLink")) {
+                    type = "sl";
+                    suffix = "@";
+                    try {
+                        Path l = Files.readSymbolicLink(abs);
+                        link = " -> " + l.toString();
+                    } catch (IOException e) {
+                        // ignore
+                    }
+                } else if (is("isRegularFile")) {
+                    type = "rg";
+                    suffix = "";
+                } else if (is("isOther")) {
+                    type = "ot";
+                    suffix = "";
                 } else {
-                    files.add(actualTarget);
+                    type = "";
+                    suffix = "";
+                }
+                String col = colors.get(type);
+                boolean addSuffix = opt.isSet("F");
+                if (col != null && !col.isEmpty()) {
+                    return "\033[" + col + "m" + path.toString() + "\033[m" + (addSuffix ? suffix : "") + link;
+                } else {
+                    return path.toString() + (addSuffix ? suffix : "") + link;
                 }
             }
+
+            String longDisplay() {
+                String username;
+                if (attributes.containsKey("owner")) {
+                    username = Objects.toString(attributes.get("owner"), null);
+                } else {
+                    username = "owner";
+                }
+                if (username.length() > 8) {
+                    username = username.substring(0, 8);
+                } else {
+                    for (int i = username.length(); i < 8; i++) {
+                        username = username + " ";
+                    }
+                }
+                String group;
+                if (attributes.containsKey("group")) {
+                    group = Objects.toString(attributes.get("group"), null);
+                } else {
+                    group = "group";
+                }
+                if (group.length() > 8) {
+                    group = group.substring(0, 8);
+                } else {
+                    for (int i = group.length(); i < 8; i++) {
+                        group = group + " ";
+                    }
+                }
+                Number length = (Number) attributes.get("size");
+                if (length == null) {
+                    length = 0L;
+                }
+                String lengthString;
+                if (opt.isSet("h")) {
+                    double l = length.longValue();
+                    String unit = "B";
+                    if (l >= 1000) {
+                         l /= 1024;
+                        unit = "K";
+                        if (l >= 1000) {
+                            l /= 1024;
+                            unit = "M";
+                            if (l >= 1000) {
+                                l /= 1024;
+                                unit = "T";
+                            }
+                        }
+                    }
+                    if (l < 10 && length.longValue() > 1000) {
+                        lengthString = String.format("%.1f", l) + unit;
+                    } else {
+                        lengthString = String.format("%3.0f", l) + unit;
+                    }
+                } else {
+                    lengthString = String.format("%1$8s", length);
+                }
+                @SuppressWarnings("unchecked")
+                Set<PosixFilePermission> perms = (Set<PosixFilePermission>) attributes.get("permissions");
+                if (perms == null) {
+                    perms = EnumSet.noneOf(PosixFilePermission.class);
+                }
+                // TODO: all fields should be padded to align
+                return (is("isDirectory") ? "d" : (is("isSymbolicLink") ? "l" : (is("isOther") ? "o" : "-")))
+                        + PosixFilePermissions.toString(perms) + " "
+                        + String.format("%3s", (attributes.containsKey("nlink") ? attributes.get("nlink").toString() : "1"))
+                        + " " + username + " " + group + " " + lengthString + " "
+                        + toString((FileTime) attributes.get("lastModifiedTime"))
+                        + " " + display();
+            }
+
+            protected String toString(FileTime time) {
+                long millis = (time != null) ? time.toMillis() : -1L;
+                if (millis < 0L) {
+                    return "------------";
+                }
+                ZonedDateTime dt = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault());
+                // Less than six months
+                if (System.currentTimeMillis() - millis < 183L * 24L * 60L * 60L * 1000L) {
+                    return DateTimeFormatter.ofPattern("MMM ppd HH:mm").format(dt);
+                }
+                // Older than six months
+                else {
+                    return DateTimeFormatter.ofPattern("MMM ppd  yyyy").format(dt);
+                }
+            }
+
+            protected Map<String, Object> readAttributes(Path path) {
+                Map<String, Object>  attrs = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+                for (String view : path.getFileSystem().supportedFileAttributeViews()) {
+                    try {
+                        Map<String, Object> ta = Files.readAttributes(path, view + ":*",
+                                IoUtils.getLinkOptions(opt.isSet("L")));
+                        ta.entrySet().forEach(e -> attrs.putIfAbsent(e.getKey(), e.getValue()));
+                    } catch (IOException e) {
+                        // Ignore
+                    }
+                }
+                attrs.computeIfAbsent("isExecutable", s -> Files.isExecutable(path));
+                attrs.computeIfAbsent("permissions", s -> IoUtils.getPermissionsFromFile(path.toFile()));
+                return attrs;
+            }
         }
-        return files;
+
+        Path currentDir = session.currentDir();
+        // Listing
+        List<Path> expanded = new ArrayList<>();
+        if (opt.args().isEmpty()) {
+            expanded.add(currentDir);
+        } else {
+            opt.args().forEach(s -> expanded.add(currentDir.resolve(s)));
+        }
+        boolean listAll = opt.isSet("a");
+        Predicate<Path> filter = p -> listAll || !p.getFileName().toString().startsWith(".");
+        List<PathEntry> all = expanded.stream()
+                .filter(filter)
+                .map(p -> new PathEntry(p, currentDir))
+                .sorted()
+                .collect(Collectors.toList());
+        // Print files first
+        List<PathEntry> files = all.stream()
+                .filter(PathEntry::isNotDirectory)
+                .collect(Collectors.toList());
+        PrintStream out = System.out;
+        Consumer<Stream<PathEntry>> display = s -> {
+            // Comma separated list
+            if (opt.isSet("m")) {
+                out.println(s.map(PathEntry::display).collect(Collectors.joining(", ")));
+            }
+            // Long listing
+            else if (opt.isSet("l")) {
+                s.map(PathEntry::longDisplay).forEach(out::println);
+            }
+            // Column listing
+            else {
+                toColumn(session, out, s.map(PathEntry::display), opt.isSet("x"));
+            }
+        };
+        boolean space = false;
+        if (!files.isEmpty()) {
+            display.accept(files.stream());
+            space = true;
+        }
+        // Print directories
+        List<PathEntry> directories = all.stream()
+                .filter(PathEntry::isDirectory)
+                .collect(Collectors.toList());
+        for (PathEntry entry : directories) {
+            if (space) {
+                out.println();
+            }
+            space = true;
+            Path path = currentDir.resolve(entry.path);
+            if (expanded.size() > 1) {
+                out.println(currentDir.relativize(path).toString() + ":");
+            }
+            display.accept(Stream.concat(Arrays.asList(".", "..").stream().map(path::resolve), Files.list(path))
+                            .filter(filter)
+                            .map(p -> new PathEntry(p, path))
+                            .sorted()
+            );
+        }
     }
 
-    public void cat(CommandSession session, String[] argv) throws Exception {
+    private void toColumn(CommandSession session, PrintStream out, Stream<String> ansi, boolean horizontal) {
+        Terminal terminal = Shell.getTerminal(session);
+        int width = terminal.getWidth();
+        List<AttributedString> strings = ansi.map(AttributedString::fromAnsi).collect(Collectors.toList());
+        if (!strings.isEmpty()) {
+            int max = strings.stream().mapToInt(AttributedString::columnLength).max().getAsInt();
+            int c = Math.max(1, width / max);
+            while (c > 1 && c * max + (c - 1) >= width) {
+                c--;
+            }
+            int columns = c;
+            int lines = (strings.size() + columns - 1) / columns;
+            IntBinaryOperator index;
+            if (horizontal) {
+                index = (i, j) -> i * columns + j;
+            } else {
+                index = (i, j) -> j * lines + i;
+            }
+            AttributedStringBuilder sb = new AttributedStringBuilder();
+            for (int i = 0; i < lines; i++) {
+                for (int j = 0; j < columns; j++) {
+                    int idx = index.applyAsInt(i, j);
+                    if (idx < strings.size()) {
+                        AttributedString str = strings.get(idx);
+                        boolean hasRightItem = j < columns - 1 && index.applyAsInt(i, j + 1) < strings.size();
+                        sb.append(str);
+                        if (hasRightItem) {
+                            for (int k = 0; k <= max - str.length(); k++) {
+                                sb.append(' ');
+                            }
+                        }
+                    }
+                }
+                sb.append('\n');
+            }
+            out.print(sb.toAnsi());
+        }
+    }
+
+    protected void cat(CommandSession session, String[] argv) throws Exception {
         final String[] usage = {
                 "cat - concatenate and print FILES",
                 "Usage: cat [OPTIONS] [FILES]",
                 "  -? --help                show help",
                 "  -n                       number the output lines, starting at 1"
         };
-
-        Options opt = Options.compile(usage).parse(argv);
-
-        if (opt.isSet("help")) {
-            opt.usage(System.err);
-            return;
-        }
-
+        Options opt = parseOptions(session, usage, argv);
         List<String> args = opt.args();
         if (args.isEmpty()) {
             args = Collections.singletonList("-");
         }
-
         Path cwd = session.currentDir();
         for (String arg : args) {
             InputStream is;
             if ("-".equals(arg)) {
                 is = System.in;
-
             } else {
                 is = cwd.toUri().resolve(arg).toURL().openStream();
             }
@@ -375,21 +539,14 @@
         }
     }
 
-    public void echo(Object[] argv) {
+    protected void echo(CommandSession session, Object[] argv) throws Exception {
         final String[] usage = {
                 "echo - echoes or prints ARGUMENT to standard output",
                 "Usage: echo [OPTIONS] [ARGUMENTS]",
                 "  -? --help                show help",
                 "  -n                       no trailing new line"
         };
-
-        Options opt = Options.compile(usage).parse(argv);
-
-        if (opt.isSet("help")) {
-            opt.usage(System.err);
-            return;
-        }
-
+        Options opt = parseOptions(session, usage, argv);
         List<String> args = opt.args();
         StringBuilder buf = new StringBuilder();
         if (args != null) {
@@ -406,115 +563,195 @@
         }
     }
 
-    public boolean grep(CommandSession session, String[] argv) throws IOException {
+    protected void grep(CommandSession session, String[] argv) throws Exception {
         final String[] usage = {
                 "grep -  search for PATTERN in each FILE or standard input.",
                 "Usage: grep [OPTIONS] PATTERN [FILES]",
-                "  -? --help                show help",
-                "  -i --ignore-case         ignore case distinctions",
-                "  -n --line-number         prefix each line with line number within its input file",
-                "  -q --quiet, --silent     suppress all normal output",
-                "  -v --invert-match        select non-matching lines"};
-
-        Options opt = Options.compile(usage).parse(argv);
-
-        if (opt.isSet("help")) {
-            opt.usage(System.err);
-            return true;
-        }
-
+                "  -? --help                Show help",
+                "  -i --ignore-case         Ignore case distinctions",
+                "  -n --line-number         Prefix each line with line number within its input file",
+                "  -q --quiet, --silent     Suppress all normal output",
+                "  -v --invert-match        Select non-matching lines",
+                "  -w --word-regexp         Select only whole words",
+                "  -x --line-regexp         Select only whole lines",
+                "  -c --count               Only print a count of matching lines per file",
+                "     --color=WHEN          Use markers to distinguish the matching string, may be `always', `never' or `auto'",
+                "  -B --before-context=NUM  Print NUM lines of leading context before matching lines",
+                "  -A --after-context=NUM   Print NUM lines of trailing context after matching lines",
+                "  -C --context=NUM         Print NUM lines of output context"
+        };
+        Options opt = parseOptions(session, usage, argv);
         List<String> args = opt.args();
-
         if (args.isEmpty()) {
-            throw opt.usageError("no pattern supplied.");
+            throw new IllegalArgumentException("no pattern supplied");
         }
 
         String regex = args.remove(0);
+        String regexp = regex;
+        if (opt.isSet("word-regexp")) {
+            regexp = "\\b" + regexp + "\\b";
+        }
+        if (opt.isSet("line-regexp")) {
+            regexp = "^" + regexp + "$";
+        } else {
+            regexp = ".*" + regexp + ".*";
+        }
+        Pattern p;
+        Pattern p2;
         if (opt.isSet("ignore-case")) {
-            regex = "(?i)" + regex;
+            p = Pattern.compile(regexp, Pattern.CASE_INSENSITIVE);
+            p2 = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
+        } else {
+            p = Pattern.compile(regexp);
+            p2 = Pattern.compile(regex);
         }
-
-        if (args.isEmpty()) {
-            args.add(null);
+        int after = opt.isSet("after-context") ? opt.getNumber("after-context") : -1;
+        int before = opt.isSet("before-context") ? opt.getNumber("before-context") : -1;
+        int context = opt.isSet("context") ? opt.getNumber("context") : 0;
+        if (after < 0) {
+            after = context;
         }
-
-        StringBuilder buf = new StringBuilder();
-
-        if (args.size() > 1) {
-            buf.append("%1$s:");
+        if (before < 0) {
+            before = context;
         }
+        List<String> lines = new ArrayList<String>();
+        boolean invertMatch = opt.isSet("invert-match");
+        boolean lineNumber = opt.isSet("line-number");
+        boolean count = opt.isSet("count");
+        String color = opt.isSet("color") ? opt.get("color") : "auto";
 
-        if (opt.isSet("line-number")) {
-            buf.append("%2$s:");
+        List<Source> sources = new ArrayList<>();
+        if (opt.args().isEmpty()) {
+            opt.args().add("-");
         }
-
-        buf.append("%3$s");
-        String format = buf.toString();
-
-        Pattern pattern = Pattern.compile(regex);
-        boolean status = true;
-        boolean match = false;
-
-        for (String arg : args) {
-            InputStream in = null;
-
-            try {
-                Path cwd = session.currentDir();
-                in = (arg == null) ? System.in : cwd.resolve(arg).toUri().toURL().openStream();
-
-                BufferedReader rdr = new BufferedReader(new InputStreamReader(in));
-                int line = 0;
-                String s;
-                while ((s = rdr.readLine()) != null) {
-                    line++;
-                    Matcher matcher = pattern.matcher(s);
-                    if (!(matcher.find() ^ !opt.isSet("invert-match"))) {
-                        match = true;
-                        if (opt.isSet("quiet"))
-                            break;
-
-                        System.out.println(String.format(format, arg, line, s));
-                    }
-                }
-
-                if (match && opt.isSet("quiet")) {
-                    break;
-                }
-            } catch (IOException e) {
-                System.err.println("grep: " + e.getMessage());
-                status = false;
-            } finally {
-                if (arg != null && in != null) {
-                    in.close();
-                }
+        for (String arg : opt.args()) {
+            if ("-".equals(arg)) {
+                sources.add(new StdInSource());
+            } else {
+                sources.add(new URLSource(session.currentDir().resolve(arg).toUri().toURL(), arg));
             }
         }
-
-        return match && status;
+        boolean match = false;
+        for (Source source : sources) {
+            boolean firstPrint = true;
+            int nb = 0;
+            int lineno = 1;
+            String line;
+            int lineMatch = 0;
+            try (BufferedReader r = new BufferedReader(new InputStreamReader(source.read()))) {
+                while ((line = r.readLine()) != null) {
+                    if (line.length() == 1 && line.charAt(0) == '\n') {
+                        break;
+                    }
+                    if (p.matcher(line).matches() ^ invertMatch) {
+                        AttributedStringBuilder sbl = new AttributedStringBuilder();
+                        sbl.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.BLACK + AttributedStyle.BRIGHT));
+                        if (!count && sources.size() > 1) {
+                            sbl.append(source.getName());
+                            sbl.append(":");
+                        }
+                        if (!count && lineNumber) {
+                            sbl.append(String.format("%6d  ", lineno));
+                        }
+                        sbl.style(AttributedStyle.DEFAULT);
+                        Matcher matcher2 = p2.matcher(line);
+                        AttributedString aLine = AttributedString.fromAnsi(line);
+                        AttributedStyle style = AttributedStyle.DEFAULT;
+                        if (!invertMatch && !color.equalsIgnoreCase("never")) {
+                            style = style.bold().foreground(AttributedStyle.RED);
+                        }
+                        int cur = 0;
+                        while (matcher2.find()) {
+                            int index = matcher2.start(0);
+                            AttributedString prefix = aLine.subSequence(cur, index);
+                            sbl.append(prefix);
+                            cur = matcher2.end();
+                            sbl.append(aLine.subSequence(index, cur), style);
+                            nb++;
+                        }
+                        sbl.append(aLine.subSequence(cur, aLine.length()));
+                        lines.add(sbl.toAnsi(Shell.getTerminal(session)));
+                        lineMatch = lines.size();
+                    } else {
+                        if (lineMatch != 0 & lineMatch + after + before <= lines.size()) {
+                            if (!count) {
+                                if (!firstPrint && before + after > 0) {
+                                    System.out.println("--");
+                                } else {
+                                    firstPrint = false;
+                                }
+                                for (int i = 0; i < lineMatch + after; i++) {
+                                    System.out.println(lines.get(i));
+                                }
+                            }
+                            while (lines.size() > before) {
+                                lines.remove(0);
+                            }
+                            lineMatch = 0;
+                        }
+                        lines.add(line);
+                        while (lineMatch == 0 && lines.size() > before) {
+                            lines.remove(0);
+                        }
+                    }
+                    lineno++;
+                }
+                if (!count && lineMatch > 0) {
+                    if (!firstPrint && before + after > 0) {
+                        System.out.println("--");
+                    } else {
+                        firstPrint = false;
+                    }
+                    for (int i = 0; i < lineMatch + after && i < lines.size(); i++) {
+                        System.out.println(lines.get(i));
+                    }
+                }
+                if (count) {
+                    System.out.println(nb);
+                }
+                match |= nb > 0;
+            }
+        }
+        Pipe.error(match ? 0 : 1);
     }
 
-    public void sleep(String[] argv) throws InterruptedException {
+    protected void sleep(CommandSession session, String[] argv) throws Exception {
         final String[] usage = {
                 "sleep -  suspend execution for an interval of time",
                 "Usage: sleep seconds",
                 "  -? --help                    show help"};
 
-        Options opt = Options.compile(usage).parse(argv);
-
-        if (opt.isSet("help")) {
-            opt.usage(System.err);
-            return;
-        }
-
+        Options opt = parseOptions(session, usage, argv);
         List<String> args = opt.args();
         if (args.size() != 1) {
-            System.err.println("usage: sleep seconds");
+            throw new IllegalArgumentException("usage: sleep seconds");
         } else {
             int s = Integer.parseInt(args.get(0));
             Thread.sleep(s * 1000);
         }
     }
 
+    protected static void read(BufferedReader r, List<String> lines) throws IOException {
+        for (String s = r.readLine(); s != null; s = r.readLine()) {
+            lines.add(s);
+        }
+    }
+
+    private static void cat(final BufferedReader reader, boolean displayLineNumbers) throws IOException {
+        String line;
+        int lineno = 1;
+        try {
+            while ((line = reader.readLine()) != null) {
+                if (displayLineNumbers) {
+                    System.out.print(String.format("%6d  ", lineno++));
+                }
+                System.out.println(line);
+            }
+        } finally {
+            reader.close();
+        }
+    }
+
     public static class SortComparator implements Comparator<String> {
 
         private static Pattern fpPattern;
@@ -546,7 +783,7 @@
             this.ignoreBlanks = ignoreBlanks;
             this.numeric = numeric;
             if (sortFields == null || sortFields.size() == 0) {
-                sortFields = new ArrayList<String>();
+                sortFields = new ArrayList<>();
                 sortFields.add("1");
             }
             sortKeys = new ArrayList<Key>();
@@ -643,7 +880,7 @@
         }
 
         protected List<Integer> getFieldIndexes(String o) {
-            List<Integer> fields = new ArrayList<Integer>();
+            List<Integer> fields = new ArrayList<>();
             if (o.length() > 0) {
                 if (separator == '\0') {
                     int i = 0;
@@ -778,4 +1015,100 @@
         }
     }
 
+    private String[] expand(CommandSession session, String[] argv) throws IOException {
+        String reserved = "(?<!\\\\)[*(|<\\[?]";
+        List<String> params = new ArrayList<>();
+        for (String arg : argv) {
+            if (arg.matches(".*" + reserved + ".*")) {
+                String org = arg;
+                List<String> expanded = new ArrayList<>();
+                Path currentDir = session.currentDir();
+                Path dir;
+                String pfx = arg.replaceFirst(reserved + ".*", "");
+                String prefix;
+                if (pfx.indexOf('/') >= 0) {
+                    pfx = pfx.substring(0, pfx.lastIndexOf('/'));
+                    arg = arg.substring(pfx.length() + 1);
+                    dir = currentDir.resolve(pfx).normalize();
+                    prefix = pfx + "/";
+                } else {
+                    dir = currentDir;
+                    prefix = "";
+                }
+                PathMatcher matcher = dir.getFileSystem().getPathMatcher("glob:" + arg);
+                Files.walkFileTree(dir,
+                        EnumSet.of(FileVisitOption.FOLLOW_LINKS),
+                        Integer.MAX_VALUE,
+                        new FileVisitor<Path>() {
+                    @Override
+                    public FileVisitResult preVisitDirectory(Path file, BasicFileAttributes attrs) throws IOException {
+                        if (file.equals(dir)) {
+                            return FileVisitResult.CONTINUE;
+                        }
+                        if (Files.isHidden(file)) {
+                            return FileVisitResult.SKIP_SUBTREE;
+                        }
+                        Path r = dir.relativize(file);
+                        if (matcher.matches(r)) {
+                            expanded.add(prefix + r.toString());
+                        }
+                        return FileVisitResult.CONTINUE;
+                    }
+                    @Override
+                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                        if (!Files.isHidden(file)) {
+                            Path r = dir.relativize(file);
+                            if (matcher.matches(r)) {
+                                expanded.add(prefix + r.toString());
+                            }
+                        }
+                        return FileVisitResult.CONTINUE;
+                    }
+                    @Override
+                    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
+                        return FileVisitResult.CONTINUE;
+                    }
+                    @Override
+                    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+                        return FileVisitResult.CONTINUE;
+                    }
+                });
+                Collections.sort(expanded);
+                if (expanded.isEmpty()) {
+                    throw new IOException("no matches found: " + org);
+                }
+                params.addAll(expanded);
+            } else {
+                params.add(arg);
+            }
+        }
+        return params.toArray(new String[params.size()]);
+    }
+
+    private Map<String, String> getColorMap(CommandSession session, String name, String def) {
+        Object obj = session.get(name + "_COLORS");
+        String str = obj != null ? obj.toString() : null;
+        if (str == null || !str.matches("[a-z]{2}=[0-9]+(;[0-9]+)*(:[a-z]{2}=[0-9]+(;[0-9]+)*)*")) {
+            str = def;
+        }
+        return Arrays.stream(str.split(":"))
+                .collect(Collectors.toMap(s -> s.substring(0, s.indexOf('=')),
+                                          s -> s.substring(s.indexOf('=') + 1)));
+    }
+
+    private Options parseOptions(CommandSession session, String[] usage, Object[] argv) throws Exception {
+        Options opt = Options.compile(usage, s -> get(session, s)).parse(argv, true);
+        if (opt.isSet("help")) {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            opt.usage(new PrintStream(baos));
+            throw new HelpException(baos.toString());
+        }
+        return opt;
+    }
+
+    private String get(CommandSession session, String name) {
+        Object o = session.get(name);
+        return o != null ? o.toString() : null;
+    }
+
 }
diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Shell.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Shell.java
index 77febe8..d5a9a3c 100644
--- a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Shell.java
+++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Shell.java
@@ -68,6 +68,7 @@
     public static final String VAR_PROCESSOR = ".processor";
     public static final String VAR_TERMINAL = ".terminal";
     public static final String VAR_EXCEPTION = "exception";
+    public static final String VAR_RESULT = "_";
     public static final String VAR_LOCATION = ".location";
     public static final String VAR_PROMPT = "prompt";
     public static final String VAR_RPROMPT = "rprompt";
@@ -229,8 +230,7 @@
             throw opt.usageError("option --command requires argument(s)");
         }
 
-        CommandSession newSession = (login ? session : processor.createSession(
-                session.getKeyboard(), session.getConsole(), System.err));
+        CommandSession newSession = (login ? session : processor.createSession(session));
 
         if (opt.isSet("xtrace")) {
             newSession.put("echo", true);
@@ -251,7 +251,7 @@
         newSession.put("#TERM", (Function) (s, arguments) -> terminal.getType());
         newSession.put("#COLUMNS", (Function) (s, arguments) -> terminal.getWidth());
         newSession.put("#LINES", (Function) (s, arguments) -> terminal.getHeight());
-        newSession.put("#CWD", (Function) (s, arguments) -> s.currentDir().toString());
+        newSession.put("#PWD", (Function) (s, arguments) -> s.currentDir().toString());
 
         LineReader reader = null;
         if (args.isEmpty() && interactive) {
@@ -294,7 +294,7 @@
                         }
                         try {
                             result = session.execute(((ParsedLineImpl) parsedLine).program());
-                            session.put("_", result); // set $_ to last result
+                            session.put(Shell.VAR_RESULT, result); // set $_ to last result
 
                             if (result != null && !Boolean.FALSE.equals(session.get(".Gogo.format"))) {
                                 System.out.println(session.format(result, Converter.INSPECT));
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 7e1cc89..2a63c11 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
@@ -31,6 +31,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
 
 import org.apache.felix.gogo.runtime.Parser.Array;
 import org.apache.felix.gogo.runtime.Parser.Executable;
@@ -39,6 +41,7 @@
 import org.apache.felix.gogo.runtime.Parser.Program;
 import org.apache.felix.gogo.runtime.Parser.Sequence;
 import org.apache.felix.gogo.runtime.Parser.Statement;
+import org.apache.felix.gogo.runtime.Pipe.Result;
 import org.apache.felix.service.command.CommandSession;
 import org.apache.felix.service.command.Function;
 
@@ -46,9 +49,10 @@
 {
 
     public static final String LOCATION = ".location";
+    public static final String PIPE_EXCEPTION = "pipe-exception";
     private static final String DEFAULT_LOCK = ".defaultLock";
 
-    private static final ThreadLocal<String> location = new ThreadLocal<String>();
+    private static final ThreadLocal<String> location = new ThreadLocal<>();
 
     private final CommandSessionImpl session;
     private final Closure parent;
@@ -178,51 +182,45 @@
             }
         }
 
-        Pipe last = null;
+        Result last = null;
         Operator operator = null;
         for (Iterator<Executable> iterator = program.tokens().iterator(); iterator.hasNext();)
         {
-            if (operator != null) {
-                if (Token.eq("&&", operator)) {
-                    if (!isSuccess(last)) {
-                        continue;
-                    }
-                }
-                else if (Token.eq("||", operator)) {
-                    if (isSuccess(last)) {
-                        continue;
-                    }
-                }
-            }
+            Operator prevOperator = operator;
             Executable executable = iterator.next();
             if (iterator.hasNext()) {
                 operator = (Operator) iterator.next();
             } else {
                 operator = null;
             }
-
-            if (operator != null && Token.eq("&", operator)) {
-                // TODO: need to start in background
+            if (prevOperator != null) {
+                if (Token.eq("&&", prevOperator)) {
+                    if (!last.isSuccess()) {
+                        continue;
+                    }
+                }
+                else if (Token.eq("||", prevOperator)) {
+                    if (last.isSuccess()) {
+                        continue;
+                    }
+                }
             }
 
-            Channel[] mark = Pipe.mark();
             Channel[] streams;
             boolean[] toclose = new boolean[10];
-            if (mark == null) {
-                streams = new Channel[10];
-                streams[0] = Channels.newChannel(session.in);
-                streams[1] = Channels.newChannel(session.out);
-                streams[2] = Channels.newChannel(session.err);
+            if (Pipe.getCurrentPipe() != null) {
+                streams = Pipe.getCurrentPipe().streams.clone();
             } else {
-                streams = mark.clone();
+                streams = new Channel[10];
+                System.arraycopy(session.channels, 0, streams, 0, 3);
             }
 
-            List<Pipe> pipes = new ArrayList<Pipe>();
+            List<Pipe> pipes = new ArrayList<>();
             if (executable instanceof Pipeline) {
                 Pipeline pipeline = (Pipeline) executable;
                 List<Executable> exec = pipeline.tokens();
                 for (int i = 0; i < exec.size(); i++) {
-                    Executable ex = exec.get(i);
+                    Statement ex = (Statement) exec.get(i);
                     Operator op = i < exec.size() - 1 ? (Operator) exec.get(++i) : null;
                     Channel[] nstreams;
                     boolean[] ntoclose;
@@ -253,57 +251,53 @@
                     pipes.add(new Pipe(this, ex, nstreams, ntoclose));
                 }
             } else {
-                pipes.add(new Pipe(this, executable, streams, toclose));
+                pipes.add(new Pipe(this, (Statement) executable, streams, toclose));
             }
 
-            // Don't start a thread if we have a single pipe
-            if (pipes.size() == 1)
-            {
-                pipes.get(0).run();
-            }
-            else {
-                // Start threads
+            // Start pipe in background
+            if (operator != null && Token.eq("&", operator)) {
+
                 for (Pipe pipe : pipes) {
-                    pipe.start();
+                    session().getExecutor().submit(pipe);
                 }
-                // Wait for them
-                try {
-                    for (Pipe pipe : pipes) {
-                        pipe.join();
-                    }
-                } catch (InterruptedException e) {
-                    for (Pipe pipe : pipes) {
-                        pipe.interrupt();
-                    }
-                    throw e;
-                }
-            }
 
-            for (int i = 0; i < pipes.size() - 1; i++)
-            {
-                Pipe pipe = pipes.get(i);
-                if (pipe.exception != null)
-                {
-                    // can't throw exception, as result is defined by last pipe
-                    session.put("pipe-exception", pipe.exception);
-                }
-            }
-            last = pipes.get(pipes.size() - 1);
+                last = new Result((Object) null);
 
-            boolean errReturn = true;
-            if (last.exit != 0 && errReturn)
-            {
-                throw last.exception;
+            }
+            // Start in foreground and wait for results
+            else {
+                List<Future<Result>> results = session().getExecutor().invokeAll(pipes);
+
+                // Get pipe exceptions
+                Exception pipeException = null;
+                for (int i = 0; i < results.size() - 1; i++) {
+                    Future<Result> future = results.get(i);
+                    Throwable e;
+                    try {
+                        Result r = future.get();
+                        e = r.exception;
+                    } catch (ExecutionException ee) {
+                        e = ee.getCause();
+                    }
+                    if (e != null) {
+                        if (pipeException == null) {
+                            pipeException = new Exception("Exception caught during pipe execution");
+                        }
+                        pipeException.addSuppressed(e);
+                    }
+                }
+                session.put(PIPE_EXCEPTION, pipeException);
+
+                last = results.get(results.size() - 1).get();
+                if (last.exception != null) {
+                    throw last.exception;
+                }
             }
         }
 
         return last == null ? null : last.result;
     }
 
-    private boolean isSuccess(Pipe pipe) {
-        return pipe.exit == 0;
-    }
-
     private Object eval(Object v)
     {
         String s = v.toString();
@@ -368,7 +362,7 @@
         }
         else if (executable instanceof Sequence)
         {
-            return new Closure(session, this, ((Sequence) executable).program()).execute(new ArrayList<Object>());
+            return new Closure(session, this, ((Sequence) executable).program()).execute(new ArrayList<>());
         }
         else
         {
@@ -393,7 +387,7 @@
         {
             // set -x execution trace
             xtrace = "+" + statement;
-            session.err.println(xtrace);
+            session.perr.println(xtrace);
         }
 
         List<Token> tokens = statement.tokens();
@@ -402,7 +396,7 @@
             return null;
         }
 
-        List<Object> values = new ArrayList<Object>();
+        List<Object> values = new ArrayList<>();
         errTok = tokens.get(0);
 
         if ((tokens.size() > 3) && Token.eq("=", tokens.get(1)))
@@ -496,7 +490,7 @@
 
             if (!trace2.equals(trace1))
             {
-                session.err.println("+" + trace2);
+                session.perr.println("+" + trace2);
             }
         }
     }
@@ -567,7 +561,7 @@
         if (dot)
         {
             Object target = cmd;
-            ArrayList<Object> args = new ArrayList<Object>();
+            ArrayList<Object> args = new ArrayList<>();
             values.remove(0);
 
             for (Object arg : values)
@@ -621,7 +615,7 @@
 
         if (list != null)
         {
-            List<Object> olist = new ArrayList<Object>();
+            List<Object> olist = new ArrayList<>();
             for (Token t : list)
             {
                 Object oval = eval(t);
@@ -638,7 +632,7 @@
         }
         else
         {
-            Map<Object, Object> omap = new LinkedHashMap<Object, Object>();
+            Map<Object, Object> omap = new LinkedHashMap<>();
             for (Entry<Token, Token> e : map.entrySet())
             {
                 Token key = e.getKey();
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 c8fcd3a..8f07108 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
@@ -19,8 +19,10 @@
 package org.apache.felix.gogo.runtime;
 
 import java.io.InputStream;
-import java.io.PrintStream;
+import java.io.OutputStream;
 import java.lang.reflect.Method;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -42,12 +44,12 @@
 
 public class CommandProcessorImpl implements CommandProcessor
 {
-    protected final Set<Converter> converters = new CopyOnWriteArraySet<Converter>();
-    protected final Set<CommandSessionListener> listeners = new CopyOnWriteArraySet<CommandSessionListener>();
-    protected final ConcurrentMap<String, Map<Object, Integer>> commands = new ConcurrentHashMap<String, Map<Object, Integer>>();
-    protected final Map<String, Object> constants = new ConcurrentHashMap<String, Object>();
+    protected final Set<Converter> converters = new CopyOnWriteArraySet<>();
+    protected final Set<CommandSessionListener> listeners = new CopyOnWriteArraySet<>();
+    protected final ConcurrentMap<String, Map<Object, Integer>> commands = new ConcurrentHashMap<>();
+    protected final Map<String, Object> constants = new ConcurrentHashMap<>();
     protected final ThreadIO threadIO;
-    protected final WeakHashMap<CommandSession, Object> sessions = new WeakHashMap<CommandSession, Object>();
+    protected final WeakHashMap<CommandSession, Object> sessions = new WeakHashMap<>();
     protected boolean stopped;
 
     public CommandProcessorImpl(ThreadIO tio)
@@ -55,7 +57,37 @@
         threadIO = tio;
     }
 
-    public CommandSession createSession(InputStream in, PrintStream out, PrintStream err)
+    @Override
+    public CommandSessionImpl createSession(CommandSession parent) {
+        synchronized (sessions) {
+            if (stopped)
+            {
+                throw new IllegalStateException("CommandProcessor has been stopped");
+            }
+            if (!sessions.containsKey(parent) || !(parent instanceof CommandSessionImpl)) {
+                throw new IllegalArgumentException();
+            }
+            CommandSessionImpl session = new CommandSessionImpl(this, (CommandSessionImpl) parent);
+            sessions.put(session, null);
+            return session;
+        }
+    }
+
+    public CommandSessionImpl createSession(ReadableByteChannel in, WritableByteChannel out, WritableByteChannel err)
+    {
+        synchronized (sessions)
+        {
+            if (stopped)
+            {
+                throw new IllegalStateException("CommandProcessor has been stopped");
+            }
+            CommandSessionImpl session = new CommandSessionImpl(this, in, out, err);
+            sessions.put(session, null);
+            return session;
+        }
+    }
+
+    public CommandSessionImpl createSession(InputStream in, OutputStream out, OutputStream err)
     {
         synchronized (sessions)
         {
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandSessionImpl.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandSessionImpl.java
index cc38c8d..f914a23 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandSessionImpl.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/CommandSessionImpl.java
@@ -22,9 +22,14 @@
 package org.apache.felix.gogo.runtime;
 
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.PrintStream;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.nio.channels.Channel;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Arrays;
@@ -37,6 +42,8 @@
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 import org.apache.felix.service.command.CommandProcessor;
 import org.apache.felix.service.command.CommandSession;
@@ -52,23 +59,62 @@
     public static final String CONSTANTS = ".constants";
     private static final String COLUMN = "%-20s %s\n";
 
+    // Streams and channels
     protected InputStream in;
-    protected PrintStream out;
-    PrintStream err;
+    protected OutputStream out;
+    protected PrintStream pout;
+    protected OutputStream err;
+    protected PrintStream perr;
+    protected Channel[] channels;
 
     private final CommandProcessorImpl processor;
-    protected final ConcurrentMap<String, Object> variables = new ConcurrentHashMap<String, Object>();
+    protected final ConcurrentMap<String, Object> variables = new ConcurrentHashMap<>();
     private volatile boolean closed;
 
+    private final ExecutorService executor;
+
     private Path currentDir;
 
-    protected CommandSessionImpl(CommandProcessorImpl shell, InputStream in, PrintStream out, PrintStream err)
+    protected CommandSessionImpl(CommandProcessorImpl shell, CommandSessionImpl parent)
     {
+        this.currentDir = parent.currentDir;
+        this.executor = Executors.newCachedThreadPool();
         this.processor = shell;
+        this.channels = parent.channels;
+        this.in = parent.in;
+        this.out = parent.out;
+        this.err = parent.err;
+        this.pout = parent.pout;
+        this.perr = parent.perr;
+    }
+
+    protected CommandSessionImpl(CommandProcessorImpl shell, ReadableByteChannel in, WritableByteChannel out, WritableByteChannel err)
+    {
+        this.currentDir = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
+        this.executor = Executors.newCachedThreadPool();
+        this.processor = shell;
+        this.channels = new Channel[] { in, out, err };
+        this.in = Channels.newInputStream(in);
+        this.out = Channels.newOutputStream(out);
+        this.err = out == err ? this.out : Channels.newOutputStream(err);
+        this.pout = new PrintStream(this.out, true);
+        this.perr = out == err ? pout : new PrintStream(this.err, true);
+    }
+
+    protected CommandSessionImpl(CommandProcessorImpl shell, InputStream in, OutputStream out, OutputStream err)
+    {
+        this.currentDir = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
+        this.executor = Executors.newCachedThreadPool();
+        this.processor = shell;
+        ReadableByteChannel inCh = Channels.newChannel(in);
+        WritableByteChannel outCh = Channels.newChannel(out);
+        WritableByteChannel errCh = out == err ? outCh : Channels.newChannel(err);
+        this.channels = new Channel[] { inCh, outCh, errCh };
         this.in = in;
         this.out = out;
         this.err = err;
-        this.currentDir = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
+        this.pout = out instanceof PrintStream ? (PrintStream) out : new PrintStream(out, true);
+        this.perr = out == err ? pout : err instanceof PrintStream ? (PrintStream) err : new PrintStream(err, true);
     }
 
     ThreadIO threadIO()
@@ -98,11 +144,16 @@
     {
         if (!this.closed)
         {
-            this.processor.closeSession(this);
             this.closed = true;
+            this.processor.closeSession(this);
+            executor.shutdownNow();
         }
     }
 
+    ExecutorService getExecutor() {
+        return executor;
+    }
+
     public Object execute(CharSequence commandline) throws Exception
     {
         assert processor != null;
@@ -199,7 +250,7 @@
 
     public PrintStream getConsole()
     {
-        return out;
+        return pout;
     }
 
     @SuppressWarnings("unchecked")
diff --git a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Parser.java b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Parser.java
index de317fc..fdf8fc1 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Parser.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/gogo/runtime/Parser.java
@@ -205,7 +205,7 @@
         int start = tz.index - 1;
         while (true)
         {
-            Executable ex;
+            Statement ex;
             Token t = next();
             if (t == null)
             {
@@ -260,7 +260,7 @@
             {
                 if (pipes == null)
                 {
-                    pipes = new ArrayList<Executable>();
+                    pipes = new ArrayList<>();
                 }
                 pipes.add(ex);
                 pipes.add(new Operator(t));
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 6b01470..e92b329 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
@@ -33,46 +33,81 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import org.apache.felix.gogo.runtime.Parser.Executable;
 import org.apache.felix.gogo.runtime.Parser.Statement;
+import org.apache.felix.gogo.runtime.Pipe.Result;
 import org.apache.felix.service.command.Converter;
 
-public class Pipe extends Thread
+public class Pipe implements Callable<Result>
 {
-    static final ThreadLocal<Channel[]> tStreams = new ThreadLocal<Channel[]>();
+    private static final ThreadLocal<Pipe> CURRENT = new ThreadLocal<>();
 
-    public static Channel[] mark() {
-        return tStreams.get();
+    public static class Result {
+        public final Object result;
+        public final Exception exception;
+        public final int error;
+
+        public Result(Object result) {
+            this.result = result;
+            this.exception = null;
+            this.error = 0;
+        }
+
+        public Result(Exception exception) {
+            this.result = null;
+            this.exception = exception;
+            this.error = 1;
+        }
+
+        public Result(int error) {
+            this.result = null;
+            this.exception = null;
+            this.error = error;
+        }
+
+        public boolean isSuccess() {
+            return exception == null && error == 0;
+        }
     }
 
-    public static void reset(Channel[] streams) {
-        tStreams.set(streams);
+    public static Pipe getCurrentPipe() {
+        return CURRENT.get();
+    }
+
+    public static void error(int error) {
+        Pipe current = getCurrentPipe();
+        if (current != null) {
+            current.error = error;
+        }
+    }
+
+    private static Pipe setCurrentPipe(Pipe pipe) {
+        Pipe previous = CURRENT.get();
+        CURRENT.set(pipe);
+        return previous;
     }
 
     final Closure closure;
-    final Executable executable;
+    final Statement statement;
     final Channel[] streams;
     final boolean[] toclose;
-    Object result;
-    Exception exception;
-    int exit = 0;
+    int error;
 
-    public Pipe(Closure closure, Executable executable, Channel[] streams, boolean[] toclose)
+    public Pipe(Closure closure, Statement statement, Channel[] streams, boolean[] toclose)
     {
-        super("pipe-" + executable);
         this.closure = closure;
-        this.executable = executable;
+        this.statement = statement;
         this.streams = streams;
         this.toclose = toclose;
     }
 
     public String toString()
     {
-        return "pipe<" + executable + "> out=" + streams[1];
+        return "pipe<" + statement + "> out=" + streams[1];
     }
 
     private static final int READ = 1;
@@ -148,8 +183,8 @@
     }
 
     private static class MultiChannel<T extends Channel> implements Channel {
-        protected final List<T> channels = new ArrayList<T>();
-        protected final List<T> toClose = new ArrayList<T>();
+        protected final List<T> channels = new ArrayList<>();
+        protected final List<T> toClose = new ArrayList<>();
         protected final AtomicBoolean opened = new AtomicBoolean(true);
         public void addChannel(T channel, boolean toclose) {
             channels.add(channel);
@@ -200,14 +235,30 @@
         }
     }
 
-    public void run()
+    @Override
+    public Result call() throws Exception {
+        Thread thread = Thread.currentThread();
+        String name = thread.getName();
+        try {
+            thread.setName("pipe-" + statement);
+            return doCall();
+        } finally {
+            thread.setName(name);
+        }
+    }
+
+    public Result doCall()
     {
-        InputStream in = null;
+        InputStream in;
         PrintStream out = null;
         PrintStream err = null;
-        WritableByteChannel errChannel = (WritableByteChannel) streams[2];
 
-        Channel[] prevStreams = tStreams.get();
+        // The errChannel will be used to print errors to the error stream
+        // Before the command is actually executed (i.e. during the initialization,
+        // including the redirection processing), it will be the original error stream.
+        // This value may be modified by redirections and the redirected error stream
+        // will be effective just before actually running the command.
+        WritableByteChannel errChannel = (WritableByteChannel) streams[2];
 
         // TODO: not sure this is the correct way
         boolean begOfPipe = !toclose[0];
@@ -215,98 +266,101 @@
 
         try
         {
-            if (executable instanceof Statement) {
-                Statement statement = (Statement) executable;
-                List<Token> tokens = statement.redirections();
-                for (int i = 0; i < tokens.size(); i++) {
-                    Token t = tokens.get(i);
-                    Matcher m;
-                    if ((m = Pattern.compile("(?:([0-9])?|(&)?)>(>)?").matcher(t)).matches()) {
-                        int fd;
-                        if (m.group(1) != null) {
-                            fd = Integer.parseInt(m.group(1));
-                        }
-                        else if (m.group(2) != null) {
-                            fd = -1; // both 1 and 2
-                        } else {
-                            fd = 1;
-                        }
-                        boolean append = m.group(3) != null;
-                        Token file = tokens.get(++i);
-                        Path outPath = closure.session().currentDir().resolve(file.toString());
-                        Set<StandardOpenOption> options = new HashSet<StandardOpenOption>();
-                        options.add(StandardOpenOption.WRITE);
-                        options.add(StandardOpenOption.CREATE);
-                        if (append) {
-                            options.add(StandardOpenOption.APPEND);
-                        } else {
-                            options.add(StandardOpenOption.TRUNCATE_EXISTING);
-                        }
-                        Channel ch = Files.newByteChannel(outPath, options);
-                        if (fd >= 0) {
-                            setStream(ch, fd, WRITE, begOfPipe, endOfPipe);
-                        } else {
-                            setStream(ch, 1, WRITE, begOfPipe, endOfPipe);
-                            setStream(ch, 2, WRITE, begOfPipe, endOfPipe);
-                        }
+            List<Token> tokens = statement.redirections();
+            for (int i = 0; i < tokens.size(); i++) {
+                Token t = tokens.get(i);
+                Matcher m;
+                if ((m = Pattern.compile("(?:([0-9])?|(&)?)>(>)?").matcher(t)).matches()) {
+                    int fd;
+                    if (m.group(1) != null) {
+                        fd = Integer.parseInt(m.group(1));
                     }
-                    else if ((m = Pattern.compile("([0-9])?>&([0-9])").matcher(t)).matches()) {
-                        int fd0 = 1;
-                        if (m.group(1) != null) {
-                            fd0 = Integer.parseInt(m.group(1));
-                        }
-                        int fd1 = Integer.parseInt(m.group(2));
-                        if (streams[fd0] != null && toclose[fd0]) {
-                            streams[fd0].close();
-                        }
-                        streams[fd0] = streams[fd1];
-                        // TODO: this is wrong, we should keep a counter somehow so that the
-                        // stream is closed when both are closed
-                        toclose[fd0] = false;
+                    else if (m.group(2) != null) {
+                        fd = -1; // both 1 and 2
+                    } else {
+                        fd = 1;
                     }
-                    else if ((m = Pattern.compile("([0-9])?<(>)?").matcher(t)).matches()) {
-                        int fd = 0;
-                        if (m.group(1) != null) {
-                            fd = Integer.parseInt(m.group(1));
-                        }
-                        boolean output = m.group(2) != null;
-                        Token file = tokens.get(++i);
-                        Path inPath = closure.session().currentDir().resolve(file.toString());
-                        Set<StandardOpenOption> options = new HashSet<StandardOpenOption>();
-                        options.add(StandardOpenOption.READ);
-                        if (output) {
-                            options.add(StandardOpenOption.WRITE);
-                            options.add(StandardOpenOption.CREATE);
-                        }
-                        Channel ch = Files.newByteChannel(inPath, options);
-                        setStream(ch, fd, READ + (output ? WRITE : 0), begOfPipe, endOfPipe);
+                    boolean append = m.group(3) != null;
+                    Token file = tokens.get(++i);
+                    Path outPath = closure.session().currentDir().resolve(file.toString());
+                    Set<StandardOpenOption> options = new HashSet<>();
+                    options.add(StandardOpenOption.WRITE);
+                    options.add(StandardOpenOption.CREATE);
+                    if (append) {
+                        options.add(StandardOpenOption.APPEND);
+                    } else {
+                        options.add(StandardOpenOption.TRUNCATE_EXISTING);
+                    }
+                    Channel ch = Files.newByteChannel(outPath, options);
+                    if (fd >= 0) {
+                        setStream(ch, fd, WRITE, begOfPipe, endOfPipe);
+                    } else {
+                        setStream(ch, 1, WRITE, begOfPipe, endOfPipe);
+                        setStream(ch, 2, WRITE, begOfPipe, endOfPipe);
                     }
                 }
-            } else {
-                new UnsupportedOperationException("what to do ?").printStackTrace();
+                else if ((m = Pattern.compile("([0-9])?>&([0-9])").matcher(t)).matches()) {
+                    int fd0 = 1;
+                    if (m.group(1) != null) {
+                        fd0 = Integer.parseInt(m.group(1));
+                    }
+                    int fd1 = Integer.parseInt(m.group(2));
+                    if (streams[fd0] != null && toclose[fd0]) {
+                        streams[fd0].close();
+                    }
+                    streams[fd0] = streams[fd1];
+                    // TODO: this is wrong, we should keep a counter somehow so that the
+                    // stream is closed when both are closed
+                    toclose[fd0] = false;
+                }
+                else if ((m = Pattern.compile("([0-9])?<(>)?").matcher(t)).matches()) {
+                    int fd = 0;
+                    if (m.group(1) != null) {
+                        fd = Integer.parseInt(m.group(1));
+                    }
+                    boolean output = m.group(2) != null;
+                    Token file = tokens.get(++i);
+                    Path inPath = closure.session().currentDir().resolve(file.toString());
+                    Set<StandardOpenOption> options = new HashSet<>();
+                    options.add(StandardOpenOption.READ);
+                    if (output) {
+                        options.add(StandardOpenOption.WRITE);
+                        options.add(StandardOpenOption.CREATE);
+                    }
+                    Channel ch = Files.newByteChannel(inPath, options);
+                    setStream(ch, fd, READ + (output ? WRITE : 0), begOfPipe, endOfPipe);
+                }
             }
 
-            tStreams.set(streams);
-
+            // Create streams
             in = Channels.newInputStream((ReadableByteChannel) streams[0]);
             out = new PrintStream(Channels.newOutputStream((WritableByteChannel) streams[1]), true);
             err = new PrintStream(Channels.newOutputStream((WritableByteChannel) streams[2]), true);
+            // Change the error stream to the redirected one, now that
+            // the command is about to be executed.
             errChannel = (WritableByteChannel) streams[2];
 
             closure.session().threadIO().setStreams(in, out, err);
 
-            result = closure.execute(executable);
-            // We don't print the result if toclose[1] == false, which means we're at the end of the pipe
-            if (result != null && !endOfPipe && !Boolean.FALSE.equals(closure.session().get(".FormatPipe"))) {
-                out.println(closure.session().format(result, Converter.INSPECT));
+            Pipe previous = setCurrentPipe(this);
+            try {
+                Object result = closure.execute(statement);
+                // If an error has been set
+                if (error != 0) {
+                    return new Result(error);
+                }
+                // We don't print the result if we're at the end of the pipe
+                if (result != null && !endOfPipe && !Boolean.FALSE.equals(closure.session().get(".FormatPipe"))) {
+                    out.println(closure.session().format(result, Converter.INSPECT));
+                }
+                return new Result(result);
+
+            } finally {
+                setCurrentPipe(previous);
             }
         }
         catch (Exception e)
         {
-            exception = e;
-            if (exit == 0) {
-                exit = 1; // failure
-            }
             // TODO: use shell name instead of 'gogo'
             // TODO: use color if not redirected
             // TODO: use conversion ?
@@ -316,6 +370,7 @@
             } catch (IOException ioe) {
                 e.addSuppressed(ioe);
             }
+            return new Result(e);
         }
         finally
         {
@@ -327,8 +382,6 @@
             }
             closure.session().threadIO().close();
 
-            tStreams.set(prevStreams);
-
             try
             {
                 for (int i = 0; i < 10; i++) {
diff --git a/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandProcessor.java b/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandProcessor.java
index 2d77053..b602ade 100644
--- a/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandProcessor.java
+++ b/gogo/runtime/src/main/java/org/apache/felix/service/command/CommandProcessor.java
@@ -19,7 +19,7 @@
 package org.apache.felix.service.command;
 
 import java.io.InputStream;
-import java.io.PrintStream;
+import java.io.OutputStream;
 
 /**
  * A command shell can create and maintain a number of command sessions.
@@ -59,5 +59,7 @@
      * @param err The stream used for System.err
      * @return A new session.
      */
-    CommandSession createSession(InputStream in, PrintStream out, PrintStream err);
+    CommandSession createSession(InputStream in, OutputStream out, OutputStream err);
+
+    CommandSession createSession(CommandSession parent);
 }