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