| /* |
| * 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"); |
| }); |
| |
| // 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. |
| */ |
| 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++) { |
| 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. |
| * @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. |
| */ |
| 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 |
| * @return this Piper |
| */ |
| Piper start() { |
| thread.setDaemon(true); |
| thread.start(); |
| return this; |
| } |
| |
| /** |
| * 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. |
| * @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. |
| * @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. |
| * @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. |
| * @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. |
| * @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. |
| * @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 |
| * @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 {@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. |
| * @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 |
| * @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 |
| * @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 |
| * @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 |
| * @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; |
| } |
| } |