Main.java revision 10967:e336cbd8b15e
1/*
2 * Copyright (c) 2005, 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 com.sun.tools.script.shell;
27
28import java.io.*;
29import java.net.*;
30import java.text.*;
31import java.util.*;
32import javax.script.*;
33
34/**
35 * This is the main class for Java script shell.
36 */
37public class Main {
38    /**
39     * main entry point to the command line tool
40     * @param args command line argument array
41     */
42    public static void main(String[] args) {
43        // parse command line options
44        String[] scriptArgs = processOptions(args);
45
46        // process each script command
47        for (Command cmd : scripts) {
48            cmd.run(scriptArgs);
49        }
50
51        System.exit(EXIT_SUCCESS);
52    }
53
54    // Each -e or -f or interactive mode is represented
55    // by an instance of Command.
56    private static interface Command {
57        public void run(String[] arguments);
58    }
59
60    /**
61     * Parses and processes command line options.
62     * @param args command line argument array
63     */
64    private static String[] processOptions(String[] args) {
65        // current scripting language selected
66        String currentLanguage = DEFAULT_LANGUAGE;
67        // current script file encoding selected
68        String currentEncoding = null;
69
70        // check for -classpath or -cp first
71        checkClassPath(args);
72
73        // have we seen -e or -f ?
74        boolean seenScript = false;
75        // have we seen -f - already?
76        boolean seenStdin = false;
77        for (int i=0; i < args.length; i++) {
78            String arg = args[i];
79            if (arg.equals("-classpath") ||
80                    arg.equals("-cp")) {
81                // handled already, just continue
82                i++;
83                continue;
84            }
85
86            // collect non-option arguments and pass these as script arguments
87            if (!arg.startsWith("-")) {
88                int numScriptArgs;
89                int startScriptArg;
90                if (seenScript) {
91                    // if we have seen -e or -f already all non-option arguments
92                    // are passed as script arguments
93                    numScriptArgs = args.length - i;
94                    startScriptArg = i;
95                } else {
96                    // if we have not seen -e or -f, first non-option argument
97                    // is treated as script file name and rest of the non-option
98                    // arguments are passed to script as script arguments
99                    numScriptArgs = args.length - i - 1;
100                    startScriptArg = i + 1;
101                    ScriptEngine se = getScriptEngine(currentLanguage);
102                    addFileSource(se, args[i], currentEncoding);
103                }
104                // collect script arguments and return to main
105                String[] result = new String[numScriptArgs];
106                System.arraycopy(args, startScriptArg, result, 0, numScriptArgs);
107                return result;
108            }
109
110            if (arg.startsWith("-D")) {
111                String value = arg.substring(2);
112                int eq = value.indexOf('=');
113                if (eq != -1) {
114                    System.setProperty(value.substring(0, eq),
115                            value.substring(eq + 1));
116                } else {
117                    if (!value.equals("")) {
118                        System.setProperty(value, "");
119                    } else {
120                        // do not allow empty property name
121                        usage(EXIT_CMD_NO_PROPNAME);
122                    }
123                }
124                continue;
125            } else if (arg.equals("-?") || arg.equals("-help")) {
126                usage(EXIT_SUCCESS);
127            } else if (arg.equals("-e")) {
128                seenScript = true;
129                if (++i == args.length)
130                    usage(EXIT_CMD_NO_SCRIPT);
131
132                ScriptEngine se = getScriptEngine(currentLanguage);
133                addStringSource(se, args[i]);
134                continue;
135            } else if (arg.equals("-encoding")) {
136                if (++i == args.length)
137                    usage(EXIT_CMD_NO_ENCODING);
138                currentEncoding = args[i];
139                continue;
140            } else if (arg.equals("-f")) {
141                seenScript = true;
142                if (++i == args.length)
143                    usage(EXIT_CMD_NO_FILE);
144                ScriptEngine se = getScriptEngine(currentLanguage);
145                if (args[i].equals("-")) {
146                    if (seenStdin) {
147                        usage(EXIT_MULTIPLE_STDIN);
148                    } else {
149                        seenStdin = true;
150                    }
151                    addInteractiveMode(se);
152                } else {
153                    addFileSource(se, args[i], currentEncoding);
154                }
155                continue;
156            } else if (arg.equals("-l")) {
157                if (++i == args.length)
158                    usage(EXIT_CMD_NO_LANG);
159                currentLanguage = args[i];
160                continue;
161            } else if (arg.equals("-q")) {
162                listScriptEngines();
163            }
164            // some unknown option...
165            usage(EXIT_UNKNOWN_OPTION);
166        }
167
168        if (! seenScript) {
169            ScriptEngine se = getScriptEngine(currentLanguage);
170            addInteractiveMode(se);
171        }
172        return new String[0];
173    }
174
175    /**
176     * Adds interactive mode Command
177     * @param se ScriptEngine to use in interactive mode.
178     */
179    private static void addInteractiveMode(final ScriptEngine se) {
180        scripts.add(new Command() {
181            public void run(String[] args) {
182                setScriptArguments(se, args);
183                processSource(se, "-", null);
184            }
185        });
186    }
187
188    /**
189     * Adds script source file Command
190     * @param se ScriptEngine used to evaluate the script file
191     * @param fileName script file name
192     * @param encoding script file encoding
193     */
194    private static void addFileSource(final ScriptEngine se,
195            final String fileName,
196            final String encoding) {
197        scripts.add(new Command() {
198            public void run(String[] args) {
199                setScriptArguments(se, args);
200                processSource(se, fileName, encoding);
201            }
202        });
203    }
204
205    /**
206     * Adds script string source Command
207     * @param se ScriptEngine to be used to evaluate the script string
208     * @param source Script source string
209     */
210    private static void addStringSource(final ScriptEngine se,
211            final String source) {
212        scripts.add(new Command() {
213            public void run(String[] args) {
214                setScriptArguments(se, args);
215                String oldFile = setScriptFilename(se, "<string>");
216                try {
217                    evaluateString(se, source);
218                } finally {
219                    setScriptFilename(se, oldFile);
220                }
221            }
222        });
223    }
224
225    /**
226     * Prints list of script engines available and exits.
227     */
228    private static void listScriptEngines() {
229        List<ScriptEngineFactory> factories = engineManager.getEngineFactories();
230        for (ScriptEngineFactory factory: factories) {
231            getError().println(getMessage("engine.info",
232                    new Object[] { factory.getLanguageName(),
233                            factory.getLanguageVersion(),
234                            factory.getEngineName(),
235                            factory.getEngineVersion()
236            }));
237        }
238        System.exit(EXIT_SUCCESS);
239    }
240
241    /**
242     * Processes a given source file or standard input.
243     * @param se ScriptEngine to be used to evaluate
244     * @param filename file name, can be null
245     * @param encoding script file encoding, can be null
246     */
247    private static void processSource(ScriptEngine se, String filename,
248            String encoding) {
249        if (filename.equals("-")) {
250            BufferedReader in = new BufferedReader
251                    (new InputStreamReader(getIn()));
252            boolean hitEOF = false;
253            String prompt = getPrompt(se);
254            se.put(ScriptEngine.FILENAME, "<STDIN>");
255            while (!hitEOF) {
256                getError().print(prompt);
257                String source = "";
258                try {
259                    source = in.readLine();
260                } catch (IOException ioe) {
261                    getError().println(ioe.toString());
262                }
263                if (source == null) {
264                    hitEOF = true;
265                    break;
266                }
267                Object res = evaluateString(se, source, false);
268                if (res != null) {
269                    res = res.toString();
270                    if (res == null) {
271                        res = "null";
272                    }
273                    getError().println(res);
274                }
275            }
276        } else {
277            FileInputStream fis = null;
278            try {
279                fis = new FileInputStream(filename);
280            } catch (FileNotFoundException fnfe) {
281                getError().println(getMessage("file.not.found",
282                        new Object[] { filename }));
283                        System.exit(EXIT_FILE_NOT_FOUND);
284            }
285            evaluateStream(se, fis, filename, encoding);
286        }
287    }
288
289    /**
290     * Evaluates given script source
291     * @param se ScriptEngine to evaluate the string
292     * @param script Script source string
293     * @param exitOnError whether to exit the process on script error
294     */
295    private static Object evaluateString(ScriptEngine se,
296            String script, boolean exitOnError) {
297        try {
298            return se.eval(script);
299        } catch (ScriptException sexp) {
300            getError().println(getMessage("string.script.error",
301                    new Object[] { sexp.getMessage() }));
302                    if (exitOnError)
303                        System.exit(EXIT_SCRIPT_ERROR);
304        } catch (Exception exp) {
305            exp.printStackTrace(getError());
306            if (exitOnError)
307                System.exit(EXIT_SCRIPT_ERROR);
308        }
309
310        return null;
311    }
312
313    /**
314     * Evaluate script string source and exit on script error
315     * @param se ScriptEngine to evaluate the string
316     * @param script Script source string
317     */
318    private static void evaluateString(ScriptEngine se, String script) {
319        evaluateString(se, script, true);
320    }
321
322    /**
323     * Evaluates script from given reader
324     * @param se ScriptEngine to evaluate the string
325     * @param reader Reader from which is script is read
326     * @param name file name to report in error.
327     */
328    private static Object evaluateReader(ScriptEngine se,
329            Reader reader, String name) {
330        String oldFilename = setScriptFilename(se, name);
331        try {
332            return se.eval(reader);
333        } catch (ScriptException sexp) {
334            getError().println(getMessage("file.script.error",
335                    new Object[] { name, sexp.getMessage() }));
336                    System.exit(EXIT_SCRIPT_ERROR);
337        } catch (Exception exp) {
338            exp.printStackTrace(getError());
339            System.exit(EXIT_SCRIPT_ERROR);
340        } finally {
341            setScriptFilename(se, oldFilename);
342        }
343        return null;
344    }
345
346    /**
347     * Evaluates given input stream
348     * @param se ScriptEngine to evaluate the string
349     * @param is InputStream from which script is read
350     * @param name file name to report in error
351     */
352    private static Object evaluateStream(ScriptEngine se,
353            InputStream is, String name,
354            String encoding) {
355        BufferedReader reader = null;
356        if (encoding != null) {
357            try {
358                reader = new BufferedReader(new InputStreamReader(is,
359                        encoding));
360            } catch (UnsupportedEncodingException uee) {
361                getError().println(getMessage("encoding.unsupported",
362                        new Object[] { encoding }));
363                        System.exit(EXIT_NO_ENCODING_FOUND);
364            }
365        } else {
366            reader = new BufferedReader(new InputStreamReader(is));
367        }
368        return evaluateReader(se, reader, name);
369    }
370
371    /**
372     * Prints usage message and exits
373     * @param exitCode process exit code
374     */
375    private static void usage(int exitCode) {
376        getError().println(getMessage("main.usage",
377                new Object[] { PROGRAM_NAME }));
378                System.exit(exitCode);
379    }
380
381    /**
382     * Gets prompt for interactive mode
383     * @return prompt string to use
384     */
385    private static String getPrompt(ScriptEngine se) {
386        List<String> names = se.getFactory().getNames();
387        return names.get(0) + "> ";
388    }
389
390    /**
391     * Get formatted, localized error message
392     */
393    private static String getMessage(String key, Object[] params) {
394        return MessageFormat.format(msgRes.getString(key), params);
395    }
396
397    // input stream from where we will read
398    private static InputStream getIn() {
399        return System.in;
400    }
401
402    // stream to print error messages
403    private static PrintStream getError() {
404        return System.err;
405    }
406
407    // get current script engine
408    private static ScriptEngine getScriptEngine(String lang) {
409        ScriptEngine se = engines.get(lang);
410        if (se == null) {
411            se = engineManager.getEngineByName(lang);
412            if (se == null) {
413                getError().println(getMessage("engine.not.found",
414                        new Object[] { lang }));
415                        System.exit(EXIT_ENGINE_NOT_FOUND);
416            }
417
418            // initialize the engine
419            initScriptEngine(se);
420            // to avoid re-initialization of engine, store it in a map
421            engines.put(lang, se);
422        }
423        return se;
424    }
425
426    // initialize a given script engine
427    private static void initScriptEngine(ScriptEngine se) {
428        // put engine global variable
429        se.put("engine", se);
430
431        // load init.<ext> file from resource
432        List<String> exts = se.getFactory().getExtensions();
433        InputStream sysIn = null;
434        ClassLoader cl = Thread.currentThread().getContextClassLoader();
435        for (String ext : exts) {
436            sysIn = cl.getResourceAsStream("com/sun/tools/script/shell/init." +
437                    ext);
438            if (sysIn != null) break;
439        }
440        if (sysIn != null) {
441            evaluateStream(se, sysIn, "<system-init>", null);
442        }
443    }
444
445    /**
446     * Checks for -classpath, -cp in command line args. Creates a ClassLoader
447     * and sets it as Thread context loader for current thread.
448     *
449     * @param args command line argument array
450     */
451    private static void checkClassPath(String[] args) {
452        String classPath = null;
453        for (int i = 0; i < args.length; i++) {
454            if (args[i].equals("-classpath") ||
455                    args[i].equals("-cp")) {
456                if (++i == args.length) {
457                    // just -classpath or -cp with no value
458                    usage(EXIT_CMD_NO_CLASSPATH);
459                } else {
460                    classPath = args[i];
461                }
462            }
463        }
464
465        if (classPath != null) {
466            /* We create a class loader, configure it with specified
467             * classpath values and set the same as context loader.
468             * Note that ScriptEngineManager uses context loader to
469             * load script engines. So, this ensures that user defined
470             * script engines will be loaded. For classes referred
471             * from scripts, Rhino engine uses thread context loader
472             * but this is script engine dependent. We don't have
473             * script engine independent solution anyway. Unless we
474             * know the class loader used by a specific engine, we
475             * can't configure correct loader.
476             */
477            URL[] urls = pathToURLs(classPath);
478            URLClassLoader loader = new URLClassLoader(urls);
479            Thread.currentThread().setContextClassLoader(loader);
480        }
481
482        // now initialize script engine manager. Note that this has to
483        // be done after setting the context loader so that manager
484        // will see script engines from user specified classpath
485        engineManager = new ScriptEngineManager();
486    }
487
488    /**
489     * Utility method for converting a search path string to an array
490     * of directory and JAR file URLs.
491     *
492     * @param path the search path string
493     * @return the resulting array of directory and JAR file URLs
494     */
495    private static URL[] pathToURLs(String path) {
496        String[] components = path.split(File.pathSeparator);
497        URL[] urls = new URL[components.length];
498        int count = 0;
499        while(count < components.length) {
500            URL url = fileToURL(new File(components[count]));
501            if (url != null) {
502                urls[count++] = url;
503            }
504        }
505        if (urls.length != count) {
506            URL[] tmp = new URL[count];
507            System.arraycopy(urls, 0, tmp, 0, count);
508            urls = tmp;
509        }
510        return urls;
511    }
512
513    /**
514     * Returns the directory or JAR file URL corresponding to the specified
515     * local file name.
516     *
517     * @param file the File object
518     * @return the resulting directory or JAR file URL, or null if unknown
519     */
520    private static URL fileToURL(File file) {
521        String name;
522        try {
523            name = file.getCanonicalPath();
524        } catch (IOException e) {
525            name = file.getAbsolutePath();
526        }
527        name = name.replace(File.separatorChar, '/');
528        if (!name.startsWith("/")) {
529            name = "/" + name;
530        }
531        // If the file does not exist, then assume that it's a directory
532        if (!file.isFile()) {
533            name = name + "/";
534        }
535        try {
536            return new URL("file", "", name);
537        } catch (MalformedURLException e) {
538            throw new IllegalArgumentException("file");
539        }
540    }
541
542    private static void setScriptArguments(ScriptEngine se, String[] args) {
543        se.put("arguments", args);
544        se.put(ScriptEngine.ARGV, args);
545    }
546
547    private static String setScriptFilename(ScriptEngine se, String name) {
548        String oldName = (String) se.get(ScriptEngine.FILENAME);
549        se.put(ScriptEngine.FILENAME, name);
550        return oldName;
551    }
552
553    // exit codes
554    private static final int EXIT_SUCCESS            = 0;
555    private static final int EXIT_CMD_NO_CLASSPATH   = 1;
556    private static final int EXIT_CMD_NO_FILE        = 2;
557    private static final int EXIT_CMD_NO_SCRIPT      = 3;
558    private static final int EXIT_CMD_NO_LANG        = 4;
559    private static final int EXIT_CMD_NO_ENCODING    = 5;
560    private static final int EXIT_CMD_NO_PROPNAME    = 6;
561    private static final int EXIT_UNKNOWN_OPTION     = 7;
562    private static final int EXIT_ENGINE_NOT_FOUND   = 8;
563    private static final int EXIT_NO_ENCODING_FOUND  = 9;
564    private static final int EXIT_SCRIPT_ERROR       = 10;
565    private static final int EXIT_FILE_NOT_FOUND     = 11;
566    private static final int EXIT_MULTIPLE_STDIN     = 12;
567
568    // default scripting language
569    private static final String DEFAULT_LANGUAGE = "js";
570    // list of scripts to process
571    private static List<Command> scripts;
572    // the script engine manager
573    private static ScriptEngineManager engineManager;
574    // map of engines we loaded
575    private static Map<String, ScriptEngine> engines;
576    // error messages resource
577    private static ResourceBundle msgRes;
578    private static String BUNDLE_NAME = "com.sun.tools.script.shell.messages";
579    private static String PROGRAM_NAME = "jrunscript";
580
581    static {
582        scripts = new ArrayList<Command>();
583        engines = new HashMap<String, ScriptEngine>();
584        msgRes = ResourceBundle.getBundle(BUNDLE_NAME, Locale.getDefault());
585    }
586}
587