CommandExecutor.java revision 1786:80120e9b3273
1/*
2 * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.internal.runtime;
27
28import java.io.ByteArrayInputStream;
29import java.io.ByteArrayOutputStream;
30import java.io.File;
31import java.io.IOException;
32import java.io.InputStream;
33import java.io.OutputStream;
34import java.io.StreamTokenizer;
35import java.io.StringReader;
36import java.lang.ProcessBuilder.Redirect;
37import java.nio.file.Path;
38import java.nio.file.Paths;
39import java.security.AccessController;
40import java.security.PrivilegedAction;
41import java.util.ArrayList;
42import java.util.HashMap;
43import java.util.Iterator;
44import java.util.List;
45import java.util.Map;
46import java.util.concurrent.TimeUnit;
47
48import static jdk.nashorn.internal.runtime.CommandExecutor.RedirectType.*;
49import static jdk.nashorn.internal.runtime.ECMAErrors.rangeError;
50
51/**
52 * The CommandExecutor class provides support for Nashorn's $EXEC
53 * builtin function. CommandExecutor provides support for command parsing,
54 * I/O redirection, piping, completion timeouts, # comments, and simple
55 * environment variable management (cd, setenv, and unsetenv).
56 */
57class CommandExecutor {
58    // Size of byte buffers used for piping.
59    private static final int BUFFER_SIZE = 1024;
60
61    // Test to see if running on Windows.
62    private static final boolean IS_WINDOWS =
63        AccessController.doPrivileged((PrivilegedAction<Boolean>)() -> {
64        return System.getProperty("os.name").contains("Windows");
65    });
66
67    // Cygwin drive alias prefix.
68    private static final String CYGDRIVE = "/cygdrive/";
69
70    // User's home directory
71    private static final String HOME_DIRECTORY =
72        AccessController.doPrivileged((PrivilegedAction<String>)() -> {
73        return System.getProperty("user.home");
74    });
75
76    // Various types of standard redirects.
77    enum RedirectType {
78        NO_REDIRECT,
79        REDIRECT_INPUT,
80        REDIRECT_OUTPUT,
81        REDIRECT_OUTPUT_APPEND,
82        REDIRECT_ERROR,
83        REDIRECT_ERROR_APPEND,
84        REDIRECT_OUTPUT_ERROR_APPEND,
85        REDIRECT_ERROR_TO_OUTPUT
86    };
87
88    // Prefix strings to standard redirects.
89    private static final String[] redirectPrefixes = new String[] {
90        "<",
91        "0<",
92        ">",
93        "1>",
94        ">>",
95        "1>>",
96        "2>",
97        "2>>",
98        "&>",
99        "2>&1"
100    };
101
102    // Map from redirectPrefixes to RedirectType.
103    private static final RedirectType[] redirects = new RedirectType[] {
104        REDIRECT_INPUT,
105        REDIRECT_INPUT,
106        REDIRECT_OUTPUT,
107        REDIRECT_OUTPUT,
108        REDIRECT_OUTPUT_APPEND,
109        REDIRECT_OUTPUT_APPEND,
110        REDIRECT_ERROR,
111        REDIRECT_ERROR_APPEND,
112        REDIRECT_OUTPUT_ERROR_APPEND,
113        REDIRECT_ERROR_TO_OUTPUT
114    };
115
116    /**
117     * The RedirectInfo class handles checking the next token in a command
118     * to see if it contains a redirect.  If the redirect file does not butt
119     * against the prefix, then the next token is consumed.
120     */
121    private static class RedirectInfo {
122        // true if a redirect was encountered on the current command.
123        private boolean hasRedirects;
124        // Redirect.PIPE or an input redirect from the command line.
125        private Redirect inputRedirect;
126        // Redirect.PIPE or an output redirect from the command line.
127        private Redirect outputRedirect;
128        // Redirect.PIPE or an error redirect from the command line.
129        private Redirect errorRedirect;
130        // true if the error stream should be merged with output.
131        private boolean mergeError;
132
133        RedirectInfo() {
134            this.hasRedirects = false;
135            this.inputRedirect = Redirect.PIPE;
136            this.outputRedirect = Redirect.PIPE;
137            this.errorRedirect = Redirect.PIPE;
138            this.mergeError = false;
139        }
140
141        /**
142         * check - tests to see if the current token contains a redirect
143         * @param token    current command line token
144         * @param iterator current command line iterator
145         * @param cwd      current working directory
146         * @return true if token is consumed
147         */
148        boolean check(String token, final Iterator<String> iterator, final String cwd) {
149            // Iterate through redirect prefixes to file a match.
150            for (int i = 0; i < redirectPrefixes.length; i++) {
151               final String prefix = redirectPrefixes[i];
152
153               // If a match is found.
154                if (token.startsWith(prefix)) {
155                    // Indicate we have at least one redirect (efficiency.)
156                    hasRedirects = true;
157                    // Map prefix to RedirectType.
158                    final RedirectType redirect = redirects[i];
159                    // Strip prefix from token
160                    token = token.substring(prefix.length());
161
162                    // Get file from either current or next token.
163                    File file = null;
164                    if (redirect != REDIRECT_ERROR_TO_OUTPUT) {
165                        // Nothing left of current token.
166                        if (token.length() == 0) {
167                            if (iterator.hasNext()) {
168                                // Use next token.
169                                token = iterator.next();
170                            } else {
171                                // Send to null device if not provided.
172                                token = IS_WINDOWS ? "NUL:" : "/dev/null";
173                            }
174                        }
175
176                        // Redirect file.
177                        file = resolvePath(cwd, token).toFile();
178                    }
179
180                    // Define redirect based on prefix.
181                    switch (redirect) {
182                        case REDIRECT_INPUT:
183                            inputRedirect = Redirect.from(file);
184                            break;
185                        case REDIRECT_OUTPUT:
186                            outputRedirect = Redirect.to(file);
187                            break;
188                        case REDIRECT_OUTPUT_APPEND:
189                            outputRedirect = Redirect.appendTo(file);
190                            break;
191                        case REDIRECT_ERROR:
192                            errorRedirect = Redirect.to(file);
193                            break;
194                        case REDIRECT_ERROR_APPEND:
195                            errorRedirect = Redirect.appendTo(file);
196                            break;
197                        case REDIRECT_OUTPUT_ERROR_APPEND:
198                            outputRedirect = Redirect.to(file);
199                            errorRedirect = Redirect.to(file);
200                            mergeError = true;
201                            break;
202                        case REDIRECT_ERROR_TO_OUTPUT:
203                            mergeError = true;
204                            break;
205                        default:
206                            return false;
207                    }
208
209                    // Indicate token is consumed.
210                    return true;
211                }
212            }
213
214            // No redirect found.
215            return false;
216        }
217
218        /**
219         * apply - apply the redirects to the current ProcessBuilder.
220         * @param pb current ProcessBuilder
221         */
222        void apply(final ProcessBuilder pb) {
223            // Only if there was redirects (saves new structure in ProcessBuilder.)
224            if (hasRedirects) {
225                // If output and error are the same file then merge.
226                final File outputFile = outputRedirect.file();
227                final File errorFile = errorRedirect.file();
228
229                if (outputFile != null && outputFile.equals(errorFile)) {
230                    mergeError = true;
231                }
232
233                // Apply redirects.
234                pb.redirectInput(inputRedirect);
235                pb.redirectOutput(outputRedirect);
236                pb.redirectError(errorRedirect);
237                pb.redirectErrorStream(mergeError);
238            }
239        }
240    }
241
242    /**
243     * The Piper class is responsible for copying from an InputStream to an
244     * OutputStream without blocking the current thread.
245     */
246    private static class Piper implements java.lang.Runnable {
247        // Stream to copy from.
248        private final InputStream input;
249        // Stream to copy to.
250        private final OutputStream output;
251
252        private final Thread thread;
253
254        Piper(final InputStream input, final OutputStream output) {
255            this.input = input;
256            this.output = output;
257            this.thread = new Thread(this, "$EXEC Piper");
258        }
259
260        /**
261         * start - start the Piper in a new daemon thread
262         * @return this Piper
263         */
264        Piper start() {
265            thread.setDaemon(true);
266            thread.start();
267            return this;
268        }
269
270        /**
271         * run - thread action
272         */
273        @Override
274        public void run() {
275            try {
276                // Buffer for copying.
277                final byte[] b = new byte[BUFFER_SIZE];
278                // Read from the InputStream until EOF.
279                int read;
280                while (-1 < (read = input.read(b, 0, b.length))) {
281                    // Write available date to OutputStream.
282                    output.write(b, 0, read);
283                }
284            } catch (final Exception e) {
285                // Assume the worst.
286                throw new RuntimeException("Broken pipe", e);
287            } finally {
288                // Make sure the streams are closed.
289                try {
290                    input.close();
291                } catch (final IOException e) {
292                    // Don't care.
293                }
294                try {
295                    output.close();
296                } catch (final IOException e) {
297                    // Don't care.
298                }
299            }
300        }
301
302        public void join() throws InterruptedException {
303            thread.join();
304        }
305
306        // Exit thread.
307    }
308
309    // Process exit statuses.
310    static final int EXIT_SUCCESS  =  0;
311    static final int EXIT_FAILURE  =  1;
312
313    // Copy of environment variables used by all processes.
314    private  Map<String, String> environment;
315    // Input string if provided on CommandExecutor call.
316    private String inputString;
317    // Output string if required from CommandExecutor call.
318    private String outputString;
319    // Error string if required from CommandExecutor call.
320    private String errorString;
321    // Last process exit code.
322    private int exitCode;
323
324    // Input stream if provided on CommandExecutor call.
325    private InputStream inputStream;
326    // Output stream if provided on CommandExecutor call.
327    private OutputStream outputStream;
328    // Error stream if provided on CommandExecutor call.
329    private OutputStream errorStream;
330
331    // Ordered collection of current or piped ProcessBuilders.
332    private List<ProcessBuilder> processBuilders = new ArrayList<>();
333
334    CommandExecutor() {
335        this.environment = new HashMap<>();
336        this.inputString = "";
337        this.outputString = "";
338        this.errorString = "";
339        this.exitCode = EXIT_SUCCESS;
340        this.inputStream = null;
341        this.outputStream = null;
342        this.errorStream = null;
343        this.processBuilders = new ArrayList<>();
344    }
345
346    /**
347     * envVarValue - return the value of the environment variable key, or
348     * deflt if not found.
349     * @param key   name of environment variable
350     * @param deflt value to return if not found
351     * @return value of the environment variable
352     */
353    private String envVarValue(final String key, final String deflt) {
354        return environment.getOrDefault(key, deflt);
355    }
356
357    /**
358     * envVarLongValue - return the value of the environment variable key as a
359     * long value.
360     * @param key name of environment variable
361     * @return long value of the environment variable
362     */
363    private long envVarLongValue(final String key) {
364        try {
365            return Long.parseLong(envVarValue(key, "0"));
366        } catch (final NumberFormatException ex) {
367            return 0L;
368        }
369    }
370
371    /**
372     * envVarBooleanValue - return the value of the environment variable key as a
373     * boolean value.  true if the value was non-zero, false otherwise.
374     * @param key name of environment variable
375     * @return boolean value of the environment variable
376     */
377    private boolean envVarBooleanValue(final String key) {
378        return envVarLongValue(key) != 0;
379    }
380
381    /**
382     * stripQuotes - strip quotes from token if present. Quoted tokens kept
383     * quotes to prevent search for redirects.
384     * @param token token to strip
385     * @return stripped token
386     */
387    private static String stripQuotes(String token) {
388        if ((token.startsWith("\"") && token.endsWith("\"")) ||
389             token.startsWith("\'") && token.endsWith("\'")) {
390            token = token.substring(1, token.length() - 1);
391        }
392        return token;
393    }
394
395    /**
396     * resolvePath - resolves a path against a current working directory.
397     * @param cwd      current working directory
398     * @param fileName name of file or directory
399     * @return resolved Path to file
400     */
401    private static Path resolvePath(final String cwd, final String fileName) {
402        return Paths.get(sanitizePath(cwd)).resolve(fileName).normalize();
403    }
404
405    /**
406     * builtIn - checks to see if the command is a builtin and performs
407     * appropriate action.
408     * @param cmd current command
409     * @param cwd current working directory
410     * @return true if was a builtin command
411     */
412    private boolean builtIn(final List<String> cmd, final String cwd) {
413        switch (cmd.get(0)) {
414            // Set current working directory.
415            case "cd":
416                final boolean cygpath = IS_WINDOWS && cwd.startsWith(CYGDRIVE);
417                // If zero args then use home directory as cwd else use first arg.
418                final String newCWD = cmd.size() < 2 ? HOME_DIRECTORY : cmd.get(1);
419                // Normalize the cwd
420                final Path cwdPath = resolvePath(cwd, newCWD);
421
422                // Check if is a directory.
423                final File file = cwdPath.toFile();
424                if (!file.exists()) {
425                    reportError("file.not.exist", file.toString());
426                    return true;
427                } else if (!file.isDirectory()) {
428                    reportError("not.directory", file.toString());
429                    return true;
430                }
431
432                // Set PWD environment variable to be picked up as cwd.
433                // Make sure Cygwin paths look like Unix paths.
434                String scwd = cwdPath.toString();
435                if (cygpath && scwd.length() >= 2 &&
436                        Character.isLetter(scwd.charAt(0)) && scwd.charAt(1) == ':') {
437                    scwd = CYGDRIVE + Character.toLowerCase(scwd.charAt(0)) + "/" + scwd.substring(2);
438                }
439                environment.put("PWD", scwd);
440                return true;
441
442            // Set an environment variable.
443            case "setenv":
444                if (3 <= cmd.size()) {
445                    final String key = cmd.get(1);
446                    final String value = cmd.get(2);
447                    environment.put(key, value);
448                }
449
450                return true;
451
452            // Unset an environment variable.
453            case "unsetenv":
454                if (2 <= cmd.size()) {
455                    final String key = cmd.get(1);
456                    environment.remove(key);
457                }
458
459                return true;
460        }
461
462        return false;
463    }
464
465    /**
466     * preprocessCommand - scan the command for redirects, and sanitize the
467     * executable path
468     * @param tokens       command tokens
469     * @param cwd          current working directory
470     * @param redirectInfo redirection information
471     * @return tokens remaining for actual command
472     */
473    private List<String>  preprocessCommand(final List<String> tokens,
474            final String cwd, final RedirectInfo redirectInfo) {
475        // Tokens remaining for actual command.
476        final List<String> command = new ArrayList<>();
477
478        // iterate through all tokens.
479        final Iterator<String> iterator = tokens.iterator();
480        while (iterator.hasNext()) {
481            final String token = iterator.next();
482
483            // Check if is a redirect.
484            if (redirectInfo.check(token, iterator, cwd)) {
485                // Don't add to the command.
486                continue;
487            }
488
489            // Strip quotes and add to command.
490            command.add(stripQuotes(token));
491        }
492
493        if (command.size() > 0) {
494            command.set(0, sanitizePath(command.get(0)));
495        }
496
497        return command;
498    }
499
500    /**
501     * Sanitize a path in case the underlying platform is Cygwin. In that case,
502     * convert from the {@code /cygdrive/x} drive specification to the usual
503     * Windows {@code X:} format.
504     *
505     * @param d a String representing a path
506     * @return a String representing the same path in a form that can be
507     *         processed by the underlying platform
508     */
509    private static String sanitizePath(final String d) {
510        if (!IS_WINDOWS || (IS_WINDOWS && !d.startsWith(CYGDRIVE))) {
511            return d;
512        }
513        final String pd = d.substring(CYGDRIVE.length());
514        if (pd.length() >= 2 && pd.charAt(1) == '/') {
515            // drive letter plus / -> convert /cygdrive/x/... to X:/...
516            return pd.charAt(0) + ":" + pd.substring(1);
517        } else if (pd.length() == 1) {
518            // just drive letter -> convert /cygdrive/x to X:
519            return pd.charAt(0) + ":";
520        }
521        // remaining case: /cygdrive/ -> can't convert
522        return d;
523    }
524
525    /**
526     * createProcessBuilder - create a ProcessBuilder for the command.
527     * @param command      command tokens
528     * @param cwd          current working directory
529     * @param redirectInfo redirect information
530     */
531    private void createProcessBuilder(final List<String> command,
532            final String cwd, final RedirectInfo redirectInfo) {
533        // Create new ProcessBuilder.
534        final ProcessBuilder pb = new ProcessBuilder(command);
535        // Set current working directory.
536        pb.directory(new File(sanitizePath(cwd)));
537
538        // Map environment variables.
539        final Map<String, String> processEnvironment = pb.environment();
540        processEnvironment.clear();
541        processEnvironment.putAll(environment);
542
543        // Apply redirects.
544        redirectInfo.apply(pb);
545        // Add to current list of commands.
546        processBuilders.add(pb);
547    }
548
549    /**
550     * command - process the command
551     * @param tokens  tokens of the command
552     * @param isPiped true if the output of this command should be piped to the next
553     */
554    private void command(final List<String> tokens, final boolean isPiped) {
555        // Test to see if we should echo the command to output.
556        if (envVarBooleanValue("JJS_ECHO")) {
557            System.out.println(String.join(" ", tokens));
558        }
559
560        // Get the current working directory.
561        final String cwd = envVarValue("PWD", HOME_DIRECTORY);
562        // Preprocess the command for redirects.
563        final RedirectInfo redirectInfo = new RedirectInfo();
564        final List<String> command = preprocessCommand(tokens, cwd, redirectInfo);
565
566        // Skip if empty or a built in.
567        if (command.isEmpty() || builtIn(command, cwd)) {
568            return;
569        }
570
571        // Create ProcessBuilder with cwd and redirects set.
572        createProcessBuilder(command, cwd, redirectInfo);
573
574        // If piped, wait for the next command.
575        if (isPiped) {
576            return;
577        }
578
579        // Fetch first and last ProcessBuilder.
580        final ProcessBuilder firstProcessBuilder = processBuilders.get(0);
581        final ProcessBuilder lastProcessBuilder = processBuilders.get(processBuilders.size() - 1);
582
583        // Determine which streams have not be redirected from pipes.
584        boolean inputIsPipe = firstProcessBuilder.redirectInput() == Redirect.PIPE;
585        boolean outputIsPipe = lastProcessBuilder.redirectOutput() == Redirect.PIPE;
586        boolean errorIsPipe = lastProcessBuilder.redirectError() == Redirect.PIPE;
587        final boolean inheritIO = envVarBooleanValue("JJS_INHERIT_IO");
588
589        // If not redirected and inputStream is current processes' input.
590        if (inputIsPipe && (inheritIO || inputStream == System.in)) {
591            // Inherit current processes' input.
592            firstProcessBuilder.redirectInput(Redirect.INHERIT);
593            inputIsPipe = false;
594        }
595
596        // If not redirected and outputStream is current processes' output.
597        if (outputIsPipe && (inheritIO || outputStream == System.out)) {
598            // Inherit current processes' output.
599            lastProcessBuilder.redirectOutput(Redirect.INHERIT);
600            outputIsPipe = false;
601        }
602
603        // If not redirected and errorStream is current processes' error.
604        if (errorIsPipe && (inheritIO || errorStream == System.err)) {
605            // Inherit current processes' error.
606            lastProcessBuilder.redirectError(Redirect.INHERIT);
607            errorIsPipe = false;
608        }
609
610        // Start the processes.
611        final List<Process> processes = new ArrayList<>();
612        for (final ProcessBuilder pb : processBuilders) {
613            try {
614                processes.add(pb.start());
615            } catch (final IOException ex) {
616                reportError("unknown.command", String.join(" ", pb.command()));
617                return;
618            }
619        }
620
621        // Clear processBuilders for next command.
622        processBuilders.clear();
623
624        // Get first and last process.
625        final Process firstProcess = processes.get(0);
626        final Process lastProcess = processes.get(processes.size() - 1);
627
628        // Prepare for string based i/o if no redirection or provided streams.
629        ByteArrayOutputStream byteOutputStream = null;
630        ByteArrayOutputStream byteErrorStream = null;
631
632        final List<Piper> piperThreads = new ArrayList<>();
633
634        // If input is not redirected.
635        if (inputIsPipe) {
636            // If inputStream other than System.in is provided.
637            if (inputStream != null) {
638                // Pipe inputStream to first process output stream.
639                piperThreads.add(new Piper(inputStream, firstProcess.getOutputStream()).start());
640            } else {
641                // Otherwise assume an input string has been provided.
642                piperThreads.add(new Piper(new ByteArrayInputStream(inputString.getBytes()), firstProcess.getOutputStream()).start());
643            }
644        }
645
646        // If output is not redirected.
647        if (outputIsPipe) {
648            // If outputStream other than System.out is provided.
649            if (outputStream != null ) {
650                // Pipe outputStream from last process input stream.
651                piperThreads.add(new Piper(lastProcess.getInputStream(), outputStream).start());
652            } else {
653                // Otherwise assume an output string needs to be prepared.
654                byteOutputStream = new ByteArrayOutputStream(BUFFER_SIZE);
655                piperThreads.add(new Piper(lastProcess.getInputStream(), byteOutputStream).start());
656            }
657        }
658
659        // If error is not redirected.
660        if (errorIsPipe) {
661            // If errorStream other than System.err is provided.
662            if (errorStream != null) {
663                piperThreads.add(new Piper(lastProcess.getErrorStream(), errorStream).start());
664            } else {
665                // Otherwise assume an error string needs to be prepared.
666                byteErrorStream = new ByteArrayOutputStream(BUFFER_SIZE);
667                piperThreads.add(new Piper(lastProcess.getErrorStream(), byteErrorStream).start());
668            }
669        }
670
671        // Pipe commands in between.
672        for (int i = 0, n = processes.size() - 1; i < n; i++) {
673            final Process prev = processes.get(i);
674            final Process next = processes.get(i + 1);
675            piperThreads.add(new Piper(prev.getInputStream(), next.getOutputStream()).start());
676        }
677
678        // Wind up processes.
679        try {
680            // Get the user specified timeout.
681            final long timeout = envVarLongValue("JJS_TIMEOUT");
682
683            // If user specified timeout (milliseconds.)
684            if (timeout != 0) {
685                // Wait for last process, with timeout.
686                if (lastProcess.waitFor(timeout, TimeUnit.MILLISECONDS)) {
687                    // Get exit code of last process.
688                    exitCode = lastProcess.exitValue();
689                } else {
690                    reportError("timeout", Long.toString(timeout));
691                 }
692            } else {
693                // Wait for last process and get exit code.
694                exitCode = lastProcess.waitFor();
695            }
696            // Wait for all piper threads to terminate
697            for (final Piper piper : piperThreads) {
698                piper.join();
699            }
700
701            // Accumulate the output and error streams.
702            outputString += byteOutputStream != null ? byteOutputStream.toString() : "";
703            errorString += byteErrorStream != null ? byteErrorStream.toString() : "";
704        } catch (final InterruptedException ex) {
705            // Kill any living processes.
706            processes.stream().forEach(p -> {
707                if (p.isAlive()) {
708                    p.destroy();
709                }
710
711                // Get the first error code.
712                exitCode = exitCode == 0 ? p.exitValue() : exitCode;
713            });
714        }
715
716        // If we got a non-zero exit code then possibly throw an exception.
717        if (exitCode != 0 && envVarBooleanValue("JJS_THROW_ON_EXIT")) {
718            throw rangeError("exec.returned.non.zero", ScriptRuntime.safeToString(exitCode));
719        }
720    }
721
722    /**
723     * createTokenizer - build up StreamTokenizer for the command script
724     * @param script command script to parsed
725     * @return StreamTokenizer for command script
726     */
727    private static StreamTokenizer createTokenizer(final String script) {
728        final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(script));
729        tokenizer.resetSyntax();
730        // Default all characters to word.
731        tokenizer.wordChars(0, 255);
732        // Spaces and special characters are white spaces.
733        tokenizer.whitespaceChars(0, ' ');
734        // Ignore # comments.
735        tokenizer.commentChar('#');
736        // Handle double and single quote strings.
737        tokenizer.quoteChar('"');
738        tokenizer.quoteChar('\'');
739        // Need to recognize the end of a command.
740        tokenizer.eolIsSignificant(true);
741        // Command separator.
742        tokenizer.ordinaryChar(';');
743        // Pipe separator.
744        tokenizer.ordinaryChar('|');
745
746        return tokenizer;
747    }
748
749    /**
750     * process - process a command string
751     * @param script command script to parsed
752     */
753    void process(final String script) {
754        // Build up StreamTokenizer for the command script.
755        final StreamTokenizer tokenizer = createTokenizer(script);
756
757        // Prepare to accumulate command tokens.
758        final List<String> command = new ArrayList<>();
759        // Prepare to acumulate partial tokens joined with "\ ".
760        final StringBuilder sb = new StringBuilder();
761
762        try {
763            // Fetch next token until end of script.
764            while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) {
765                // Next word token.
766                String token = tokenizer.sval;
767
768                // If special token.
769                if (token == null) {
770                    // Flush any partial token.
771                    if (sb.length() != 0) {
772                        command.add(sb.append(token).toString());
773                        sb.setLength(0);
774                    }
775
776                    // Process a completed command.
777                    // Will be either ';' (command end) or '|' (pipe), true if '|'.
778                    command(command, tokenizer.ttype == '|');
779
780                    if (exitCode != EXIT_SUCCESS) {
781                        return;
782                    }
783
784                    // Start with a new set of tokens.
785                    command.clear();
786                } else if (token.endsWith("\\")) {
787                    // Backslash followed by space.
788                    sb.append(token.substring(0, token.length() - 1)).append(' ');
789                } else if (sb.length() == 0) {
790                    // If not a word then must be a quoted string.
791                    if (tokenizer.ttype != StreamTokenizer.TT_WORD) {
792                        // Quote string, sb is free to use (empty.)
793                        sb.append((char)tokenizer.ttype);
794                        sb.append(token);
795                        sb.append((char)tokenizer.ttype);
796                        token = sb.toString();
797                        sb.setLength(0);
798                    }
799
800                    command.add(token);
801                } else {
802                    // Partial token pending.
803                    command.add(sb.append(token).toString());
804                    sb.setLength(0);
805                }
806            }
807        } catch (final IOException ex) {
808            // Do nothing.
809        }
810
811        // Partial token pending.
812        if (sb.length() != 0) {
813            command.add(sb.toString());
814        }
815
816        // Process last command.
817        command(command, false);
818    }
819
820    /**
821     * process - process a command array of strings
822     * @param tokens command script to be processed
823     */
824    void process(final List<String> tokens) {
825        // Prepare to accumulate command tokens.
826        final List<String> command = new ArrayList<>();
827
828        // Iterate through tokens.
829        final Iterator<String> iterator = tokens.iterator();
830        while (iterator.hasNext() && exitCode == EXIT_SUCCESS) {
831            // Next word token.
832            final String token = iterator.next();
833
834            if (token == null) {
835                continue;
836            }
837
838            switch (token) {
839                case "|":
840                    // Process as a piped command.
841                    command(command, true);
842                    // Start with a new set of tokens.
843                    command.clear();
844
845                    continue;
846                case ";":
847                    // Process as a normal command.
848                    command(command, false);
849                    // Start with a new set of tokens.
850                    command.clear();
851
852                    continue;
853            }
854
855            command.add(token);
856        }
857
858        // Process last command.
859        command(command, false);
860    }
861
862    void reportError(final String msg, final String object) {
863        errorString += ECMAErrors.getMessage("range.error.exec." + msg, object);
864        exitCode = EXIT_FAILURE;
865    }
866
867    String getOutputString() {
868        return outputString;
869    }
870
871    String getErrorString() {
872        return errorString;
873    }
874
875    int getExitCode() {
876        return exitCode;
877    }
878
879    void setEnvironment(final Map<String, String> environment) {
880        this.environment = environment;
881    }
882
883    void setInputStream(final InputStream inputStream) {
884        this.inputStream = inputStream;
885    }
886
887    void setInputString(final String inputString) {
888        this.inputString = inputString;
889    }
890
891    void setOutputStream(final OutputStream outputStream) {
892        this.outputStream = outputStream;
893    }
894
895    void setErrorStream(final OutputStream errorStream) {
896        this.errorStream = errorStream;
897    }
898}
899