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