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