8141209: $EXEC should allow streaming
authorjlaskey
Wed, 10 Feb 2016 11:18:02 -0400
changeset 35794 049749629dbe
parent 35793 b89ddacf3104
child 35795 5b9049223c34
8141209: $EXEC should allow streaming Reviewed-by: sundar
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/Global.java
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/CommandExecutor.java
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptingFunctions.java
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/resources/Messages.properties
nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/tools/Shell.java
nashorn/test/script/basic/JDK-8141209.js
nashorn/test/script/basic/JDK-8141209.js.EXPECTED
nashorn/test/script/trusted/JDK-8087292.js
nashorn/test/src/jdk/nashorn/internal/test/framework/TestFinder.java
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/Global.java	Tue Feb 09 14:14:06 2016 +0100
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/objects/Global.java	Wed Feb 10 11:18:02 2016 -0400
@@ -2459,8 +2459,6 @@
 
         final String execName = ScriptingFunctions.EXEC_NAME;
         value = ScriptFunction.createBuiltin(execName, ScriptingFunctions.EXEC);
-        value.addOwnProperty(ScriptingFunctions.THROW_ON_ERROR_NAME, Attribute.NOT_ENUMERABLE, false);
-
         addOwnProperty(execName, Attribute.NOT_ENUMERABLE, value);
 
         // Nashorn extension: global.echo (scripting-mode-only)
@@ -2474,10 +2472,10 @@
         addOwnProperty("$OPTIONS", Attribute.NOT_ENUMERABLE, options);
 
         // Nashorn extension: global.$ENV (scripting-mode-only)
+        final ScriptObject env = newObject();
         if (System.getSecurityManager() == null) {
             // do not fill $ENV if we have a security manager around
             // Retrieve current state of ENV variables.
-            final ScriptObject env = newObject();
             env.putAll(System.getenv(), scriptEnv._strict);
 
             // Some platforms, e.g., Windows, do not define the PWD environment
@@ -2486,11 +2484,8 @@
             if (!env.containsKey(ScriptingFunctions.PWD_NAME)) {
                 env.put(ScriptingFunctions.PWD_NAME, System.getProperty("user.dir"), scriptEnv._strict);
             }
-
-            addOwnProperty(ScriptingFunctions.ENV_NAME, Attribute.NOT_ENUMERABLE, env);
-        } else {
-            addOwnProperty(ScriptingFunctions.ENV_NAME, Attribute.NOT_ENUMERABLE, UNDEFINED);
         }
+        addOwnProperty(ScriptingFunctions.ENV_NAME, Attribute.NOT_ENUMERABLE, env);
 
         // add other special properties for exec support
         addOwnProperty(ScriptingFunctions.OUT_NAME, Attribute.NOT_ENUMERABLE, UNDEFINED);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/CommandExecutor.java	Wed Feb 10 11:18:02 2016 -0400
