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 
26 package com.sun.tools.script.shell;
27 
28 import java.io.*;
29 import java.net.*;
30 import java.text.*;
31 import java.util.*;
32 import javax.script.*;
33 
34 /**
35  * This is the main class for Java script shell.
36  */
37 public class Main {
38     /**
39      * main entry point to the command line tool
40      * @param args command line argument array
41      */
main(String[] args)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 {
run(String[] arguments)57         public void run(String[] arguments);
58     }
59 
60     /**
61      * Parses and processes command line options.
62      * @param args command line argument array
63      */
processOptions(String[] args)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      */
addInteractiveMode(final ScriptEngine se)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      */
addFileSource(final ScriptEngine se, final String fileName, final String encoding)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      */
addStringSource(final ScriptEngine se, final String source)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      */
listScriptEngines()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      */
processSource(ScriptEngine se, String filename, String encoding)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      */
evaluateString(ScriptEngine se, String script, boolean exitOnError)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      */
evaluateString(ScriptEngine se, String script)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      */
evaluateReader(ScriptEngine se, Reader reader, String name)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      */
evaluateStream(ScriptEngine se, InputStream is, String name, String encoding)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      */
usage(int exitCode)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      */
getPrompt(ScriptEngine se)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      */
getMessage(String key, Object[] params)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
getIn()398     private static InputStream getIn() {
399         return System.in;
400     }
401 
402     // stream to print error messages
getError()403     private static PrintStream getError() {
404         return System.err;
405     }
406 
407     // get current script engine
getScriptEngine(String lang)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
initScriptEngine(ScriptEngine se)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      */
checkClassPath(String[] args)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             ClassLoader parent = Main.class.getClassLoader();
478             URL[] urls = pathToURLs(classPath);
479             URLClassLoader loader = new URLClassLoader(urls, parent);
480             Thread.currentThread().setContextClassLoader(loader);
481         }
482 
483         // now initialize script engine manager. Note that this has to
484         // be done after setting the context loader so that manager
485         // will see script engines from user specified classpath
486         engineManager = new ScriptEngineManager();
487     }
488 
489     /**
490      * Utility method for converting a search path string to an array
491      * of directory and JAR file URLs.
492      *
493      * @param path the search path string
494      * @return the resulting array of directory and JAR file URLs
495      */
pathToURLs(String path)496     private static URL[] pathToURLs(String path) {
497         String[] components = path.split(File.pathSeparator);
498         URL[] urls = new URL[components.length];
499         int count = 0;
500         while(count < components.length) {
501             URL url = fileToURL(new File(components[count]));
502             if (url != null) {
503                 urls[count++] = url;
504             }
505         }
506         if (urls.length != count) {
507             URL[] tmp = new URL[count];
508             System.arraycopy(urls, 0, tmp, 0, count);
509             urls = tmp;
510         }
511         return urls;
512     }
513 
514     /**
515      * Returns the directory or JAR file URL corresponding to the specified
516      * local file name.
517      *
518      * @param file the File object
519      * @return the resulting directory or JAR file URL, or null if unknown
520      */
fileToURL(File file)521     private static URL fileToURL(File file) {
522         String name;
523         try {
524             name = file.getCanonicalPath();
525         } catch (IOException e) {
526             name = file.getAbsolutePath();
527         }
528         name = name.replace(File.separatorChar, '/');
529         if (!name.startsWith("/")) {
530             name = "/" + name;
531         }
532         // If the file does not exist, then assume that it's a directory
533         if (!file.isFile()) {
534             name = name + "/";
535         }
536         try {
537             return new URL("file", "", name);
538         } catch (MalformedURLException e) {
539             throw new IllegalArgumentException("file");
540         }
541     }
542 
setScriptArguments(ScriptEngine se, String[] args)543     private static void setScriptArguments(ScriptEngine se, String[] args) {
544         se.put("arguments", args);
545         se.put(ScriptEngine.ARGV, args);
546     }
547 
setScriptFilename(ScriptEngine se, String name)548     private static String setScriptFilename(ScriptEngine se, String name) {
549         String oldName = (String) se.get(ScriptEngine.FILENAME);
550         se.put(ScriptEngine.FILENAME, name);
551         return oldName;
552     }
553 
554     // exit codes
555     private static final int EXIT_SUCCESS            = 0;
556     private static final int EXIT_CMD_NO_CLASSPATH   = 1;
557     private static final int EXIT_CMD_NO_FILE        = 2;
558     private static final int EXIT_CMD_NO_SCRIPT      = 3;
559     private static final int EXIT_CMD_NO_LANG        = 4;
560     private static final int EXIT_CMD_NO_ENCODING    = 5;
561     private static final int EXIT_CMD_NO_PROPNAME    = 6;
562     private static final int EXIT_UNKNOWN_OPTION     = 7;
563     private static final int EXIT_ENGINE_NOT_FOUND   = 8;
564     private static final int EXIT_NO_ENCODING_FOUND  = 9;
565     private static final int EXIT_SCRIPT_ERROR       = 10;
566     private static final int EXIT_FILE_NOT_FOUND     = 11;
567     private static final int EXIT_MULTIPLE_STDIN     = 12;
568 
569     // default scripting language
570     private static final String DEFAULT_LANGUAGE = "js";
571     // list of scripts to process
572     private static List<Command> scripts;
573     // the script engine manager
574     private static ScriptEngineManager engineManager;
575     // map of engines we loaded
576     private static Map<String, ScriptEngine> engines;
577     // error messages resource
578     private static ResourceBundle msgRes;
579     private static String BUNDLE_NAME = "com.sun.tools.script.shell.messages";
580     private static String PROGRAM_NAME = "jrunscript";
581 
582     static {
583         scripts = new ArrayList<Command>();
584         engines = new HashMap<String, ScriptEngine>();
585         msgRes = ResourceBundle.getBundle(BUNDLE_NAME, Locale.getDefault());
586     }
587 }
588