1 /* 2 * Copyright (c) 2010, 2013, 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.tools; 27 28 import jdk.nashorn.api.scripting.NashornException; 29 import jdk.nashorn.internal.codegen.Compiler; 30 import jdk.nashorn.internal.codegen.Compiler.CompilationPhases; 31 import jdk.nashorn.internal.ir.Expression; 32 import jdk.nashorn.internal.ir.FunctionNode; 33 import jdk.nashorn.internal.ir.debug.ASTWriter; 34 import jdk.nashorn.internal.ir.debug.PrintVisitor; 35 import jdk.nashorn.internal.objects.Global; 36 import jdk.nashorn.internal.objects.NativeSymbol; 37 import jdk.nashorn.internal.parser.Parser; 38 import jdk.nashorn.internal.runtime.Context; 39 import jdk.nashorn.internal.runtime.ErrorManager; 40 import jdk.nashorn.internal.runtime.JSType; 41 import jdk.nashorn.internal.runtime.Property; 42 import jdk.nashorn.internal.runtime.ScriptEnvironment; 43 import jdk.nashorn.internal.runtime.ScriptFunction; 44 import jdk.nashorn.internal.runtime.ScriptObject; 45 import jdk.nashorn.internal.runtime.ScriptRuntime; 46 import jdk.nashorn.internal.runtime.ScriptingFunctions; 47 import jdk.nashorn.internal.runtime.Symbol; 48 import jdk.nashorn.internal.runtime.arrays.ArrayLikeIterator; 49 import jdk.nashorn.internal.runtime.options.Options; 50 51 import java.io.BufferedReader; 52 import java.io.File; 53 import java.io.FileReader; 54 import java.io.IOException; 55 import java.io.InputStream; 56 import java.io.InputStreamReader; 57 import java.io.OutputStream; 58 import java.io.PrintStream; 59 import java.io.PrintWriter; 60 import java.io.StreamTokenizer; 61 import java.io.StringReader; 62 import java.nio.file.Files; 63 import java.nio.file.Path; 64 import java.nio.file.Paths; 65 import java.util.ArrayList; 66 import java.util.Arrays; 67 import java.util.Iterator; 68 import java.util.List; 69 import java.util.Locale; 70 import java.util.ResourceBundle; 71 72 import static jdk.nashorn.internal.runtime.Source.sourceFor; 73 74 /** 75 * Command line Shell for processing JavaScript files. 76 */ 77 public class Shell implements PartialParser { 78 79 /** 80 * Resource name for properties file 81 */ 82 private static final String MESSAGE_RESOURCE = "jdk.nashorn.tools.resources.Shell"; 83 /** 84 * Shell message bundle. 85 */ 86 protected static final ResourceBundle bundle = ResourceBundle.getBundle(MESSAGE_RESOURCE, Locale.getDefault()); 87 88 /** 89 * Exit code for command line tool - successful 90 */ 91 public static final int SUCCESS = 0; 92 /** 93 * Exit code for command line tool - error on command line 94 */ 95 public static final int COMMANDLINE_ERROR = 100; 96 /** 97 * Exit code for command line tool - error compiling script 98 */ 99 public static final int COMPILATION_ERROR = 101; 100 /** 101 * Exit code for command line tool - error during runtime 102 */ 103 public static final int RUNTIME_ERROR = 102; 104 /** 105 * Exit code for command line tool - i/o error 106 */ 107 public static final int IO_ERROR = 103; 108 /** 109 * Exit code for command line tool - internal error 110 */ 111 public static final int INTERNAL_ERROR = 104; 112 113 /** 114 * Constructor 115 */ 116 protected Shell() { 117 } 118 119 /** 120 * Main entry point with the default input, output and error streams. 121 * 122 * @param args The command line arguments 123 */ 124 public static void main(final String[] args) { 125 try { 126 final int exitCode = main(System.in, System.out, System.err, args); 127 if (exitCode != SUCCESS) { 128 System.exit(exitCode); 129 } 130 } catch (final IOException e) { 131 System.err.println(e); //bootstrapping, Context.err may not exist 132 System.exit(IO_ERROR); 133 } 134 } 135 136 /** 137 * Starting point for executing a {@code Shell}. Starts a shell with the 138 * given arguments and streams and lets it run until exit. 139 * 140 * @param in input stream for Shell 141 * @param out output stream for Shell 142 * @param err error stream for Shell 143 * @param args arguments to Shell 144 * 145 * @return exit code 146 * 147 * @throws IOException if there's a problem setting up the streams 148 */ 149 public static int main(final InputStream in, final OutputStream out, final OutputStream err, final String[] args) throws IOException { 150 return new Shell().run(in, out, err, args); 151 } 152 153 /** 154 * Run method logic. 155 * 156 * @param in input stream for Shell 157 * @param out output stream for Shell 158 * @param err error stream for Shell 159 * @param args arguments to Shell 160 * 161 * @return exit code 162 * 163 * @throws IOException if there's a problem setting up the streams 164 */ 165 protected final int run(final InputStream in, final OutputStream out, final OutputStream err, final String[] args) throws IOException { 166 final Context context = makeContext(in, out, err, args); 167 if (context == null) { 168 return COMMANDLINE_ERROR; 169 } 170 171 final Global global = context.createGlobal(); 172 final ScriptEnvironment env = context.getEnv(); 173 if (!env._no_deprecation_warning) { 174 System.err.println("Warning: The jjs tool is planned to be removed from a future JDK release"); 175 } 176 final List<String> files = env.getFiles(); 177 if (files.isEmpty()) { 178 return readEvalPrint(context, global); 179 } 180 181 if (env._compile_only) { 182 return compileScripts(context, global, files); 183 } 184 185 if (env._fx) { 186 return runFXScripts(context, global, files); 187 } 188 189 return runScripts(context, global, files); 190 } 191 192 /** 193 * Make a new Nashorn Context to compile and/or run JavaScript files. 194 * 195 * @param in input stream for Shell 196 * @param out output stream for Shell 197 * @param err error stream for Shell 198 * @param args arguments to Shell 199 * 200 * @return null if there are problems with option parsing. 201 */ 202 private static Context makeContext(final InputStream in, final OutputStream out, final OutputStream err, final String[] args) { 203 final PrintStream pout = out instanceof PrintStream ? (PrintStream) out : new PrintStream(out); 204 final PrintStream perr = err instanceof PrintStream ? (PrintStream) err : new PrintStream(err); 205 final PrintWriter wout = new PrintWriter(pout, true); 206 final PrintWriter werr = new PrintWriter(perr, true); 207 208 // Set up error handler. 209 final ErrorManager errors = new ErrorManager(werr); 210 // Set up options. 211 final Options options = new Options("nashorn", werr); 212 213 // parse options 214 if (args != null) { 215 try { 216 final String[] prepArgs = preprocessArgs(args); 217 options.process(prepArgs); 218 } catch (final IllegalArgumentException e) { 219 werr.println(bundle.getString("shell.usage")); 220 options.displayHelp(e); 221 return null; 222 } 223 } 224 225 // detect scripting mode by any source's first character being '#' 226 if (!options.getBoolean("scripting")) { 227 for (final String fileName : options.getFiles()) { 228 final File firstFile = new File(fileName); 229 if (firstFile.isFile()) { 230 try (final FileReader fr = new FileReader(firstFile)) { 231 final int firstChar = fr.read(); 232 // starts with '# 233 if (firstChar == '#') { 234 options.set("scripting", true); 235 break; 236 } 237 } catch (final IOException e) { 238 // ignore this. File IO errors will be reported later anyway 239 } 240 } 241 } 242 } 243 244 return new Context(options, errors, wout, werr, Thread.currentThread().getContextClassLoader()); 245 } 246 247 /** 248 * Preprocess the command line arguments passed in by the shell. This method checks, for the first non-option 249 * argument, whether the file denoted by it begins with a shebang line. If so, it is assumed that execution in 250 * shebang mode is intended. The consequence of this is that the identified script file will be treated as the 251 * <em>only</em> script file, and all subsequent arguments will be regarded as arguments to the script. 252 * <p> 253 * This method canonicalizes the command line arguments to the form {@code <options> <script> -- <arguments>} if a 254 * shebang script is identified. On platforms that pass shebang arguments as single strings, the shebang arguments 255 * will be broken down into single arguments; whitespace is used as separator. 256 * <p> 257 * Shebang mode is entered regardless of whether the script is actually run directly from the shell, or indirectly 258 * via the {@code jjs} executable. It is the user's / script author's responsibility to ensure that the arguments 259 * given on the shebang line do not lead to a malformed argument sequence. In particular, the shebang arguments 260 * should not contain any whitespace for purposes other than separating arguments, as the different platforms deal 261 * with whitespace in different and incompatible ways. 262 * <p> 263 * @implNote Example:<ul> 264 * <li>Shebang line in {@code script.js}: {@code #!/path/to/jjs --language=es6}</li> 265 * <li>Command line: {@code ./script.js arg2}</li> 266 * <li>{@code args} array passed to Nashorn: {@code --language=es6,./script.js,arg}</li> 267 * <li>Required canonicalized arguments array: {@code --language=es6,./script.js,--,arg2}</li> 268 * </ul> 269 * 270 * @param args the command line arguments as passed into Nashorn. 271 * @return the passed and possibly canonicalized argument list 272 */ 273 private static String[] preprocessArgs(final String[] args) { 274 if (args.length == 0) { 275 return args; 276 } 277 278 final List<String> processedArgs = new ArrayList<>(); 279 processedArgs.addAll(Arrays.asList(args)); 280 281 // Nashorn supports passing multiple shebang arguments. On platforms that pass anything following the 282 // shebang interpreter notice as one argument, the first element of the argument array needs to be special-cased 283 // as it might actually contain several arguments. Mac OS X splits shebang arguments, other platforms don't. 284 // This special handling is also only necessary if the first argument actually starts with an option. 285 if (args[0].startsWith("-") && !System.getProperty("os.name", "generic").startsWith("Mac OS X")) { 286 processedArgs.addAll(0, tokenizeString(processedArgs.remove(0))); 287 } 288 289 int shebangFilePos = -1; // -1 signifies "none found" 290 // identify a shebang file and its position in the arguments array (if any) 291 for (int i = 0; i < processedArgs.size(); ++i) { 292 final String a = processedArgs.get(i); 293 if (!a.startsWith("-")) { 294 final Path p = Paths.get(a); 295 String l = ""; 296 try (final BufferedReader r = Files.newBufferedReader(p)) { 297 l = r.readLine(); 298 } catch (final IOException ioe) { 299 // ignore 300 } 301 if (l != null && l.startsWith("#!")) { 302 shebangFilePos = i; 303 } 304 // We're only checking the first non-option argument. If it's not a shebang file, we're in normal 305 // execution mode. 306 break; 307 } 308 } 309 if (shebangFilePos != -1) { 310 // Insert the argument separator after the shebang script file. 311 processedArgs.add(shebangFilePos + 1, "--"); 312 } 313 return processedArgs.stream().toArray(String[]::new); 314 } 315 316 public static List<String> tokenizeString(final String str) { 317 final StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(str)); 318 tokenizer.resetSyntax(); 319 tokenizer.wordChars(0, 255); 320 tokenizer.whitespaceChars(0, ' '); 321 tokenizer.commentChar('#'); 322 tokenizer.quoteChar('"'); 323 tokenizer.quoteChar('\''); 324 final List<String> tokenList = new ArrayList<>(); 325 final StringBuilder toAppend = new StringBuilder(); 326 while (nextToken(tokenizer) != StreamTokenizer.TT_EOF) { 327 final String s = tokenizer.sval; 328 // The tokenizer understands about honoring quoted strings and recognizes 329 // them as one token that possibly contains multiple space-separated words. 330 // It does not recognize quoted spaces, though, and will split after the 331 // escaping \ character. This is handled here. 332 if (s.endsWith("\\")) { 333 // omit trailing \, append space instead 334 toAppend.append(s.substring(0, s.length() - 1)).append(' '); 335 } else { 336 tokenList.add(toAppend.append(s).toString()); 337 toAppend.setLength(0); 338 } 339 } 340 if (toAppend.length() != 0) { 341 tokenList.add(toAppend.toString()); 342 } 343 return tokenList; 344 } 345 346 private static int nextToken(final StreamTokenizer tokenizer) { 347 try { 348 return tokenizer.nextToken(); 349 } catch (final IOException ioe) { 350 return StreamTokenizer.TT_EOF; 351 } 352 } 353 354 /** 355 * Compiles the given script files in the command line 356 * This is called only when using the --compile-only flag 357 * 358 * @param context the nashorn context 359 * @param global the global scope 360 * @param files the list of script files to compile 361 * 362 * @return error code 363 * @throws IOException when any script file read results in I/O error 364 */ 365 private static int compileScripts(final Context context, final Global global, final List<String> files) throws IOException { 366 final Global oldGlobal = Context.getGlobal(); 367 final boolean globalChanged = (oldGlobal != global); 368 final ScriptEnvironment env = context.getEnv(); 369 try { 370 if (globalChanged) { 371 Context.setGlobal(global); 372 } 373 final ErrorManager errors = context.getErrorManager(); 374 375 // For each file on the command line. 376 for (final String fileName : files) { 377 final FunctionNode functionNode = new Parser(env, sourceFor(fileName, new File(fileName)), errors, env._strict, 0, context.getLogger(Parser.class)).parse(); 378 379 if (errors.getNumberOfErrors() != 0) { 380 return COMPILATION_ERROR; 381 } 382 383 Compiler.forNoInstallerCompilation( 384 context, 385 functionNode.getSource(), 386 env._strict | functionNode.isStrict()). 387 compile(functionNode, CompilationPhases.COMPILE_ALL_NO_INSTALL); 388 389 if (env._print_ast) { 390 context.getErr().println(new ASTWriter(functionNode)); 391 } 392 393 if (env._print_parse) { 394 context.getErr().println(new PrintVisitor(functionNode)); 395 } 396 397 if (errors.getNumberOfErrors() != 0) { 398 return COMPILATION_ERROR; 399 } 400 } 401 } finally { 402 env.getOut().flush(); 403 env.getErr().flush(); 404 if (globalChanged) { 405 Context.setGlobal(oldGlobal); 406 } 407 } 408 409 return SUCCESS; 410 } 411 412 /** 413 * Runs the given JavaScript files in the command line 414 * 415 * @param context the nashorn context 416 * @param global the global scope 417 * @param files the list of script files to run 418 * 419 * @return error code 420 * @throws IOException when any script file read results in I/O error 421 */ 422 private int runScripts(final Context context, final Global global, final List<String> files) throws IOException { 423 final Global oldGlobal = Context.getGlobal(); 424 final boolean globalChanged = (oldGlobal != global); 425 try { 426 if (globalChanged) { 427 Context.setGlobal(global); 428 } 429 final ErrorManager errors = context.getErrorManager(); 430 431 // For each file on the command line. 432 for (final String fileName : files) { 433 if ("-".equals(fileName)) { 434 final int res = readEvalPrint(context, global); 435 if (res != SUCCESS) { 436 return res; 437 } 438 continue; 439 } 440 441 final File file = new File(fileName); 442 final ScriptFunction script = context.compileScript(sourceFor(fileName, file), global); 443 if (script == null || errors.getNumberOfErrors() != 0) { 444 if (context.getEnv()._parse_only && !errors.hasErrors()) { 445 continue; // No error, continue to consume all files in list 446 } 447 return COMPILATION_ERROR; 448 } 449 450 try { 451 apply(script, global); 452 } catch (final NashornException e) { 453 errors.error(e.toString()); 454 if (context.getEnv()._dump_on_error) { 455 e.printStackTrace(context.getErr()); 456 } 457 458 return RUNTIME_ERROR; 459 } 460 } 461 } finally { 462 context.getOut().flush(); 463 context.getErr().flush(); 464 if (globalChanged) { 465 Context.setGlobal(oldGlobal); 466 } 467 } 468 469 return SUCCESS; 470 } 471 472 /** 473 * Runs launches "fx:bootstrap.js" with the given JavaScript files provided 474 * as arguments. 475 * 476 * @param context the nashorn context 477 * @param global the global scope 478 * @param files the list of script files to provide 479 * 480 * @return error code 481 * @throws IOException when any script file read results in I/O error 482 */ 483 private static int runFXScripts(final Context context, final Global global, final List<String> files) throws IOException { 484 final Global oldGlobal = Context.getGlobal(); 485 final boolean globalChanged = (oldGlobal != global); 486 try { 487 if (globalChanged) { 488 Context.setGlobal(global); 489 } 490 491 global.addOwnProperty("$GLOBAL", Property.NOT_ENUMERABLE, global); 492 global.addOwnProperty("$SCRIPTS", Property.NOT_ENUMERABLE, files); 493 context.load(global, "fx:bootstrap.js"); 494 } catch (final NashornException e) { 495 context.getErrorManager().error(e.toString()); 496 if (context.getEnv()._dump_on_error) { 497 e.printStackTrace(context.getErr()); 498 } 499 500 return RUNTIME_ERROR; 501 } finally { 502 context.getOut().flush(); 503 context.getErr().flush(); 504 if (globalChanged) { 505 Context.setGlobal(oldGlobal); 506 } 507 } 508 509 return SUCCESS; 510 } 511 512 /** 513 * Hook to ScriptFunction "apply". A performance metering shell may 514 * introduce enter/exit timing here. 515 * 516 * @param target target function for apply 517 * @param self self reference for apply 518 * 519 * @return result of the function apply 520 */ 521 protected Object apply(final ScriptFunction target, final Object self) { 522 return ScriptRuntime.apply(target, self); 523 } 524 525 /** 526 * Parse potentially partial code and keep track of the start of last expression. 527 * This 'partial' parsing support is meant to be used for code-completion. 528 * 529 * @param context the nashorn context 530 * @param code code that is to be parsed 531 * @return the start index of the last expression parsed in the (incomplete) code. 532 */ 533 @Override 534 public final int getLastExpressionStart(final Context context, final String code) { 535 final int[] exprStart = { -1 }; 536 537 final Parser p = new Parser(context.getEnv(), sourceFor("<partial_code>", code),new Context.ThrowErrorManager()) { 538 @Override 539 protected Expression expression() { 540 exprStart[0] = this.start; 541 return super.expression(); 542 } 543 544 @Override 545 protected Expression assignmentExpression(final boolean noIn) { 546 exprStart[0] = this.start; 547 return super.assignmentExpression(noIn); 548 } 549 }; 550 551 try { 552 p.parse(); 553 } catch (final Exception ignored) { 554 // throw any parser exception, but we are partial parsing anyway 555 } 556 557 return exprStart[0]; 558 } 559 560 561 /** 562 * read-eval-print loop for Nashorn shell. 563 * 564 * @param context the nashorn context 565 * @param global global scope object to use 566 * @return return code 567 */ 568 protected int readEvalPrint(final Context context, final Global global) { 569 final String prompt = bundle.getString("shell.prompt"); 570 final BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); 571 final PrintWriter err = context.getErr(); 572 final Global oldGlobal = Context.getGlobal(); 573 final boolean globalChanged = (oldGlobal != global); 574 final ScriptEnvironment env = context.getEnv(); 575 576 try { 577 if (globalChanged) { 578 Context.setGlobal(global); 579 } 580 581 global.addShellBuiltins(); 582 583 while (true) { 584 err.print(prompt); 585 err.flush(); 586 587 String source = ""; 588 try { 589 source = in.readLine(); 590 } catch (final IOException ioe) { 591 err.println(ioe.toString()); 592 } 593 594 if (source == null) { 595 break; 596 } 597 598 if (source.isEmpty()) { 599 continue; 600 } 601 602 try { 603 final Object res = context.eval(global, source, global, "<shell>"); 604 if (res != ScriptRuntime.UNDEFINED) { 605 err.println(toString(res, global)); 606 } 607 } catch (final Exception e) { 608 err.println(e); 609 if (env._dump_on_error) { 610 e.printStackTrace(err); 611 } 612 } 613 } 614 } finally { 615 if (globalChanged) { 616 Context.setGlobal(oldGlobal); 617 } 618 } 619 620 return SUCCESS; 621 } 622 623 /** 624 * Converts {@code result} to a printable string. The reason we don't use {@link JSType#toString(Object)} 625 * or {@link ScriptRuntime#safeToString(Object)} is that we want to be able to render Symbol values 626 * even if they occur within an Array, and therefore have to implement our own Array to String 627 * conversion. 628 * 629 * @param result the result 630 * @param global the global object 631 * @return the string representation 632 */ 633 protected static String toString(final Object result, final Global global) { 634 if (result instanceof Symbol) { 635 // Normal implicit conversion of symbol to string would throw TypeError 636 return result.toString(); 637 } 638 639 if (result instanceof NativeSymbol) { 640 return JSType.toPrimitive(result).toString(); 641 } 642 643 if (isArrayWithDefaultToString(result, global)) { 644 // This should yield the same string as Array.prototype.toString but 645 // will not throw if the array contents include symbols. 646 final StringBuilder sb = new StringBuilder(); 647 final Iterator<Object> iter = ArrayLikeIterator.arrayLikeIterator(result, true); 648 649 while (iter.hasNext()) { 650 final Object obj = iter.next(); 651 652 if (obj != null && obj != ScriptRuntime.UNDEFINED) { 653 sb.append(toString(obj, global)); 654 } 655 656 if (iter.hasNext()) { 657 sb.append(','); 658 } 659 } 660 661 return sb.toString(); 662 } 663 664 return JSType.toString(result); 665 } 666 667 private static boolean isArrayWithDefaultToString(final Object result, final Global global) { 668 if (result instanceof ScriptObject) { 669 final ScriptObject sobj = (ScriptObject) result; 670 return sobj.isArray() && sobj.get("toString") == global.getArrayPrototype().get("toString"); 671 } 672 return false; 673 } 674 }