@@ -0,0 +1,845 @@
+/*
+ * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.nashorn.internal.runtime;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.StreamTokenizer;
+import java.io.StringReader;
+import java.lang.ProcessBuilder.Redirect;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static jdk.nashorn.internal.runtime.CommandExecutor.RedirectType.*;
+import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
+
+/**
+ * The CommandExecutor class provides support for Nashorn's $EXEC
+ * builtin function. CommandExecutor provides support for command parsing,
+ * I/O redirection, piping, completion timeouts, # comments, and simple
+ * environment variable management (cd, setenv, and unsetenv).
+ */
+class CommandExecutor {
+    // Size of byte buffers used for piping.
+    private static final int BUFFER_SIZE = 1024;
+
+    // Test to see if running on Windows.
+    private static final boolean IS_WINDOWS =
+        AccessController.doPrivileged((PrivilegedAction<Boolean>)() -> {
+        return System.getProperty("os.name").contains("Windows");
+    });
+
+    // User's home directory
+    private static final String HOME_DIRECTORY =
+        AccessController.doPrivileged((PrivilegedAction<String>)() -> {
+        return System.getProperty("user.home");
+    });
+
+    // Various types of standard redirects.
+    enum RedirectType {
+        NO_REDIRECT,
+        REDIRECT_INPUT,
+        REDIRECT_OUTPUT,
+        REDIRECT_OUTPUT_APPEND,
+        REDIRECT_ERROR,
+        REDIRECT_ERROR_APPEND,
+        REDIRECT_OUTPUT_ERROR_APPEND,
+        REDIRECT_ERROR_TO_OUTPUT
+    };
+
+    // Prefix strings to standard redirects.
+    private static final String[] redirectPrefixes = new String[] {
+        "<",
+        "0<",
+        ">",
+        "1>",
+        ">>",
+        "1>>",
+        "2>",
+        "2>>",
+        "&>",
+        "2>&1"
+    };
+
+    // Map from redirectPrefixes to RedirectType.
+    private static final RedirectType[] redirects = new RedirectType[] {
+        REDIRECT_INPUT,
+        REDIRECT_INPUT,
+        REDIRECT_OUTPUT,
+        REDIRECT_OUTPUT,
+        REDIRECT_OUTPUT_APPEND,
+        REDIRECT_OUTPUT_APPEND,
+        REDIRECT_ERROR,
+        REDIRECT_ERROR_APPEND,
+        REDIRECT_OUTPUT_ERROR_APPEND,
+        REDIRECT_ERROR_TO_OUTPUT
+    };
+
+    /**
+     * The RedirectInfo class handles checking the next token in a command
+     * to see if it contains a redirect.  If the redirect file does not butt
+     * against the prefix, then the next token is consumed.
+     */
+    private static class RedirectInfo {
+        // true if a redirect was encountered on the current command.
+        private boolean hasRedirects;
+        // Redirect.PIPE or an input redirect from the command line.
+        private Redirect inputRedirect;
+        // Redirect.PIPE or an output redirect from the command line.
+        private Redirect outputRedirect;
+        // Redirect.PIPE or an error redirect from the command line.
+        private Redirect errorRedirect;
+        // true if the error stream should be merged with output.
+        private boolean mergeError;
+
+        RedirectInfo() {
+            this.hasRedirects = false;
+            this.inputRedirect = Redirect.PIPE;
+            this.outputRedirect = Redirect.PIPE;
+            this.errorRedirect = Redirect.PIPE;
+            this.mergeError = false;
+        }
+
+        /**
+         * check - tests to see if the current token contains a redirect
+         * @param token    current command line token
+         * @param iterator current command line iterator
+         * @param cwd      current working directory
+         * @return true if token is consumed
+         */
+        boolean check(String token, final Iterator<String> iterator, final String cwd) {
+            // Iterate through redirect prefixes to file a match.
+            for (int i = 0; i < redirectPrefixes.length; i++) {
+               String prefix = redirectPrefixes[i];
+
+               // If a match is found.
+                if (token.startsWith(prefix)) {
+                    // Indicate we have at least one redirect (efficiency.)
+                    hasRedirects = true;
+                    // Map prefix to RedirectType.
+                    RedirectType redirect = redirects[i];
+                    // Strip prefix from token
+                    token = token.substring(prefix.length());
+
+                    // Get file from either current or next token.
+                    File file = null;
+                    if (redirect != REDIRECT_ERROR_TO_OUTPUT) {
+                        // Nothing left of current token.
+                        if (token.length() == 0) {
+                            if (iterator.hasNext()) {
+                                // Use next token.
+                                token = iterator.next();
+                            } else {
+                                // Send to null device if not provided.
+                                token = IS_WINDOWS ? "NUL:" : "/dev/null";
+                            }
+                        }
+
+                        // Redirect file.
+                        file = resolvePath(cwd, token).toFile();
+                    }
+
+                    // Define redirect based on prefix.
+                    switch (redirect) {
+                        case REDIRECT_INPUT:
+                            inputRedirect = Redirect.from(file);
+                            break;
+                        case REDIRECT_OUTPUT:
+                            outputRedirect = Redirect.to(file);
+                            break;
+                        case REDIRECT_OUTPUT_APPEND:
+                            outputRedirect = Redirect.appendTo(file);
+                            break;
+                        case REDIRECT_ERROR:
+                            errorRedirect = Redirect.to(file);
+                            break;
+                        case REDIRECT_ERROR_APPEND:
+                            errorRedirect = Redirect.appendTo(file);
+                            break;
+                        case REDIRECT_OUTPUT_ERROR_APPEND:
+                            outputRedirect = Redirect.to(file);
+                            errorRedirect = Redirect.to(file);
+                            mergeError = true;
+                            break;
+                        case REDIRECT_ERROR_TO_OUTPUT:
+                            mergeError = true;
+                            break;
+                        default:
+                            return false;
+                    }
+
+                    // Indicate token is consumed.
+                    return true;
+                }
+            }
+
+            // No redirect found.
+            return false;
+        }
+
+        /**
+         * apply - apply the redirects to the current ProcessBuilder.
+         * @param pb current ProcessBuilder
+         */
+        void apply(final ProcessBuilder pb) {
+            // Only if there was redirects (saves new structure in ProcessBuilder.)
+            if (hasRedirects) {
+                // If output and error are the same file then merge.
+                File outputFile = outputRedirect.file();
+                File errorFile = errorRedirect.file();
+
+                if (outputFile != null && outputFile.equals(errorFile)) {
+                    mergeError = true;
+                }
+
+                // Apply redirects.
+                pb.redirectInput(inputRedirect);
+                pb.redirectOutput(outputRedirect);
+                pb.redirectError(errorRedirect);
+                pb.redirectErrorStream(mergeError);
+            }
+        }
+    }
+
+    /**
+     * The Piper class is responsible for copying from an InputStream to an
+     * OutputStream without blocking the current thread.
+     */
+    private static class Piper implements java.lang.Runnable {
+        // Stream to copy from.
+        private final InputStream input;
+        // Stream to copy to.
+        private final OutputStream output;
+
+        Piper(final InputStream input, final OutputStream output) {
+            this.input = input;
+            this.output = output;
+        }
+
+        /**
+         * start - start the Piper in a new daemon thread
+         */
+        void start() {
+            Thread thread = new Thread(this, "$EXEC Piper");
+            thread.setDaemon(true);
+            thread.start();
+        }
+
+        /**
+         * run - thread action
+         */
+        @Override
+        public void run() {
+            try {
+                // Buffer for copying.
+                byte[] b = new byte[BUFFER_SIZE];
+                // Read from the InputStream until EOF.
+                int read;
+                while (-1 < (read = input.read(b, 0, b.length))) {
+                    // Write available date to OutputStream.
+                    output.write(b, 0, read);
+                }
+            } catch (Exception e) {
+                // Assume the worst.
+                throw new RuntimeException("Broken pipe", e);
+            } finally {
+                // Make sure the streams are closed.
+                try {
+                    input.close();
+                } catch (IOException e) {
+                    // Don't care.
+                }
+                try {
+                    output.close();
+                } catch (IOException e) {
+                    // Don't care.
+                }
+            }
+        }
+
+        // Exit thread.
+    }
+
+    // Process exit statuses.
+    static final int EXIT_SUCCESS  =  0;
+    static final int EXIT_FAILURE  =  1;
+
+    // Copy of environment variables used by all processes.
+    private  Map<String, String> environment;
+    // Input string if provided on CommandExecutor call.
+    private String inputString;
+    // Output string if required from CommandExecutor call.
+    private String outputString;
+    // Error string if required from CommandExecutor call.
+    private String errorString;
+    // Last process exit code.
+    private int exitCode;
+
+    // Input stream if provided on CommandExecutor call.
+    private InputStream inputStream;
+    // Output stream if provided on CommandExecutor call.
+    private OutputStream outputStream;
+    // Error stream if provided on CommandExecutor call.
+    private OutputStream errorStream;
+
+    // Ordered collection of current or piped ProcessBuilders.
+    private List<ProcessBuilder> processBuilders = new ArrayList<>();
+
+    CommandExecutor() {
+        this.environment = new HashMap<>();
+        this.inputString = "";
+        this.outputString = "";
+        this.errorString = "";
+        this.exitCode = EXIT_SUCCESS;
+        this.inputStream = null;
+        this.outputStream = null;
+        this.errorStream = null;
+        this.processBuilders = new ArrayList<>();
+    }
+
+    /**
+     * envVarValue - return the value of the environment variable key, or
+     * deflt if not found.
+     * @param key   name of environment variable
+     * @param deflt value to return if not found
+     * @return value of the environment variable
+     */
+    private String envVarValue(String key, String deflt) {
+        return environment.getOrDefault(key, deflt);
+    }
+
+    /**
+     * envVarLongValue - return the value of the environment variable key as a
+     * long value.
+     * @param key name of environment variable
+     * @return long value of the environment variable
+     */
+    private long envVarLongValue(String key) {
+        try {
+            return Long.parseLong(envVarValue(key, "0"));
+        } catch (NumberFormatException ex) {
+            return 0L;
+        }
+    }
+
+    /**
+     * envVarBooleanValue - return the value of the environment variable key as a
+     * boolean value.  true if the value was non-zero, false otherwise.
+     * @param key name of environment variable
+     * @return boolean value of the environment variable
+     */
+    private boolean envVarBooleanValue(String key) {
+        return envVarLongValue(key) != 0;
+    }
+
+    /**
+     * stripQuotes - strip quotes from token if present. Quoted tokens kept
+     * quotes to prevent search for redirects.
+     * @param token token to strip
+     * @return stripped token
+     */
+    private String stripQuotes(String token) {
+        if ((token.startsWith("\"") && token.endsWith("\"")) ||
+             token.startsWith("\'") && token.endsWith("\'")) {
+            token = token.substring(1, token.length() - 1);
+        }
+        return token;
+    }
+
+    /**
+     * resolvePath - resolves a path against a current working directory.
+     * @param cwd      current working directory
+     * @param fileName name of file or directory
+     * @return resolved Path to file
+     */
+    private static Path resolvePath(String cwd, String fileName) {
+        return Paths.get(cwd).resolve(fileName).normalize();
+    }
+
+    /**
+     * builtIn - checks to see if the command is a builtin and performs
+     * appropriate action.
+     * @param cmd current command
+     * @param cwd current working directory
+     * @return true if was a builtin command
+     */
+    private boolean builtIn(final List<String> cmd, final String cwd) {
+        switch (cmd.get(0)) {
+            // Set current working directory.
+            case "cd":
+                // If zero args then use home dirrectory as cwd else use first arg.
+                String newCWD = cmd.size() < 2 ? HOME_DIRECTORY : cmd.get(1);
+                // Normalize the cwd
+                Path cwdPath = resolvePath(cwd, newCWD);
+
+                // Check if is a directory.
+                File file = cwdPath.toFile();
+                if (!file.exists()) {
+                    reportError("file.not.exist", file.toString());
+                    return true;
+                } else if (!file.isDirectory()) {
+                    reportError("not.directory", file.toString());
+                    return true;
+                }
+
+                // Set PWD environment variable to be picked up as cwd.
+                environment.put("PWD", cwdPath.toString());
+                return true;
+
+            // Set an environment variable.
+            case "setenv":
+                if (3 <= cmd.size()) {
+                    String key = cmd.get(1);
+                    String value = cmd.get(2);
+                    environment.put(key, value);
+                }
+
+                return true;
+
+            // Unset an environment variable.
+            case "unsetenv":
+                if (2 <= cmd.size()) {
+                    String key = cmd.get(1);
+                    environment.remove(key);
+                }
+
+                return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * preprocessCommand - scan the command for redirects
+     * @param tokens       command tokens
+     * @param cwd          current working directory
+     * @param redirectInfo redirection information
+     * @return tokens remaining for actual command
+     */
+    private List<String>  preprocessCommand(final List<String> tokens,
+            final String cwd, final RedirectInfo redirectInfo) {
+        // Tokens remaining for actual command.
+        List<String> command = new ArrayList<>();
+
+        // iterate through all tokens.
+        Iterator<String> iterator = tokens.iterator();
+        while (iterator.hasNext()) {
+            String token = iterator.next();
+
+            // Check if is a redirect.
+            if (redirectInfo.check(token, iterator, cwd)) {
+                // Don't add to the command.
+                continue;
+            }
+
+            // Strip quotes and add to command.
+            command.add(stripQuotes(token));
+        }
+
+        return command;
+    }
+
+    /**
+     * createProcessBuilder - create a ProcessBuilder for the command.
+     * @param command      command tokens
+     * @param cwd          current working directory
+     * @param redirectInfo redirect information
+     */
+    private void createProcessBuilder(final List<String> command,
+            final String cwd, final RedirectInfo redirectInfo) {
+        // Create new ProcessBuilder.
+        ProcessBuilder pb = new ProcessBuilder(command);
+        // Set current working directory.
+        pb.directory(new File(cwd));
+
+        // Map environment variables.
+        Map<String, String> processEnvironment = pb.environment();
+        processEnvironment.clear();
+        processEnvironment.putAll(environment);
+
+        // Apply redirects.
+        redirectInfo.apply(pb);
+        // Add to current list of commands.
+        processBuilders.add(pb);
+    }
+
+    /**
+     * command - process the command
+     * @param tokens  tokens of the command
+     * @param isPiped true if the output of this command should be piped to the next
+     */
+    private void command(final List<String> tokens, boolean isPiped) {
+        // Test to see if we should echo the command to output.
+        if (envVarBooleanValue("JJS_ECHO")) {
+            System.out.println(String.join(" ", tokens));
+        }
+
+        // Get the current working directory.
+        String cwd = envVarValue("PWD", HOME_DIRECTORY);
+        // Preprocess the command for redirects.
+        RedirectInfo redirectInfo = new RedirectInfo();
+        List<String> command = preprocessCommand(tokens, cwd, redirectInfo);
+
+        // Skip if empty or a built in.
+        if (command.isEmpty() || builtIn(command, cwd)) {
+            return;
+        }
+
+        // Create ProcessBuilder with cwd and redirects set.
+        createProcessBuilder(command, cwd, redirectInfo);
+
+        // If piped the wait for the next command.
+        if (isPiped) {
+            return;
+        }
+
+        // Fetch first and last ProcessBuilder.
+        ProcessBuilder firstProcessBuilder = processBuilders.get(0);
+        ProcessBuilder lastProcessBuilder = processBuilders.get(processBuilders.size() - 1);
+
+        // Determine which streams have not be redirected from pipes.
+        boolean inputIsPipe = firstProcessBuilder.redirectInput() == Redirect.PIPE;
+        boolean outputIsPipe = lastProcessBuilder.redirectOutput() == Redirect.PIPE;
+        boolean errorIsPipe = lastProcessBuilder.redirectError() == Redirect.PIPE;
+        boolean inheritIO = envVarBooleanValue("JJS_INHERIT_IO");
+
+        // If not redirected and inputStream is current processes' input.
+        if (inputIsPipe && (inheritIO || inputStream == System.in)) {
+            // Inherit current processes' input.
+            firstProcessBuilder.redirectInput(Redirect.INHERIT);
+            inputIsPipe = false;
+        }
+
+        // If not redirected and outputStream is current processes' output.
+        if (outputIsPipe && (inheritIO || outputStream == System.out)) {
+            // Inherit current processes' output.
+            lastProcessBuilder.redirectOutput(Redirect.INHERIT);
+            outputIsPipe = false;
+        }
+
+        // If not redirected and errorStream is current processes' error.
+        if (errorIsPipe && (inheritIO || errorStream == System.err)) {
+            // Inherit current processes' error.
+            lastProcessBuilder.redirectError(Redirect.INHERIT);
+            errorIsPipe = false;
+        }
+
+        // Start the processes.
+        List<Process> processes = new ArrayList<>();
+        for (ProcessBuilder pb : processBuilders) {
+            try {
+                processes.add(pb.start());
+            } catch (IOException ex) {
+                reportError("unknown.command", String.join(" ", pb.command()));
+                return;
+            }
+        }
+
+        // Clear processBuilders for next command.
+        processBuilders.clear();
+
+        // Get first and last process.
+        Process firstProcess = processes.get(0);
+        Process lastProcess = processes.get(processes.size() - 1);
+
+        // Prepare for string based i/o if no redirection or provided streams.
+        ByteArrayOutputStream byteOutputStream = null;
+        ByteArrayOutputStream byteErrorStream = null;
+
+        // If input is not redirected.
+        if (inputIsPipe) {
+            // If inputStream other than System.in is provided.
+            if (inputStream != null) {
+                // Pipe inputStream to first process output stream.
+                new Piper(inputStream, firstProcess.getOutputStream()).start();
+            } else {
+                // Otherwise assume an input string has been provided.
+                ByteArrayInputStream byteInputStream = new ByteArrayInputStream(inputString.getBytes());
+                new Piper(new ByteArrayInputStream(inputString.getBytes()), firstProcess.getOutputStream()).start();
+            }
+        }
+
+        // If output is not redirected.
+        if (outputIsPipe) {
+            // If outputStream other than System.out is provided.
+            if (outputStream != null ) {
+                // Pipe outputStream from last process input stream.
+                new Piper(lastProcess.getInputStream(), outputStream).start();
+            } else {
+                // Otherwise assume an output string needs to be prepared.
+                byteOutputStream = new ByteArrayOutputStream(BUFFER_SIZE);
+                new Piper(lastProcess.getInputStream(), byteOutputStream).start();
+            }
+        }
+
+        // If error is not redirected.
+        if (errorIsPipe) {
+            // If errorStream other than System.err is provided.
+            if (errorStream != null) {
+                new Piper(lastProcess.getErrorStream(), errorStream).start();
+            } else {
+                // Otherwise assume an error string needs to be prepared.
+                byteErrorStream = new ByteArrayOutputStream(BUFFER_SIZE);
+                new Piper(lastProcess.getErrorStream(), byteErrorStream).start();
+            }
+        }
+
+        // Pipe commands in between.
+        for (int i = 0, n = processes.size() - 1; i < n; i++) {
+            Process prev = processes.get(i);
+            Process next = processes.get(i + 1);
+            new Piper(prev.getInputStream(), next.getOutputStream()).start();
+        }
+
+        // Wind up processes.
+        try {
+            // Get the user specified timeout.
+            long timeout = envVarLongValue("JJS_TIMEOUT");
+
+            // If user specified timeout (milliseconds.)
+            if (timeout != 0) {
+                // Wait for last process, with timeout.
+                if (lastProcess.waitFor(timeout, TimeUnit.MILLISECONDS)) {
+                    // Get exit code of last process.
+                    exitCode = lastProcess.exitValue();
+                } else {
+                    reportError("timeout", Long.toString(timeout));
+                 }
+            } else {
+                // Wait for last process and get exit code.
+                exitCode = lastProcess.waitFor();
+            }
+
+            // Accumulate the output and error streams.
+            outputString += byteOutputStream != null ? byteOutputStream.toString() : "";
+            errorString += byteErrorStream != null ? byteErrorStream.toString() : "";
+        } catch (InterruptedException ex) {
+            // Kill any living processes.
+            processes.stream().forEach(p -> {
+                if (p.isAlive()) {
+                    p.destroy();
+                }
+
+                // Get the first error code.
+                exitCode = exitCode == 0 ? p.exitValue() : exitCode;
+            });
+        }
+
+        // If we got a non-zero exit code then possibly throw an exception.
+        if (exitCode != 0 && envVarBooleanValue("JJS_THROW_ON_EXIT")) {
+            throw rangeError("exec.returned.non.zero", ScriptRuntime.safeToString(exitCode));
+        }
+    }
+
+    /**
+     * createTokenizer - build up StreamTokenizer for the command script
+     * @param script command script to parsed
+     * @return StreamTokenizer for command script
+     */
+    private StreamTokenizer createTokenizer(final String script) {
+        final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(script));
+        tokenizer.resetSyntax();
+        // Default all characters to word.
+        tokenizer.wordChars(0, 255);
+        // Spaces and special characters are white spaces.
+        tokenizer.whitespaceChars(0, ' ');
+        // Ignore # comments.
+        tokenizer.commentChar('#');
+        // Handle double and single quote strings.
+        tokenizer.quoteChar('"');
+        tokenizer.quoteChar('\'');
+        // Need to recognize the end of a command.
+        tokenizer.eolIsSignificant(true);
+        // Command separator.
+        tokenizer.ordinaryChar(';');
+        // Pipe separator.
+        tokenizer.ordinaryChar('|');
+
+        return tokenizer;
+    }
+
+    /**
+     * process - process a command string
+     * @param script command script to parsed
+     */
+    void process(final String script) {
+        // Build up StreamTokenizer for the command script.
+        final StreamTokenizer tokenizer = createTokenizer(script);
+
+        // Prepare to accumulate command tokens.
+        List<String> command = new ArrayList<>();
+        // Prepare to acumulate partial tokens joined with "\ ".
+        StringBuilder sb = new StringBuilder();
+
+        try {
+            // Fetch next token until end of script.
+            while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) {
+                // Next word token.
+                String token = tokenizer.sval;
+
+                // If special token.
+                if (token == null) {
+                    // Flush any partial token.
+                    if (sb.length() != 0) {
+                        command.add(sb.append(token).toString());
+                        sb.setLength(0);
+                    }
+
+                    // Process a completed command.
+                    // Will be either ';' (command end) or '|' (pipe), true if '|'.
+                    command(command, tokenizer.ttype == '|');
+
+                    if (exitCode != EXIT_SUCCESS) {
+                        return;
+                    }
+
+                    // Start with a new set of tokens.
+                    command.clear();
+                } else if (token.endsWith("\\")) {
+                    // Backslash followed by space.
+                    sb.append(token.substring(0, token.length() - 1)).append(' ');
+                } else if (sb.length() == 0) {
+                    // If not a word then must be a quoted string.
+                    if (tokenizer.ttype != StreamTokenizer.TT_WORD) {
+                        // Quote string, sb is free to use (empty.)
+                        sb.append((char)tokenizer.ttype);
+                        sb.append(token);
+                        sb.append((char)tokenizer.ttype);
+                        token = sb.toString();
+                        sb.setLength(0);
+                    }
+
+                    command.add(token);
+                } else {
+                    // Partial token pending.
+                    command.add(sb.append(token).toString());
+                    sb.setLength(0);
+                }
+            }
+        } catch (final IOException ex) {
+            // Do nothing.
+        }
+
+        // Partial token pending.
+        if (sb.length() != 0) {
+            command.add(sb.toString());
+        }
+
+        // Process last command.
+        command(command, false);
+    }
+
+    /**
+     * process - process a command array of strings
+     * @param script command script to be processed
+     */
+    void process(List<String> tokens) {
+        // Prepare to accumulate command tokens.
+        List<String> command = new ArrayList<>();
+
+        // Iterate through tokens.
+        Iterator<String> iterator = tokens.iterator();
+        while (iterator.hasNext() && exitCode == EXIT_SUCCESS) {
+            // Next word token.
+            String token = iterator.next();
+
+            if (token == null) {
+                continue;
+            }
+
+            switch (token) {
+                case "|":
+                    // Process as a piped command.
+                    command(command, true);
+                    // Start with a new set of tokens.
+                    command.clear();
+
+                    continue;
+                case ";":
+                    // Process as a normal command.
+                    command(command, false);
+                    // Start with a new set of tokens.
+                    command.clear();
+
+                    continue;
+            }
+
+            command.add(token);
+        }
+
+        // Process last command.
+        command(command, false);
+    }
+
+    void reportError(String msg, String object) {
+        errorString += ECMAErrors.getMessage("range.error.exec." + msg, object);
+        exitCode = EXIT_FAILURE;
+    }
+
+    String getOutputString() {
+        return outputString;
+    }
+
+    String getErrorString() {
+        return errorString;
+    }
+
+    int getExitCode() {
+        return exitCode;
+    }
+
+    void setEnvironment(Map<String, String> environment) {
+        this.environment = environment;
+    }
+
+    void setInputStream(InputStream inputStream) {
+        this.inputStream = inputStream;
+    }
+
+    void setInputString(String inputString) {
+        this.inputString = inputString;
+    }
+
+    void setOutputStream(OutputStream outputStream) {
+        this.outputStream = outputStream;
+    }
+
+    void setErrorStream(OutputStream errorStream) {
+        this.errorStream = errorStream;
+    }
+}
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptingFunctions.java	Tue Feb 09 14:14:06 2016 +0100
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/ScriptingFunctions.java	Wed Feb 10 11:18:02 2016 -0400
@@ -26,24 +26,24 @@
 package jdk.nashorn.internal.runtime;
 
 import static jdk.nashorn.internal.lookup.Lookup.MH;
