< prev index next >

src/jdk.scripting.nashorn/share/classes/jdk/nashorn/internal/runtime/CommandExecutor.java

Print this page
rev 1636 : 8151291: $EXEC yields "unknown command" on Cygwin
Reviewed-by: jlaskey, hannesw, sdama


  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     // User's home directory
  68     private static final String HOME_DIRECTORY =
  69         AccessController.doPrivileged((PrivilegedAction<String>)() -> {
  70         return System.getProperty("user.home");
  71     });
  72 
  73     // Various types of standard redirects.
  74     enum RedirectType {
  75         NO_REDIRECT,
  76         REDIRECT_INPUT,
  77         REDIRECT_OUTPUT,
  78         REDIRECT_OUTPUT_APPEND,
  79         REDIRECT_ERROR,
  80         REDIRECT_ERROR_APPEND,
  81         REDIRECT_OUTPUT_ERROR_APPEND,
  82         REDIRECT_ERROR_TO_OUTPUT
  83     };
  84 
  85     // Prefix strings to standard redirects.
  86     private static final String[] redirectPrefixes = new String[] {


 371      * stripQuotes - strip quotes from token if present. Quoted tokens kept
 372      * quotes to prevent search for redirects.
 373      * @param token token to strip
 374      * @return stripped token
 375      */
 376     private static String stripQuotes(String token) {
 377         if ((token.startsWith("\"") && token.endsWith("\"")) ||
 378              token.startsWith("\'") && token.endsWith("\'")) {
 379             token = token.substring(1, token.length() - 1);
 380         }
 381         return token;
 382     }
 383 
 384     /**
 385      * resolvePath - resolves a path against a current working directory.
 386      * @param cwd      current working directory
 387      * @param fileName name of file or directory
 388      * @return resolved Path to file
 389      */
 390     private static Path resolvePath(final String cwd, final String fileName) {
 391         return Paths.get(cwd).resolve(fileName).normalize();
 392     }
 393 
 394     /**
 395      * builtIn - checks to see if the command is a builtin and performs
 396      * appropriate action.
 397      * @param cmd current command
 398      * @param cwd current working directory
 399      * @return true if was a builtin command
 400      */
 401     private boolean builtIn(final List<String> cmd, final String cwd) {
 402         switch (cmd.get(0)) {
 403             // Set current working directory.
 404             case "cd":
 405                 // If zero args then use home dirrectory as cwd else use first arg.

 406                 final String newCWD = cmd.size() < 2 ? HOME_DIRECTORY : cmd.get(1);
 407                 // Normalize the cwd
 408                 final Path cwdPath = resolvePath(cwd, newCWD);
 409 
 410                 // Check if is a directory.
 411                 final File file = cwdPath.toFile();
 412                 if (!file.exists()) {
 413                     reportError("file.not.exist", file.toString());
 414                     return true;
 415                 } else if (!file.isDirectory()) {
 416                     reportError("not.directory", file.toString());
 417                     return true;
 418                 }
 419 
 420                 // Set PWD environment variable to be picked up as cwd.
 421                 environment.put("PWD", cwdPath.toString());






 422                 return true;
 423 
 424             // Set an environment variable.
 425             case "setenv":
 426                 if (3 <= cmd.size()) {
 427                     final String key = cmd.get(1);
 428                     final String value = cmd.get(2);
 429                     environment.put(key, value);
 430                 }
 431 
 432                 return true;
 433 
 434             // Unset an environment variable.
 435             case "unsetenv":
 436                 if (2 <= cmd.size()) {
 437                     final String key = cmd.get(1);
 438                     environment.remove(key);
 439                 }
 440 
 441                 return true;
 442         }
 443 
 444         return false;
 445     }
 446 
 447     /**
 448      * preprocessCommand - scan the command for redirects

 449      * @param tokens       command tokens
 450      * @param cwd          current working directory
 451      * @param redirectInfo redirection information
 452      * @return tokens remaining for actual command
 453      */
 454     private List<String>  preprocessCommand(final List<String> tokens,
 455             final String cwd, final RedirectInfo redirectInfo) {
 456         // Tokens remaining for actual command.
 457         final List<String> command = new ArrayList<>();
 458 
 459         // iterate through all tokens.
 460         final Iterator<String> iterator = tokens.iterator();
 461         while (iterator.hasNext()) {
 462             String token = iterator.next();
 463 
 464             // Check if is a redirect.
 465             if (redirectInfo.check(token, iterator, cwd)) {
 466                 // Don't add to the command.
 467                 continue;
 468             }
 469 
 470             // Strip quotes and add to command.
 471             command.add(stripQuotes(token));
 472         }
 473 




 474         return command;
 475     }
 476 
 477     /**

























 478      * createProcessBuilder - create a ProcessBuilder for the command.
 479      * @param command      command tokens
 480      * @param cwd          current working directory
 481      * @param redirectInfo redirect information
 482      */
 483     private void createProcessBuilder(final List<String> command,
 484             final String cwd, final RedirectInfo redirectInfo) {
 485         // Create new ProcessBuilder.
 486         final ProcessBuilder pb = new ProcessBuilder(command);
 487         // Set current working directory.
 488         pb.directory(new File(cwd));
 489 
 490         // Map environment variables.
 491         final Map<String, String> processEnvironment = pb.environment();
 492         processEnvironment.clear();
 493         processEnvironment.putAll(environment);
 494 
 495         // Apply redirects.
 496         redirectInfo.apply(pb);
 497         // Add to current list of commands.
 498         processBuilders.add(pb);
 499     }
 500 
 501     /**
 502      * command - process the command
 503      * @param tokens  tokens of the command
 504      * @param isPiped true if the output of this command should be piped to the next
 505      */
 506     private void command(final List<String> tokens, boolean isPiped) {
 507         // Test to see if we should echo the command to output.
 508         if (envVarBooleanValue("JJS_ECHO")) {
 509             System.out.println(String.join(" ", tokens));
 510         }
 511 
 512         // Get the current working directory.
 513         final String cwd = envVarValue("PWD", HOME_DIRECTORY);
 514         // Preprocess the command for redirects.
 515         final RedirectInfo redirectInfo = new RedirectInfo();
 516         final List<String> command = preprocessCommand(tokens, cwd, redirectInfo);
 517 
 518         // Skip if empty or a built in.
 519         if (command.isEmpty() || builtIn(command, cwd)) {
 520             return;
 521         }
 522 
 523         // Create ProcessBuilder with cwd and redirects set.
 524         createProcessBuilder(command, cwd, redirectInfo);
 525 
 526         // If piped the wait for the next command.
 527         if (isPiped) {
 528             return;
 529         }
 530 
 531         // Fetch first and last ProcessBuilder.
 532         final ProcessBuilder firstProcessBuilder = processBuilders.get(0);
 533         final ProcessBuilder lastProcessBuilder = processBuilders.get(processBuilders.size() - 1);
 534 
 535         // Determine which streams have not be redirected from pipes.
 536         boolean inputIsPipe = firstProcessBuilder.redirectInput() == Redirect.PIPE;
 537         boolean outputIsPipe = lastProcessBuilder.redirectOutput() == Redirect.PIPE;
 538         boolean errorIsPipe = lastProcessBuilder.redirectError() == Redirect.PIPE;
 539         boolean inheritIO = envVarBooleanValue("JJS_INHERIT_IO");
 540 
 541         // If not redirected and inputStream is current processes' input.
 542         if (inputIsPipe && (inheritIO || inputStream == System.in)) {
 543             // Inherit current processes' input.
 544             firstProcessBuilder.redirectInput(Redirect.INHERIT);
 545             inputIsPipe = false;
 546         }


 748                     // Partial token pending.
 749                     command.add(sb.append(token).toString());
 750                     sb.setLength(0);
 751                 }
 752             }
 753         } catch (final IOException ex) {
 754             // Do nothing.
 755         }
 756 
 757         // Partial token pending.
 758         if (sb.length() != 0) {
 759             command.add(sb.toString());
 760         }
 761 
 762         // Process last command.
 763         command(command, false);
 764     }
 765 
 766     /**
 767      * process - process a command array of strings
 768      * @param script command script to be processed
 769      */
 770     void process(final List<String> tokens) {
 771         // Prepare to accumulate command tokens.
 772         final List<String> command = new ArrayList<>();
 773 
 774         // Iterate through tokens.
 775         final Iterator<String> iterator = tokens.iterator();
 776         while (iterator.hasNext() && exitCode == EXIT_SUCCESS) {
 777             // Next word token.
 778             String token = iterator.next();
 779 
 780             if (token == null) {
 781                 continue;
 782             }
 783 
 784             switch (token) {
 785                 case "|":
 786                     // Process as a piped command.
 787                     command(command, true);
 788                     // Start with a new set of tokens.




  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[] {


 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         }


 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.


< prev index next >