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