-import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
 import static jdk.nashorn.internal.runtime.ECMAErrors.typeError;
 import static jdk.nashorn.internal.runtime.ScriptRuntime.UNDEFINED;
 
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.StreamTokenizer;
-import java.io.StringReader;
+import java.io.OutputStream;
 import java.lang.invoke.MethodHandle;
 import java.lang.invoke.MethodHandles;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import jdk.nashorn.internal.objects.NativeArray;
+import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
 
 /**
  * Global functions supported only in scripting mode.
@@ -71,9 +71,6 @@
     /** EXIT name - special property used by $EXEC API. */
     public static final String EXIT_NAME = "$EXIT";
 
-    /** THROW_ON_ERROR name - special property of the $EXEC function used by $EXEC API. */
-    public static final String THROW_ON_ERROR_NAME = "throwOnError";
-
     /** Names of special properties used by $ENV API. */
     public  static final String ENV_NAME  = "$ENV";
 
@@ -132,188 +129,100 @@
      * Nashorn extension: exec a string in a separate process.
      *
      * @param self   self reference
-     * @param args   string to execute, input and additional arguments, to be appended to {@code string}. Additional
-     *               arguments can be passed as either one JavaScript array, whose elements will be converted to
-     *               strings; or as a sequence of varargs, each of which will be converted to a string.
+     * @param args   In one of four forms
+     *               1. String script, String input
+     *               2. String script, InputStream input, OutputStream output, OutputStream error
+     *               3. Array scriptTokens, String input
+     *               4. Array scriptTokens, InputStream input, OutputStream output, OutputStream error
      *
