       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 }
    --- EOF ---