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 26 package jdk.nashorn.internal.runtime; 27 28 import java.io.ByteArrayInputStream; 29 import java.io.ByteArrayOutputStream; 30 import java.io.File; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.io.OutputStream; 34 import java.io.StreamTokenizer; 35 import java.io.StringReader; 36 import java.lang.ProcessBuilder.Redirect; 37 import java.nio.file.Path; 38 import java.nio.file.Paths; 39 import java.security.AccessController; 40 import java.security.PrivilegedAction; 41 import java.util.ArrayList; 42 import java.util.HashMap; 43 import java.util.Iterator; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.concurrent.TimeUnit; 47 48 import static jdk.nashorn.internal.runtime.CommandExecutor.RedirectType.*; 49 import 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 */ 57 class 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 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 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 File outputFile = outputRedirect.file(); 227 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 Piper(final InputStream input, final OutputStream output) { 253 this.input = input; 254 this.output = output; 255 } 256 257 /** 258 * start - start the Piper in a new daemon thread 259 */ 260 void start() { 261 Thread thread = new Thread(this, "$EXEC Piper"); 262 thread.setDaemon(true); 263 thread.start(); 264 } 265 266 /** 267 * run - thread action 268 */ 269 @Override 270 public void run() { 271 try { 272 // Buffer for copying. 273 byte[] b = new byte[BUFFER_SIZE]; 274 // Read from the InputStream until EOF. 275 int read; 276 while (-1 < (read = input.read(b, 0, b.length))) { 277 // Write available date to OutputStream. 278 output.write(b, 0, read); 279 } 280 } catch (Exception e) { 281 // Assume the worst. 282 throw new RuntimeException("Broken pipe", e); 283 } finally { 284 // Make sure the streams are closed. 285 try { 286 input.close(); 287 } catch (IOException e) { 288 // Don't care. 289 } 290 try { 291 output.close(); 292 } catch (IOException e) { 293 // Don't care. 294 } 295 } 296 } 297 298 // Exit thread. 299 } 300 301 // Process exit statuses. 302 static final int EXIT_SUCCESS = 0; 303 static final int EXIT_FAILURE = 1; 304 305 // Copy of environment variables used by all processes. 306 private Map<String, String> environment; 307 // Input string if provided on CommandExecutor call. 308 private String inputString; 309 // Output string if required from CommandExecutor call. 310 private String outputString; 311 // Error string if required from CommandExecutor call. 312 private String errorString; 313 // Last process exit code. 314 private int exitCode; 315 316 // Input stream if provided on CommandExecutor call. 317 private InputStream inputStream; 318 // Output stream if provided on CommandExecutor call. 319 private OutputStream outputStream; 320 // Error stream if provided on CommandExecutor call. 321 private OutputStream errorStream; 322 323 // Ordered collection of current or piped ProcessBuilders. 324 private List<ProcessBuilder> processBuilders = new ArrayList<>(); 325 326 CommandExecutor() { 327 this.environment = new HashMap<>(); 328 this.inputString = ""; 329 this.outputString = ""; 330 this.errorString = ""; 331 this.exitCode = EXIT_SUCCESS; 332 this.inputStream = null; 333 this.outputStream = null; 334 this.errorStream = null; 335 this.processBuilders = new ArrayList<>(); 336 } 337 338 /** 339 * envVarValue - return the value of the environment variable key, or 340 * deflt if not found. 341 * @param key name of environment variable 342 * @param deflt value to return if not found 343 * @return value of the environment variable 344 */ 345 private String envVarValue(final String key, final String deflt) { 346 return environment.getOrDefault(key, deflt); 347 } 348 349 /** 350 * envVarLongValue - return the value of the environment variable key as a 351 * long value. 352 * @param key name of environment variable 353 * @return long value of the environment variable 354 */ 355 private long envVarLongValue(final String key) { 356 try { 357 return Long.parseLong(envVarValue(key, "0")); 358 } catch (NumberFormatException ex) { 359 return 0L; 360 } 361 } 362 363 /** 364 * envVarBooleanValue - return the value of the environment variable key as a 365 * boolean value. true if the value was non-zero, false otherwise. 366 * @param key name of environment variable 367 * @return boolean value of the environment variable 368 */ 369 private boolean envVarBooleanValue(final String key) { 370 return envVarLongValue(key) != 0; 371 } 372 373 /** 374 * stripQuotes - strip quotes from token if present. Quoted tokens kept 375 * quotes to prevent search for redirects. 376 * @param token token to strip 377 * @return stripped token 378 */ 379 private static String stripQuotes(String token) { 380 if ((token.startsWith("\"") && token.endsWith("\"")) || 381 token.startsWith("\'") && token.endsWith("\'")) { 382 token = token.substring(1, token.length() - 1); 383 } 384 return token; 385 } 386 387 /** 388 * resolvePath - resolves a path against a current working directory. 389 * @param cwd current working directory 390 * @param fileName name of file or directory 391 * @return resolved Path to file 392 */ 393 private static Path resolvePath(final String cwd, final String fileName) { 394 return Paths.get(sanitizePath(cwd)).resolve(fileName).normalize(); 395 } 396 397 /** 398 * builtIn - checks to see if the command is a builtin and performs 399 * appropriate action. 400 * @param cmd current command 401 * @param cwd current working directory 402 * @return true if was a builtin command 403 */ 404 private boolean builtIn(final List<String> cmd, final String cwd) { 405 switch (cmd.get(0)) { 406 // Set current working directory. 407 case "cd": 408 final boolean cygpath = IS_WINDOWS && cwd.startsWith(CYGDRIVE); 409 // If zero args then use home directory as cwd else use first arg. 410 final String newCWD = cmd.size() < 2 ? HOME_DIRECTORY : cmd.get(1); 411 // Normalize the cwd 412 final Path cwdPath = resolvePath(cwd, newCWD); 413 414 // Check if is a directory. 415 final File file = cwdPath.toFile(); 416 if (!file.exists()) { 417 reportError("file.not.exist", file.toString()); 418 return true; 419 } else if (!file.isDirectory()) { 420 reportError("not.directory", file.toString()); 421 return true; 422 } 423 424 // Set PWD environment variable to be picked up as cwd. 425 // Make sure Cygwin paths look like Unix paths. 426 String scwd = cwdPath.toString(); 427 if (cygpath && scwd.length() >= 2 && 428 Character.isLetter(scwd.charAt(0)) && scwd.charAt(1) == ':') { 429 scwd = CYGDRIVE + Character.toLowerCase(scwd.charAt(0)) + "/" + scwd.substring(2); 430 } 431 environment.put("PWD", scwd); 432 return true; 433 434 // Set an environment variable. 435 case "setenv": 436 if (3 <= cmd.size()) { 437 final String key = cmd.get(1); 438 final String value = cmd.get(2); 439 environment.put(key, value); 440 } 441 442 return true; 443 444 // Unset an environment variable. 445 case "unsetenv": 446 if (2 <= cmd.size()) { 447 final String key = cmd.get(1); 448 environment.remove(key); 449 } 450 451 return true; 452 } 453 454 return false; 455 } 456 457 /** 458 * preprocessCommand - scan the command for redirects, and sanitize the 459 * executable path 460 * @param tokens command tokens 461 * @param cwd current working directory 462 * @param redirectInfo redirection information 463 * @return tokens remaining for actual command 464 */ 465 private List<String> preprocessCommand(final List<String> tokens, 466 final String cwd, final RedirectInfo redirectInfo) { 467 // Tokens remaining for actual command. 468 final List<String> command = new ArrayList<>(); 469 470 // iterate through all tokens. 471 final Iterator<String> iterator = tokens.iterator(); 472 while (iterator.hasNext()) { 473 String token = iterator.next(); 474 475 // Check if is a redirect. 476 if (redirectInfo.check(token, iterator, cwd)) { 477 // Don't add to the command. 478 continue; 479 } 480 481 // Strip quotes and add to command. 482 command.add(stripQuotes(token)); 483 } 484 485 if (command.size() > 0) { 486 command.set(0, sanitizePath(command.get(0))); 487 } 488 489 return command; 490 } 491 492 /** 493 * Sanitize a path in case the underlying platform is Cygwin. In that case, 494 * convert from the {@code /cygdrive/x} drive specification to the usual 495 * Windows {@code X:} format. 496 * 497 * @param d a String representing a path 498 * @return a String representing the same path in a form that can be 499 * processed by the underlying platform 500 */ 501 private static String sanitizePath(final String d) { 502 if (!IS_WINDOWS || (IS_WINDOWS && !d.startsWith(CYGDRIVE))) { 503 return d; 504 } 505 final String pd = d.substring(CYGDRIVE.length()); 506 if (pd.length() >= 2 && pd.charAt(1) == '/') { 507 // drive letter plus / -> convert /cygdrive/x/... to X:/... 508 return pd.charAt(0) + ":" + pd.substring(1); 509 } else if (pd.length() == 1) { 510 // just drive letter -> convert /cygdrive/x to X: 511 return pd.charAt(0) + ":"; 512 } 513 // remaining case: /cygdrive/ -> can't convert 514 return d; 515 } 516 517 /** 518 * createProcessBuilder - create a ProcessBuilder for the command. 519 * @param command command tokens 520 * @param cwd current working directory 521 * @param redirectInfo redirect information 522 */ 523 private void createProcessBuilder(final List<String> command, 524 final String cwd, final RedirectInfo redirectInfo) { 525 // Create new ProcessBuilder. 526 final ProcessBuilder pb = new ProcessBuilder(command); 527 // Set current working directory. 528 pb.directory(new File(sanitizePath(cwd))); 529 530 // Map environment variables. 531 final Map<String, String> processEnvironment = pb.environment(); 532 processEnvironment.clear(); 533 processEnvironment.putAll(environment); 534 535 // Apply redirects. 536 redirectInfo.apply(pb); 537 // Add to current list of commands. 538 processBuilders.add(pb); 539 } 540 541 /** 542 * command - process the command 543 * @param tokens tokens of the command 544 * @param isPiped true if the output of this command should be piped to the next 545 */ 546 private void command(final List<String> tokens, boolean isPiped) { 547 // Test to see if we should echo the command to output. 548 if (envVarBooleanValue("JJS_ECHO")) { 549 System.out.println(String.join(" ", tokens)); 550 } 551 552 // Get the current working directory. 553 final String cwd = envVarValue("PWD", HOME_DIRECTORY); 554 // Preprocess the command for redirects. 555 final RedirectInfo redirectInfo = new RedirectInfo(); 556 final List<String> command = preprocessCommand(tokens, cwd, redirectInfo); 557 558 // Skip if empty or a built in. 559 if (command.isEmpty() || builtIn(command, cwd)) { 560 return; 561 } 562 563 // Create ProcessBuilder with cwd and redirects set. 564 createProcessBuilder(command, cwd, redirectInfo); 565 566 // If piped, wait for the next command. 567 if (isPiped) { 568 return; 569 } 570 571 // Fetch first and last ProcessBuilder. 572 final ProcessBuilder firstProcessBuilder = processBuilders.get(0); 573 final ProcessBuilder lastProcessBuilder = processBuilders.get(processBuilders.size() - 1); 574 575 // Determine which streams have not be redirected from pipes. 576 boolean inputIsPipe = firstProcessBuilder.redirectInput() == Redirect.PIPE; 577 boolean outputIsPipe = lastProcessBuilder.redirectOutput() == Redirect.PIPE; 578 boolean errorIsPipe = lastProcessBuilder.redirectError() == Redirect.PIPE; 579 boolean inheritIO = envVarBooleanValue("JJS_INHERIT_IO"); 580 581 // If not redirected and inputStream is current processes' input. 582 if (inputIsPipe && (inheritIO || inputStream == System.in)) { 583 // Inherit current processes' input. 584 firstProcessBuilder.redirectInput(Redirect.INHERIT); 585 inputIsPipe = false; 586 } 587 588 // If not redirected and outputStream is current processes' output. 589 if (outputIsPipe && (inheritIO || outputStream == System.out)) { 590 // Inherit current processes' output. 591 lastProcessBuilder.redirectOutput(Redirect.INHERIT); 592 outputIsPipe = false; 593 } 594 595 // If not redirected and errorStream is current processes' error. 596 if (errorIsPipe && (inheritIO || errorStream == System.err)) { 597 // Inherit current processes' error. 598 lastProcessBuilder.redirectError(Redirect.INHERIT); 599 errorIsPipe = false; 600 } 601 602 // Start the processes. 603 final List<Process> processes = new ArrayList<>(); 604 for (ProcessBuilder pb : processBuilders) { 605 try { 606 processes.add(pb.start()); 607 } catch (IOException ex) { 608 reportError("unknown.command", String.join(" ", pb.command())); 609 return; 610 } 611 } 612 613 // Clear processBuilders for next command. 614 processBuilders.clear(); 615 616 // Get first and last process. 617 final Process firstProcess = processes.get(0); 618 final Process lastProcess = processes.get(processes.size() - 1); 619 620 // Prepare for string based i/o if no redirection or provided streams. 621 ByteArrayOutputStream byteOutputStream = null; 622 ByteArrayOutputStream byteErrorStream = null; 623 624 // If input is not redirected. 625 if (inputIsPipe) { 626 // If inputStream other than System.in is provided. 627 if (inputStream != null) { 628 // Pipe inputStream to first process output stream. 629 new Piper(inputStream, firstProcess.getOutputStream()).start(); 630 } else { 631 // Otherwise assume an input string has been provided. 632 new Piper(new ByteArrayInputStream(inputString.getBytes()), firstProcess.getOutputStream()).start(); 633 } 634 } 635 636 // If output is not redirected. 637 if (outputIsPipe) { 638 // If outputStream other than System.out is provided. 639 if (outputStream != null ) { 640 // Pipe outputStream from last process input stream. 641 new Piper(lastProcess.getInputStream(), outputStream).start(); 642 } else { 643 // Otherwise assume an output string needs to be prepared. 644 byteOutputStream = new ByteArrayOutputStream(BUFFER_SIZE); 645 new Piper(lastProcess.getInputStream(), byteOutputStream).start(); 646 } 647 } 648 649 // If error is not redirected. 650 if (errorIsPipe) { 651 // If errorStream other than System.err is provided. 652 if (errorStream != null) { 653 new Piper(lastProcess.getErrorStream(), errorStream).start(); 654 } else { 655 // Otherwise assume an error string needs to be prepared. 656 byteErrorStream = new ByteArrayOutputStream(BUFFER_SIZE); 657 new Piper(lastProcess.getErrorStream(), byteErrorStream).start(); 658 } 659 } 660 661 // Pipe commands in between. 662 for (int i = 0, n = processes.size() - 1; i < n; i++) { 663 final Process prev = processes.get(i); 664 final Process next = processes.get(i + 1); 665 new Piper(prev.getInputStream(), next.getOutputStream()).start(); 666 } 667 668 // Wind up processes. 669 try { 670 // Get the user specified timeout. 671 long timeout = envVarLongValue("JJS_TIMEOUT"); 672 673 // If user specified timeout (milliseconds.) 674 if (timeout != 0) { 675 // Wait for last process, with timeout. 676 if (lastProcess.waitFor(timeout, TimeUnit.MILLISECONDS)) { 677 // Get exit code of last process. 678 exitCode = lastProcess.exitValue(); 679 } else { 680 reportError("timeout", Long.toString(timeout)); 681 } 682 } else { 683 // Wait for last process and get exit code. 684 exitCode = lastProcess.waitFor(); 685 } 686 687 // Accumulate the output and error streams. 688 outputString += byteOutputStream != null ? byteOutputStream.toString() : ""; 689 errorString += byteErrorStream != null ? byteErrorStream.toString() : ""; 690 } catch (InterruptedException ex) { 691 // Kill any living processes. 692 processes.stream().forEach(p -> { 693 if (p.isAlive()) { 694 p.destroy(); 695 } 696 697 // Get the first error code. 698 exitCode = exitCode == 0 ? p.exitValue() : exitCode; 699 }); 700 } 701 702 // If we got a non-zero exit code then possibly throw an exception. 703 if (exitCode != 0 && envVarBooleanValue("JJS_THROW_ON_EXIT")) { 704 throw rangeError("exec.returned.non.zero", ScriptRuntime.safeToString(exitCode)); 705 } 706 } 707 708 /** 709 * createTokenizer - build up StreamTokenizer for the command script 710 * @param script command script to parsed 711 * @return StreamTokenizer for command script 712 */ 713 private static StreamTokenizer createTokenizer(final String script) { 714 final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(script)); 715 tokenizer.resetSyntax(); 716 // Default all characters to word. 717 tokenizer.wordChars(0, 255); 718 // Spaces and special characters are white spaces. 719 tokenizer.whitespaceChars(0, ' '); 720 // Ignore # comments. 721 tokenizer.commentChar('#'); 722 // Handle double and single quote strings. 723 tokenizer.quoteChar('"'); 724 tokenizer.quoteChar('\''); 725 // Need to recognize the end of a command. 726 tokenizer.eolIsSignificant(true); 727 // Command separator. 728 tokenizer.ordinaryChar(';'); 729 // Pipe separator. 730 tokenizer.ordinaryChar('|'); 731 732 return tokenizer; 733 } 734 735 /** 736 * process - process a command string 737 * @param script command script to parsed 738 */ 739 void process(final String script) { 740 // Build up StreamTokenizer for the command script. 741 final StreamTokenizer tokenizer = createTokenizer(script); 742 743 // Prepare to accumulate command tokens. 744 final List<String> command = new ArrayList<>(); 745 // Prepare to acumulate partial tokens joined with "\ ". 746 final StringBuilder sb = new StringBuilder(); 747 748 try { 749 // Fetch next token until end of script. 750 while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) { 751 // Next word token. 752 String token = tokenizer.sval; 753 754 // If special token. 755 if (token == null) { 756 // Flush any partial token. 757 if (sb.length() != 0) { 758 command.add(sb.append(token).toString()); 759 sb.setLength(0); 760 } 761 762 // Process a completed command. 763 // Will be either ';' (command end) or '|' (pipe), true if '|'. 764 command(command, tokenizer.ttype == '|'); 765 766 if (exitCode != EXIT_SUCCESS) { 767 return; 768 } 769 770 // Start with a new set of tokens. 771 command.clear(); 772 } else if (token.endsWith("\\")) { 773 // Backslash followed by space. 774 sb.append(token.substring(0, token.length() - 1)).append(' '); 775 } else if (sb.length() == 0) { 776 // If not a word then must be a quoted string. 777 if (tokenizer.ttype != StreamTokenizer.TT_WORD) { 778 // Quote string, sb is free to use (empty.) 779 sb.append((char)tokenizer.ttype); 780 sb.append(token); 781 sb.append((char)tokenizer.ttype); 782 token = sb.toString(); 783 sb.setLength(0); 784 } 785 786 command.add(token); 787 } else { 788 // Partial token pending. 789 command.add(sb.append(token).toString()); 790 sb.setLength(0); 791 } 792 } 793 } catch (final IOException ex) { 794 // Do nothing. 795 } 796 797 // Partial token pending. 798 if (sb.length() != 0) { 799 command.add(sb.toString()); 800 } 801 802 // Process last command. 803 command(command, false); 804 } 805 806 /** 807 * process - process a command array of strings 808 * @param tokens command script to be processed 809 */ 810 void process(final List<String> tokens) { 811 // Prepare to accumulate command tokens. 812 final List<String> command = new ArrayList<>(); 813 814 // Iterate through tokens. 815 final Iterator<String> iterator = tokens.iterator(); 816 while (iterator.hasNext() && exitCode == EXIT_SUCCESS) { 817 // Next word token. 818 String token = iterator.next(); 819 820 if (token == null) { 821 continue; 822 } 823 824 switch (token) { 825 case "|": 826 // Process as a piped command. 827 command(command, true); 828 // Start with a new set of tokens. 829 command.clear(); 830 831 continue; 832 case ";": 833 // Process as a normal command. 834 command(command, false); 835 // Start with a new set of tokens. 836 command.clear(); 837 838 continue; 839 } 840 841 command.add(token); 842 } 843 844 // Process last command. 845 command(command, false); 846 } 847 848 void reportError(final String msg, final String object) { 849 errorString += ECMAErrors.getMessage("range.error.exec." + msg, object); 850 exitCode = EXIT_FAILURE; 851 } 852 853 String getOutputString() { 854 return outputString; 855 } 856 857 String getErrorString() { 858 return errorString; 859 } 860 861 int getExitCode() { 862 return exitCode; 863 } 864 865 void setEnvironment(Map<String, String> environment) { 866 this.environment = environment; 867 } 868 869 void setInputStream(InputStream inputStream) { 870 this.inputStream = inputStream; 871 } 872 873 void setInputString(String inputString) { 874 this.inputString = inputString; 875 } 876 877 void setOutputStream(OutputStream outputStream) { 878 this.outputStream = outputStream; 879 } 880 881 void setErrorStream(OutputStream errorStream) { 882 this.errorStream = errorStream; 883 } 884 }