-     * @return output string from the request
+     * @return output string from the request if in form of 1. or 3., empty string otherwise
      *
      * @throws IOException           if any stream access fails
      * @throws InterruptedException  if execution is interrupted
      */
     public static Object exec(final Object self, final Object... args) throws IOException, InterruptedException {
+        final Object arg0 = args.length > 0 ? args[0] : UNDEFINED;
+        final Object arg1 = args.length > 1 ? args[1] : UNDEFINED;
+        final Object arg2 = args.length > 2 ? args[2] : UNDEFINED;
+        final Object arg3 = args.length > 3 ? args[3] : UNDEFINED;
+
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+        OutputStream errorStream = null;
+        String script = null;
+        List<String> tokens = null;
+        String inputString = null;
+
+        if (arg0 instanceof NativeArray) {
+            String[] array = (String[])JSType.toJavaArray(arg0, String.class);
+            tokens = new ArrayList<>();
+            tokens.addAll(Arrays.asList(array));
+        } else {
+            script = JSType.toString(arg0);
+        }
+
+        if (arg1 instanceof InputStream) {
+            inputStream = (InputStream)arg1;
+        } else {
+            inputString = JSType.toString(arg1);
+        }
+
+        if (arg2 instanceof OutputStream) {
+            outputStream = (OutputStream)arg2;
+        }
+
+        if (arg3 instanceof OutputStream) {
+            errorStream = (OutputStream)arg3;
+        }
+
         // Current global is need to fetch additional inputs and for additional results.
         final ScriptObject global = Context.getGlobal();
-        final Object string = args.length > 0? args[0] : UNDEFINED;
-        final Object input = args.length > 1? args[1] : UNDEFINED;
-        final Object[] argv = (args.length > 2)? Arrays.copyOfRange(args, 2, args.length) : ScriptRuntime.EMPTY_ARRAY;
-        // Assemble command line, process additional arguments.
-        final List<String> cmdLine = tokenizeString(JSType.toString(string));
-        final Object[] additionalArgs = argv.length == 1 && argv[0] instanceof NativeArray ?
-                ((NativeArray) argv[0]).asObjectArray() :
-                argv;
-        for (Object arg : additionalArgs) {
-            cmdLine.add(JSType.toString(arg));
-        }
 
-        // Set up initial process.
-        final ProcessBuilder processBuilder = new ProcessBuilder(cmdLine);
+        // Capture ENV property state.
+        Map<String, String> environment = new HashMap<>();
+        final Object env = global.get(ENV_NAME);
 
-        // Current ENV property state.
-        final Object env = global.get(ENV_NAME);
         if (env instanceof ScriptObject) {
             final ScriptObject envProperties = (ScriptObject)env;
 
-            // If a working directory is present, use it.
-            final Object pwd = envProperties.get(PWD_NAME);
-            if (pwd != UNDEFINED) {
-                final File pwdFile = new File(JSType.toString(pwd));
-                if (pwdFile.exists()) {
-                    processBuilder.directory(pwdFile);
-                }
-            }
-
-            // Set up ENV variables.
-            final Map<String, String> environment = processBuilder.environment();
-            environment.clear();
-            for (final Map.Entry<Object, Object> entry : envProperties.entrySet()) {
+            // Copy ENV variables.
+            envProperties.entrySet().stream().forEach((entry) -> {
                 environment.put(JSType.toString(entry.getKey()), JSType.toString(entry.getValue()));
-            }
+            });
         }
 
-        // Start the process.
-        final Process process = processBuilder.start();
-        final IOException exception[] = new IOException[2];
-
-        // Collect output.
-        final StringBuilder outBuffer = new StringBuilder();
-        final Thread outThread = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                final char buffer[] = new char[1024];
-                try (final InputStreamReader inputStream = new InputStreamReader(process.getInputStream())) {
-                    for (int length; (length = inputStream.read(buffer, 0, buffer.length)) != -1; ) {
-                        outBuffer.append(buffer, 0, length);
-                    }
-                } catch (final IOException ex) {
-                    exception[0] = ex;
-                }
-            }
-        }, "$EXEC output");
+        // get the $EXEC function object from the global object
+        final Object exec = global.get(EXEC_NAME);
+        assert exec instanceof ScriptObject : EXEC_NAME + " is not a script object!";
 
