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.util.List;
  40 import java.util.Locale;
  41 import java.util.ResourceBundle;
  42 import jdk.nashorn.api.scripting.NashornException;
  43 import jdk.nashorn.internal.codegen.Compiler;
  44 import jdk.nashorn.internal.ir.FunctionNode;
  45 import jdk.nashorn.internal.ir.debug.ASTWriter;
  46 import jdk.nashorn.internal.ir.debug.PrintVisitor;
  47 import jdk.nashorn.internal.objects.Global;
  48 import jdk.nashorn.internal.parser.Parser;
  49 import jdk.nashorn.internal.runtime.Context;
  50 import jdk.nashorn.internal.runtime.ErrorManager;
  51 import jdk.nashorn.internal.runtime.JSType;
  52 import jdk.nashorn.internal.runtime.Property;
  53 import jdk.nashorn.internal.runtime.ScriptEnvironment;
  54 import jdk.nashorn.internal.runtime.ScriptFunction;
  55 import jdk.nashorn.internal.runtime.ScriptRuntime;
  56 import jdk.nashorn.internal.runtime.Source;
  57 import jdk.nashorn.internal.runtime.options.Options;
  58 
  59 /**
  60  * Command line Shell for processing JavaScript files.
  61  */
  62 public class Shell {
  63 
  64     /**
  65      * Resource name for properties file
  66      */
  67     private static final String MESSAGE_RESOURCE = "jdk.nashorn.tools.resources.Shell";
  68     /**
  69      * Shell message bundle.
  70      */
  71     private static final ResourceBundle bundle = ResourceBundle.getBundle(MESSAGE_RESOURCE, Locale.getDefault());
  72 
  73     /**
  74      * Exit code for command line tool - successful
  75      */
  76     public static final int SUCCESS = 0;
  77     /**
  78      * Exit code for command line tool - error on command line
  79      */
  80     public static final int COMMANDLINE_ERROR = 100;
  81     /**
  82      * Exit code for command line tool - error compiling script
  83      */
  84     public static final int COMPILATION_ERROR = 101;
  85     /**
  86      * Exit code for command line tool - error during runtime
  87      */
  88     public static final int RUNTIME_ERROR = 102;
  89     /**
  90      * Exit code for command line tool - i/o error
  91      */
  92     public static final int IO_ERROR = 103;
  93     /**
  94      * Exit code for command line tool - internal error
  95      */
  96     public static final int INTERNAL_ERROR = 104;
  97 
  98     /**
  99      * Constructor
 100      */
 101     protected Shell() {
 102     }
 103 
 104     /**
 105      * Main entry point with the default input, output and error streams.
 106      *
 107      * @param args The command line arguments
 108      */
 109     public static void main(final String[] args) {
 110         try {
 111             System.exit(main(System.in, System.out, System.err, args));
 112         } catch (final IOException e) {
 113             System.err.println(e); //bootstrapping, Context.err may not exist
 114             System.exit(IO_ERROR);
 115         }
 116     }
 117 
 118     /**
 119      * Starting point for executing a {@code Shell}. Starts a shell with the
 120      * given arguments and streams and lets it run until exit.
 121      *
 122      * @param in input stream for Shell
 123      * @param out output stream for Shell
 124      * @param err error stream for Shell
 125      * @param args arguments to Shell
 126      *
 127      * @return exit code
 128      *
 129      * @throws IOException if there's a problem setting up the streams
 130      */
 131     public static int main(final InputStream in, final OutputStream out, final OutputStream err, final String[] args) throws IOException {
 132         return new Shell().run(in, out, err, args);
 133     }
 134 
 135     /**
 136      * Run method logic.
 137      *
 138      * @param in input stream for Shell
 139      * @param out output stream for Shell
 140      * @param err error stream for Shell
 141      * @param args arguments to Shell
 142      *
 143      * @return exit code
 144      *
 145      * @throws IOException if there's a problem setting up the streams
 146      */
 147     protected final int run(final InputStream in, final OutputStream out, final OutputStream err, final String[] args) throws IOException {
 148         final Context context = makeContext(in, out, err, args);
 149         if (context == null) {
 150             return COMMANDLINE_ERROR;
 151         }
 152 
 153         final Global global = context.createGlobal();
 154         final ScriptEnvironment env = context.getEnv();
 155         final List<String> files = env.getFiles();
 156         if (files.isEmpty()) {
 157             return readEvalPrint(context, global);
 158         }
 159 
 160         if (env._compile_only) {
 161             return compileScripts(context, global, files);
 162         }
 163 
 164         if (env._fx) {
 165             return runFXScripts(context, global, files);
 166         }
 167 
 168         return runScripts(context, global, files);
 169     }
 170 
 171     /**
 172      * Make a new Nashorn Context to compile and/or run JavaScript files.
 173      *
 174      * @param in input stream for Shell
 175      * @param out output stream for Shell
 176      * @param err error stream for Shell
 177      * @param args arguments to Shell
 178      *
 179      * @return null if there are problems with option parsing.
 180      */
 181     @SuppressWarnings("resource")
 182     private static Context makeContext(final InputStream in, final OutputStream out, final OutputStream err, final String[] args) {
 183         final PrintStream pout = out instanceof PrintStream ? (PrintStream) out : new PrintStream(out);
 184         final PrintStream perr = err instanceof PrintStream ? (PrintStream) err : new PrintStream(err);
 185         final PrintWriter wout = new PrintWriter(pout, true);
 186         final PrintWriter werr = new PrintWriter(perr, true);
 187 
 188         // Set up error handler.
 189         final ErrorManager errors = new ErrorManager(werr);
 190         // Set up options.
 191         final Options options = new Options("nashorn", werr);
 192 
 193         // parse options
 194         if (args != null) {
 195             try {
 196                 options.process(args);
 197             } catch (final IllegalArgumentException e) {
 198                 werr.println(bundle.getString("shell.usage"));
 199                 options.displayHelp(e);
 200                 return null;
 201             }
 202         }
 203 
 204         // detect scripting mode by any source's first character being '#'
 205         if (!options.getBoolean("scripting")) {
 206             for (final String fileName : options.getFiles()) {
 207                 final File firstFile = new File(fileName);
 208                 if (firstFile.isFile()) {
 209                     try (final FileReader fr = new FileReader(firstFile)) {
 210                         final int firstChar = fr.read();
 211                         // starts with '#
 212                         if (firstChar == '#') {
 213                             options.set("scripting", true);
 214                             break;
 215                         }
 216                     } catch (final IOException e) {
 217                         // ignore this. File IO errors will be reported later anyway
 218                     }
 219                 }
 220             }
 221         }
 222 
 223         return new Context(options, errors, wout, werr, Thread.currentThread().getContextClassLoader());
 224     }
 225 
 226     /**
 227      * Compiles the given script files in the command line
 228      *
 229      * @param context the nashorn context
 230      * @param global the global scope
 231      * @param files the list of script files to compile
 232      *
 233      * @return error code
 234      * @throws IOException when any script file read results in I/O error
 235      */
 236     private static int compileScripts(final Context context, final Global global, final List<String> files) throws IOException {
 237         final Global oldGlobal = Context.getGlobal();
 238         final boolean globalChanged = (oldGlobal != global);
 239         final ScriptEnvironment env = context.getEnv();
 240         try {
 241             if (globalChanged) {
 242                 Context.setGlobal(global);
 243             }
 244             final ErrorManager errors = context.getErrorManager();
 245 
 246             // For each file on the command line.
 247             for (final String fileName : files) {
 248                 final FunctionNode functionNode = new Parser(env, sourceFor(fileName, new File(fileName)), errors).parse();
 249 
 250                 if (errors.getNumberOfErrors() != 0) {
 251                     return COMPILATION_ERROR;
 252                 }
 253 
 254                 if (env._print_ast) {
 255                     context.getErr().println(new ASTWriter(functionNode));
 256                 }
 257 
 258                 if (env._print_parse) {
 259                     context.getErr().println(new PrintVisitor(functionNode));
 260                 }
 261 
 262                 //null - pass no code installer - this is compile only
 263                 new Compiler(env).compile(functionNode);
 264             }
 265         } finally {
 266             env.getOut().flush();
 267             env.getErr().flush();
 268             if (globalChanged) {
 269                 Context.setGlobal(oldGlobal);
 270             }
 271         }
 272 
 273         return SUCCESS;
 274     }
 275 
 276     /**
 277      * Runs the given JavaScript files in the command line
 278      *
 279      * @param context the nashorn context
 280      * @param global the global scope
 281      * @param files the list of script files to run
 282      *
 283      * @return error code
 284      * @throws IOException when any script file read results in I/O error
 285      */
 286     private int runScripts(final Context context, final Global global, final List<String> files) throws IOException {
 287         final Global oldGlobal = Context.getGlobal();
 288         final boolean globalChanged = (oldGlobal != global);
 289         try {
 290             if (globalChanged) {
 291                 Context.setGlobal(global);
 292             }
 293             final ErrorManager errors = context.getErrorManager();
 294 
 295             // For each file on the command line.
 296             for (final String fileName : files) {
 297                 if ("-".equals(fileName)) {
 298                     final int res = readEvalPrint(context, global);
 299                     if (res != SUCCESS) {
 300                         return res;
 301                     }
 302                     continue;
 303                 }
 304 
 305                 final File file = new File(fileName);
 306                 final ScriptFunction script = context.compileScript(sourceFor(fileName, file), global);
 307                 if (script == null || errors.getNumberOfErrors() != 0) {
 308                     return COMPILATION_ERROR;
 309                 }
 310 
 311                 try {
 312                     apply(script, global);
 313                 } catch (final NashornException e) {
 314                     errors.error(e.toString());
 315                     if (context.getEnv()._dump_on_error) {
 316                         e.printStackTrace(context.getErr());
 317                     }
 318 
 319                     return RUNTIME_ERROR;
 320                 }
 321             }
 322         } finally {
 323             context.getOut().flush();
 324             context.getErr().flush();
 325             if (globalChanged) {
 326                 Context.setGlobal(oldGlobal);
 327             }
 328         }
 329 
 330         return SUCCESS;
 331     }
 332 
 333     /**
 334      * Runs launches "fx:bootstrap.js" with the given JavaScript files provided
 335      * as arguments.
 336      *
 337      * @param context the nashorn context
 338      * @param global the global scope
 339      * @param files the list of script files to provide
 340      *
 341      * @return error code
 342      * @throws IOException when any script file read results in I/O error
 343      */
 344     private static int runFXScripts(final Context context, final Global global, final List<String> files) throws IOException {
 345         final Global oldGlobal = Context.getGlobal();
 346         final boolean globalChanged = (oldGlobal != global);
 347         try {
 348             if (globalChanged) {
 349                 Context.setGlobal(global);
 350             }
 351 
 352             global.addOwnProperty("$GLOBAL", Property.NOT_ENUMERABLE, global);
 353             global.addOwnProperty("$SCRIPTS", Property.NOT_ENUMERABLE, files);
 354             context.load(global, "fx:bootstrap.js");
 355         } catch (final NashornException e) {
 356             context.getErrorManager().error(e.toString());
 357             if (context.getEnv()._dump_on_error) {
 358                 e.printStackTrace(context.getErr());
 359             }
 360 
 361             return RUNTIME_ERROR;
 362         } finally {
 363             context.getOut().flush();
 364             context.getErr().flush();
 365             if (globalChanged) {
 366                 Context.setGlobal(oldGlobal);
 367             }
 368         }
 369 
 370         return SUCCESS;
 371     }
 372 
 373     /**
 374      * Hook to ScriptFunction "apply". A performance metering shell may
 375      * introduce enter/exit timing here.
 376      *
 377      * @param target target function for apply
 378      * @param self self reference for apply
 379      *
 380      * @return result of the function apply
 381      */
 382     protected Object apply(final ScriptFunction target, final Object self) {
 383         return ScriptRuntime.apply(target, self);
 384     }
 385 
 386     /**
 387      * read-eval-print loop for Nashorn shell.
 388      *
 389      * @param context the nashorn context
 390      * @param global  global scope object to use
 391      * @return return code
 392      */
 393     @SuppressWarnings("resource")
 394     private static int readEvalPrint(final Context context, final Global global) {
 395         final String prompt = bundle.getString("shell.prompt");
 396         final BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
 397         final PrintWriter err = context.getErr();
 398         final Global oldGlobal = Context.getGlobal();
 399         final boolean globalChanged = (oldGlobal != global);
 400         final ScriptEnvironment env = context.getEnv();
 401 
 402         try {
 403             if (globalChanged) {
 404                 Context.setGlobal(global);
 405             }
 406 
 407             // initialize with "shell.js" script
 408             try {
 409                 final Source source = sourceFor("<shell.js>", Shell.class.getResource("resources/shell.js"));
 410                 context.eval(global, source.getString(), global, "<shell.js>", false);
 411             } catch (final Exception e) {
 412                 err.println(e);
 413                 if (env._dump_on_error) {
 414                     e.printStackTrace(err);
 415                 }
 416 
 417                 return INTERNAL_ERROR;
 418             }
 419 
 420             while (true) {
 421                 err.print(prompt);
 422                 err.flush();
 423 
 424                 String source = "";
 425                 try {
 426                     source = in.readLine();
 427                 } catch (final IOException ioe) {
 428                     err.println(ioe.toString());
 429                 }
 430 
 431                 if (source == null) {
 432                     break;
 433                 }
 434 
 435                 if (source.isEmpty()) {
 436                     continue;
 437                 }
 438 
 439                 Object res;
 440                 try {
 441                     res = context.eval(global, source, global, "<shell>", env._strict);
 442                 } catch (final Exception e) {
 443                     err.println(e);
 444                     if (env._dump_on_error) {
 445                         e.printStackTrace(err);
 446                     }
 447                     continue;
 448                 }
 449 
 450                 if (res != ScriptRuntime.UNDEFINED) {
 451                     err.println(JSType.toString(res));
 452                 }
 453             }
 454         } finally {
 455             if (globalChanged) {
 456                 Context.setGlobal(oldGlobal);
 457             }
 458         }
 459 
 460         return SUCCESS;
 461     }
 462 }