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
26package jdk.nashorn.tools;
27
28import jdk.nashorn.api.scripting.NashornException;
29import jdk.nashorn.internal.codegen.Compiler;
30import jdk.nashorn.internal.codegen.Compiler.CompilationPhases;
31import jdk.nashorn.internal.ir.Expression;
32import jdk.nashorn.internal.ir.FunctionNode;
33import jdk.nashorn.internal.ir.debug.ASTWriter;
34import jdk.nashorn.internal.ir.debug.PrintVisitor;
35import jdk.nashorn.internal.objects.Global;
36import jdk.nashorn.internal.objects.NativeSymbol;
37import jdk.nashorn.internal.parser.Parser;
38import jdk.nashorn.internal.runtime.Context;
39import jdk.nashorn.internal.runtime.ErrorManager;
40import jdk.nashorn.internal.runtime.JSType;
41import jdk.nashorn.internal.runtime.Property;
42import jdk.nashorn.internal.runtime.ScriptEnvironment;
43import jdk.nashorn.internal.runtime.ScriptFunction;
44import jdk.nashorn.internal.runtime.ScriptObject;
45import jdk.nashorn.internal.runtime.ScriptRuntime;
46import jdk.nashorn.internal.runtime.ScriptingFunctions;
47import jdk.nashorn.internal.runtime.Symbol;
48import jdk.nashorn.internal.runtime.arrays.ArrayLikeIterator;
49import jdk.nashorn.internal.runtime.options.Options;
50
51import java.io.BufferedReader;
52import java.io.File;
53import java.io.FileReader;
54import java.io.IOException;
55import java.io.InputStream;
56import java.io.InputStreamReader;
57import java.io.OutputStream;
58import java.io.PrintStream;
59import java.io.PrintWriter;
60import java.io.StreamTokenizer;
61import java.io.StringReader;
62import java.nio.file.Files;
63import java.nio.file.Path;
64import java.nio.file.Paths;
65import java.util.ArrayList;
66import java.util.Arrays;
67import java.util.Iterator;
68import java.util.List;
69import java.util.Locale;
70import java.util.ResourceBundle;
71
72import static jdk.nashorn.internal.runtime.Source.sourceFor;
73
74/**
75 * Command line Shell for processing JavaScript files.
76 */
77public 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.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}
672