-        // Collect errors.
-        final StringBuilder errBuffer = new StringBuilder();
-        final Thread errThread = new Thread(new Runnable() {
-            @Override
-            public void run() {
-                final char buffer[] = new char[1024];
-                try (final InputStreamReader inputStream = new InputStreamReader(process.getErrorStream())) {
-                    for (int length; (length = inputStream.read(buffer, 0, buffer.length)) != -1; ) {
-                        errBuffer.append(buffer, 0, length);
-                    }
-                } catch (final IOException ex) {
-                    exception[1] = ex;
-                }
-            }
-        }, "$EXEC error");
+        // Execute the commands
+        final CommandExecutor executor = new CommandExecutor();
+        executor.setInputString(inputString);
+        executor.setInputStream(inputStream);
+        executor.setOutputStream(outputStream);
+        executor.setErrorStream(errorStream);
+        executor.setEnvironment(environment);
 
-        // Start gathering output.
-        outThread.start();
-        errThread.start();
-
-        // If input is present, pass on to process.
-        if (!JSType.nullOrUndefined(input)) {
-            try (OutputStreamWriter outputStream = new OutputStreamWriter(process.getOutputStream())) {
-                final String in = JSType.toString(input);
-                outputStream.write(in, 0, in.length());
-            } catch (final IOException ex) {
-                // Process was not expecting input.  May be normal state of affairs.
-            }
+        if (tokens != null) {
+            executor.process(tokens);
+        } else {
+            executor.process(script);
         }
 
-        // Wait for the process to complete.
-        final int exit = process.waitFor();
-        outThread.join();
-        errThread.join();
-
-        final String out = outBuffer.toString();
-        final String err = errBuffer.toString();
+        final String outString = executor.getOutputString();
+        final String errString = executor.getErrorString();
+        int exitCode = executor.getExitCode();
 
         // Set globals for secondary results.
-        global.set(OUT_NAME, out, 0);
-        global.set(ERR_NAME, err, 0);
-        global.set(EXIT_NAME, exit, 0);
-
-        // Propagate exception if present.
-        for (final IOException element : exception) {
-            if (element != null) {
-                throw element;
-            }
-        }
-
-        // if we got a non-zero exit code ("failure"), then we have to decide to throw error or not
-        if (exit != 0) {
-            // get the $EXEC function object from the global object
-            final Object exec = global.get(EXEC_NAME);
-            assert exec instanceof ScriptObject : EXEC_NAME + " is not a script object!";
-
-            // Check if the user has set $EXEC.throwOnError property to true. If so, throw RangeError
-            // If that property is not set or set to false, then silently proceed with the rest.
-            if (JSType.toBoolean(((ScriptObject)exec).get(THROW_ON_ERROR_NAME))) {
-                throw rangeError("exec.returned.non.zero", ScriptRuntime.safeToString(exit));
-            }
-        }
+        global.set(OUT_NAME, outString, 0);
+        global.set(ERR_NAME, errString, 0);
+        global.set(EXIT_NAME, exitCode, 0);
 
         // Return the result from stdout.
-        return out;
+        return outString;
     }
 
     private static MethodHandle findOwnMH(final String name, final Class<?> rtype, final Class<?>... types) {
         return MH.findStatic(MethodHandles.lookup(), ScriptingFunctions.class, name, MH.type(rtype, types));
     }
