/*
 * 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).
/** * 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"); }); // Cygwin drive alias prefix. private static final String CYGDRIVE = "/cygdrive/"; // 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.
/** * 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
Params:
  • token – current command line token
  • iterator – current command line iterator
  • cwd – current working directory
Returns:true if token is consumed
/** * 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++) { final 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. final 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.
Params:
  • pb – current ProcessBuilder
/** * 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. final File outputFile = outputRedirect.file(); final 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.
/** * 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; private final Thread thread; Piper(final InputStream input, final OutputStream output) { this.input = input; this.output = output; this.thread = new Thread(this, "$EXEC Piper"); }
start - start the Piper in a new daemon thread
Returns:this Piper
/** * start - start the Piper in a new daemon thread * @return this Piper */
Piper start() { thread.setDaemon(true); thread.start(); return this; }
run - thread action
/** * run - thread action */
@Override public void run() { try { // Buffer for copying. final 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 (final Exception e) { // Assume the worst. throw new RuntimeException("Broken pipe", e); } finally { // Make sure the streams are closed. try { input.close(); } catch (final IOException e) { // Don't care. } try { output.close(); } catch (final IOException e) { // Don't care. } } } public void join() throws InterruptedException { thread.join(); } // 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.
Params:
  • key – name of environment variable
  • deflt – value to return if not found
Returns:value of the environment variable
/** * 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(final String key, final String deflt) { return environment.getOrDefault(key, deflt); }
envVarLongValue - return the value of the environment variable key as a long value.
Params:
  • key – name of environment variable
Returns:long value of the environment variable
/** * 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(final String key) { try { return Long.parseLong(envVarValue(key, "0")); } catch (final 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.
Params:
  • key – name of environment variable
Returns:boolean value of the environment variable
/** * 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(final String key) { return envVarLongValue(key) != 0; }
stripQuotes - strip quotes from token if present. Quoted tokens kept quotes to prevent search for redirects.
Params:
  • token – token to strip
Returns:stripped token
/** * 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 static 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.
Params:
  • cwd – current working directory
  • fileName – name of file or directory
Returns:resolved Path to file
/** * 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(final String cwd, final String fileName) { return Paths.get(sanitizePath(cwd)).resolve(fileName).normalize(); }
builtIn - checks to see if the command is a builtin and performs appropriate action.
Params:
  • cmd – current command
  • cwd – current working directory
Returns:true if was a builtin command
/** * 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": final boolean cygpath = IS_WINDOWS && cwd.startsWith(CYGDRIVE); // If zero args then use home directory as cwd else use first arg. final String newCWD = cmd.size() < 2 ? HOME_DIRECTORY : cmd.get(1); // Normalize the cwd final Path cwdPath = resolvePath(cwd, newCWD); // Check if is a directory. final 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. // Make sure Cygwin paths look like Unix paths. String scwd = cwdPath.toString(); if (cygpath && scwd.length() >= 2 && Character.isLetter(scwd.charAt(0)) && scwd.charAt(1) == ':') { scwd = CYGDRIVE + Character.toLowerCase(scwd.charAt(0)) + "/" + scwd.substring(2); } environment.put("PWD", scwd); return true; // Set an environment variable. case "setenv": if (3 <= cmd.size()) { final String key = cmd.get(1); final String value = cmd.get(2); environment.put(key, value); } return true; // Unset an environment variable. case "unsetenv": if (2 <= cmd.size()) { final String key = cmd.get(1); environment.remove(key); } return true; } return false; }
preprocessCommand - scan the command for redirects, and sanitize the executable path
Params:
  • tokens – command tokens
  • cwd – current working directory
  • redirectInfo – redirection information
Returns:tokens remaining for actual command
/** * preprocessCommand - scan the command for redirects, and sanitize the * executable path * @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. final List<String> command = new ArrayList<>(); // iterate through all tokens. final Iterator<String> iterator = tokens.iterator(); while (iterator.hasNext()) { final 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)); } if (command.size() > 0) { command.set(0, sanitizePath(command.get(0))); } return command; }
Sanitize a path in case the underlying platform is Cygwin. In that case, convert from the /cygdrive/x drive specification to the usual Windows X: format.
Params:
  • d – a String representing a path
Returns:a String representing the same path in a form that can be processed by the underlying platform
/** * Sanitize a path in case the underlying platform is Cygwin. In that case, * convert from the {@code /cygdrive/x} drive specification to the usual * Windows {@code X:} format. * * @param d a String representing a path * @return a String representing the same path in a form that can be * processed by the underlying platform */
private static String sanitizePath(final String d) { if (!IS_WINDOWS || (IS_WINDOWS && !d.startsWith(CYGDRIVE))) { return d; } final String pd = d.substring(CYGDRIVE.length()); if (pd.length() >= 2 && pd.charAt(1) == '/') { // drive letter plus / -> convert /cygdrive/x/... to X:/... return pd.charAt(0) + ":" + pd.substring(1); } else if (pd.length() == 1) { // just drive letter -> convert /cygdrive/x to X: return pd.charAt(0) + ":"; } // remaining case: /cygdrive/ -> can't convert return d; }
createProcessBuilder - create a ProcessBuilder for the command.
Params:
  • command – command tokens
  • cwd – current working directory
  • redirectInfo – redirect information
/** * 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. final ProcessBuilder pb = new ProcessBuilder(command); // Set current working directory. pb.directory(new File(sanitizePath(cwd))); // Map environment variables. final 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
Params:
  • tokens – tokens of the command
  • isPiped – true if the output of this command should be piped to the next
/** * 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, final 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. final String cwd = envVarValue("PWD", HOME_DIRECTORY); // Preprocess the command for redirects. final RedirectInfo redirectInfo = new RedirectInfo(); final 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, wait for the next command. if (isPiped) { return; } // Fetch first and last ProcessBuilder. final ProcessBuilder firstProcessBuilder = processBuilders.get(0); final 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; final 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. final List<Process> processes = new ArrayList<>(); for (final ProcessBuilder pb : processBuilders) { try { processes.add(pb.start()); } catch (final IOException ex) { reportError("unknown.command", String.join(" ", pb.command())); return; } } // Clear processBuilders for next command. processBuilders.clear(); // Get first and last process. final Process firstProcess = processes.get(0); final 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; final List<Piper> piperThreads = new ArrayList<>(); // 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. piperThreads.add(new Piper(inputStream, firstProcess.getOutputStream()).start()); } else { // Otherwise assume an input string has been provided. piperThreads.add(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. piperThreads.add(new Piper(lastProcess.getInputStream(), outputStream).start()); } else { // Otherwise assume an output string needs to be prepared. byteOutputStream = new ByteArrayOutputStream(BUFFER_SIZE); piperThreads.add(new Piper(lastProcess.getInputStream(), byteOutputStream).start()); } } // If error is not redirected. if (errorIsPipe) { // If errorStream other than System.err is provided. if (errorStream != null) { piperThreads.add(new Piper(lastProcess.getErrorStream(), errorStream).start()); } else { // Otherwise assume an error string needs to be prepared. byteErrorStream = new ByteArrayOutputStream(BUFFER_SIZE); piperThreads.add(new Piper(lastProcess.getErrorStream(), byteErrorStream).start()); } } // Pipe commands in between. for (int i = 0, n = processes.size() - 1; i < n; i++) { final Process prev = processes.get(i); final Process next = processes.get(i + 1); piperThreads.add(new Piper(prev.getInputStream(), next.getOutputStream()).start()); } // Wind up processes. try { // Get the user specified timeout. final 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(); } // Wait for all piper threads to terminate for (final Piper piper : piperThreads) { piper.join(); } // Accumulate the output and error streams. outputString += byteOutputStream != null ? byteOutputStream.toString() : ""; errorString += byteErrorStream != null ? byteErrorStream.toString() : ""; } catch (final 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
Params:
  • script – command script to parsed
Returns:StreamTokenizer for command script
/** * createTokenizer - build up StreamTokenizer for the command script * @param script command script to parsed * @return StreamTokenizer for command script */
private static 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
Params:
  • script – command script to parsed
/** * 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. final List<String> command = new ArrayList<>(); // Prepare to acumulate partial tokens joined with "\ ". final 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
Params:
  • tokens – command script to be processed
/** * process - process a command array of strings * @param tokens command script to be processed */
void process(final List<String> tokens) { // Prepare to accumulate command tokens. final List<String> command = new ArrayList<>(); // Iterate through tokens. final Iterator<String> iterator = tokens.iterator(); while (iterator.hasNext() && exitCode == EXIT_SUCCESS) { // Next word token. final 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(final String msg, final 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(final Map<String, String> environment) { this.environment = environment; } void setInputStream(final InputStream inputStream) { this.inputStream = inputStream; } void setInputString(final String inputString) { this.inputString = inputString; } void setOutputStream(final OutputStream outputStream) { this.outputStream = outputStream; } void setErrorStream(final OutputStream errorStream) { this.errorStream = errorStream; } }