-
-    /**
-     * Break a string into tokens, honoring quoted arguments and escaped spaces.
-     *
-     * @param str a {@link String} to tokenize.
-     * @return a {@link List} of {@link String}s representing the tokens that
-     * constitute the string.
-     */
-    public static List<String> tokenizeString(final String str) {
-        final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(str));
-        tokenizer.resetSyntax();
-        tokenizer.wordChars(0, 255);
-        tokenizer.whitespaceChars(0, ' ');
-        tokenizer.commentChar('#');
-        tokenizer.quoteChar('"');
-        tokenizer.quoteChar('\'');
-        final List<String> tokenList = new ArrayList<>();
-        final StringBuilder toAppend = new StringBuilder();
-        while (nextToken(tokenizer) != StreamTokenizer.TT_EOF) {
-            final String s = tokenizer.sval;
-            // The tokenizer understands about honoring quoted strings and recognizes
-            // them as one token that possibly contains multiple space-separated words.
-            // It does not recognize quoted spaces, though, and will split after the
-            // escaping \ character. This is handled here.
-            if (s.endsWith("\\")) {
-                // omit trailing \, append space instead
-                toAppend.append(s.substring(0, s.length() - 1)).append(' ');
-            } else {
-                tokenList.add(toAppend.append(s).toString());
-                toAppend.setLength(0);
-            }
-        }
-        if (toAppend.length() != 0) {
-            tokenList.add(toAppend.toString());
-        }
-        return tokenList;
-    }
-
-    private static int nextToken(final StreamTokenizer tokenizer) {
-        try {
-            return tokenizer.nextToken();
-        } catch (final IOException ioe) {
-            return StreamTokenizer.TT_EOF;
-        }
-    }
 }
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/resources/Messages.properties	Tue Feb 09 14:14:06 2016 +0100
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/resources/Messages.properties	Wed Feb 10 11:18:02 2016 -0400
@@ -170,7 +170,11 @@
 range.error.invalid.date=Invalid Date
 range.error.too.many.errors=Script contains too many errors: {0} errors
 range.error.concat.string.too.big=Concatenated String is too big
+range.error.exec.file.not.exist=$EXEC File or directory does not exist : {0}
+range.error.exec.not.directory=$EXEC Not a directory : {0}
 range.error.exec.returned.non.zero=$EXEC returned non-zero exit code: {0}
+range.error.exec.timeout=$EXEC Command timeout : {0}
+range.error.exec.unknown.command=$EXEC Unknown command : {0}
 
 reference.error.not.defined="{0}" is not defined
 reference.error.cant.be.used.as.lhs="{0}" can not be used as the left-hand side of assignment
--- a/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/tools/Shell.java	Tue Feb 09 14:14:06 2016 +0100
+++ b/nashorn/src/jdk.scripting.nashorn/share/classes/jdk/nashorn/tools/Shell.java	Wed Feb 10 11:18:02 2016 -0400
@@ -57,6 +57,8 @@
 import java.io.OutputStream;
 import java.io.PrintStream;
 import java.io.PrintWriter;
+import java.io.StreamTokenizer;
+import java.io.StringReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -278,7 +280,7 @@
         // as it might actually contain several arguments. Mac OS X splits shebang arguments, other platforms don't.
         // This special handling is also only necessary if the first argument actually starts with an option.
         if (args[0].startsWith("-") && !System.getProperty("os.name", "generic").startsWith("Mac OS X")) {
-            processedArgs.addAll(0, ScriptingFunctions.tokenizeString(processedArgs.remove(0)));
+            processedArgs.addAll(0, tokenizeString(processedArgs.remove(0)));
         }
 
         int shebangFilePos = -1; // -1 signifies "none found"
@@ -308,6 +310,44 @@
         return processedArgs.stream().toArray(String[]::new);
     }
 
+    public static List<String> tokenizeString(final String str) {
+        final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(str));
+        tokenizer.resetSyntax();
+        tokenizer.wordChars(0, 255);
+        tokenizer.whitespaceChars(0, ' ');
+        tokenizer.commentChar('#');
+        tokenizer.quoteChar('"');
+        tokenizer.quoteChar('\'');
+        final List<String> tokenList = new ArrayList<>();
+        final StringBuilder toAppend = new StringBuilder();
+        while (nextToken(tokenizer) != StreamTokenizer.TT_EOF) {
+            final String s = tokenizer.sval;
+            // The tokenizer understands about honoring quoted strings and recognizes
+            // them as one token that possibly contains multiple space-separated words.
+            // It does not recognize quoted spaces, though, and will split after the
+            // escaping \ character. This is handled here.
+            if (s.endsWith("\\")) {
+                // omit trailing \, append space instead
+                toAppend.append(s.substring(0, s.length() - 1)).append(' ');
+            } else {
+                tokenList.add(toAppend.append(s).toString());
+                toAppend.setLength(0);
+            }
+        }
+        if (toAppend.length() != 0) {
+            tokenList.add(toAppend.toString());
+        }
+        return tokenList;
+    }
+
+    private static int nextToken(final StreamTokenizer tokenizer) {
+        try {
+            return tokenizer.nextToken();
+        } catch (final IOException ioe) {
+            return StreamTokenizer.TT_EOF;
+        }
+    }
+
     /**
      * Compiles the given script files in the command line
      * This is called only when using the --compile-only flag
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/test/script/basic/JDK-8141209.js	Wed Feb 10 11:18:02 2016 -0400
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/**
+ * JDK-8141209 : $EXEC should allow streaming
+ *
+ * @test
+ * @option -scripting
+ * @runif os.not.windows
+ * @run
+ */
+
+
+var System = Java.type("java.lang.System");
+var File = Java.type("java.io.File");
+var ByteArrayInputStream = Java.type("java.io.ByteArrayInputStream");
+var ByteArrayOutputStream = Java.type("java.io.ByteArrayOutputStream");
+
+var input = <<<EOD
+There was an Old Man with a beard,
+Who said, It is just as I feared!
+Two Owls and a Hen,
+Four Larks and a Wren,
+Have all built their nests in my beard!
+EOD
+
+function tempFile() {
+    return File.createTempFile("JDK-8141209", ".txt").toString();
+}
+
+`ls -l / | sed > ${tempFile()} -e '/^d/ d'`
+
+$EXEC(["ls", "-l", "|", "sed", "-e", "/^d/ d", ">", tempFile()])
+
+var t1 = tempFile();
+
+$EXEC(<<<EOD)
+ls -l >${t1}
+sed <${t1} >${tempFile()} -e '/^d/ d'
+EOD
+
+$EXEC(<<<EOD, `ls -l`)
+sed >${tempFile()} -e '/^d/ d'
+EOD
+
+var instream = new ByteArrayInputStream(input.getBytes());
+var outstream = new ByteArrayOutputStream();
+var errstream = new ByteArrayOutputStream();
+$EXEC("sed -e '/beard/ d'", instream, outstream, errstream);
+var out = outstream.toString();
+var err = errstream.toString();
+
+instream = new ByteArrayInputStream(input.getBytes());
+$EXEC("sed -e '/beard/ d'", instream, System.out, System.err);
+
+
+$EXEC(<<<EOD)
+cd .
+setenv TEMP 0
+unsetenv TEMP
+EOD
+
+$ENV.JJS_THROW_ON_EXIT = "1";
+$ENV.JJS_TIMEOUT = "1000";
+$ENV.JJS_ECHO = "1";
+$ENV.JJS_INHERIT_IO = "1";
+
+$EXEC("echo hello world", instream);
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nashorn/test/script/basic/JDK-8141209.js.EXPECTED	Wed Feb 10 11:18:02 2016 -0400
@@ -0,0 +1,5 @@
+Who said, It is just as I feared!
+Two Owls and a Hen,
+Four Larks and a Wren,
+echo hello world
+hello world
--- a/nashorn/test/script/trusted/JDK-8087292.js	Tue Feb 09 14:14:06 2016 +0100
+++ b/nashorn/test/script/trusted/JDK-8087292.js	Wed Feb 10 11:18:02 2016 -0400
@@ -46,9 +46,9 @@
 tryExec();
 
 // turn on error with non-zero exit code
-$EXEC.throwOnError = true;
+$ENV.JJS_THROW_ON_EXIT = "1";
 tryExec();
 
 // no exception after this
-$EXEC.throwOnError = false;
+$ENV.JJS_THROW_ON_EXIT = "0";
 tryExec();
--- a/nashorn/test/src/jdk/nashorn/internal/test/framework/TestFinder.java	Tue Feb 09 14:14:06 2016 +0100
+++ b/nashorn/test/src/jdk/nashorn/internal/test/framework/TestFinder.java	Wed Feb 10 11:18:02 2016 -0400
@@ -68,7 +68,7 @@
 import javax.xml.xpath.XPathConstants;
 import javax.xml.xpath.XPathExpressionException;
 import javax.xml.xpath.XPathFactory;
-import jdk.nashorn.internal.runtime.ScriptingFunctions;
+import jdk.nashorn.tools.Shell;
 import org.w3c.dom.NodeList;
 import org.xml.sax.InputSource;
 
@@ -225,7 +225,7 @@
         boolean explicitOptimistic = false;
 
         String allContent = new String(Files.readAllBytes(testFile));
-        Iterator<String> scanner = ScriptingFunctions.tokenizeString(allContent).iterator();
+        Iterator<String> scanner = Shell.tokenizeString(allContent).iterator();
         while (scanner.hasNext()) {
             // TODO: Scan for /ref=file qualifiers, etc, to determine run
             // behavior