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      */
  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             try {
 437                 sysIn = Main.class.getModule().getResourceAsStream("com/sun/tools/script/shell/init." + ext);
 438             } catch (IOException ioe) {
 439                 throw new RuntimeException(ioe);
 440             }
 441             if (sysIn != null) break;
 442         }
 443         if (sysIn != null) {
 444             evaluateStream(se, sysIn, "<system-init>", null);
 445         }
 446     }
 447 
 448     /**
 449      * Checks for -classpath, -cp in command line args. Creates a ClassLoader
 450      * and sets it as Thread context loader for current thread.
 451      *
 452      * @param args command line argument array
 453      */
 454     private static void checkClassPath(String[] args) {
 455         String classPath = null;
 456         for (int i = 0; i < args.length; i++) {
 457             if (args[i].equals("-classpath") ||
 458                     args[i].equals("-cp")) {
 459                 if (++i == args.length) {
 460                     // just -classpath or -cp with no value
 461                     usage(EXIT_CMD_NO_CLASSPATH);
 462                 } else {
 463                     classPath = args[i];
 464                 }
 465             }
 466         }
 467 
 468         if (classPath != null) {
 469             /* We create a class loader, configure it with specified
 470              * classpath values and set the same as context loader.
 471              * Note that ScriptEngineManager uses context loader to
 472              * load script engines. So, this ensures that user defined
 473              * script engines will be loaded. For classes referred
 474              * from scripts, Rhino engine uses thread context loader
 475              * but this is script engine dependent. We don't have
 476              * script engine independent solution anyway. Unless we
 477              * know the class loader used by a specific engine, we
 478              * can't configure correct loader.
 479              */
 480             URL[] urls = pathToURLs(classPath);
 481             URLClassLoader loader = new URLClassLoader(urls);
 482             Thread.currentThread().setContextClassLoader(loader);
 483         }
 484 
 485         // now initialize script engine manager. Note that this has to
 486         // be done after setting the context loader so that manager
 487         // will see script engines from user specified classpath
 488         engineManager = new ScriptEngineManager();
 489     }
 490 
 491     /**
 492      * Utility method for converting a search path string to an array
 493      * of directory and JAR file URLs.
 494      *
 495      * @param path the search path string
 496      * @return the resulting array of directory and JAR file URLs
 497      */
 498     private static URL[] pathToURLs(String path) {
 499         String[] components = path.split(File.pathSeparator);
 500         URL[] urls = new URL[components.length];
 501         int count = 0;
 502         while(count < components.length) {
 503             URL url = fileToURL(new File(components[count]));
 504             if (url != null) {
 505                 urls[count++] = url;
 506             }
 507         }
 508         if (urls.length != count) {
 509             URL[] tmp = new URL[count];
 510             System.arraycopy(urls, 0, tmp, 0, count);
 511             urls = tmp;
 512         }
 513         return urls;
 514     }
 515 
 516     /**
 517      * Returns the directory or JAR file URL corresponding to the specified
 518      * local file name.
 519      *
 520      * @param file the File object
 521      * @return the resulting directory or JAR file URL, or null if unknown
 522      */
 523     private static URL fileToURL(File file) {
 524         String name;
 525         try {
 526             name = file.getCanonicalPath();
 527         } catch (IOException e) {
 528             name = file.getAbsolutePath();
 529         }
 530         name = name.replace(File.separatorChar, '/');
 531         if (!name.startsWith("/")) {
 532             name = "/" + name;
 533         }
 534         // If the file does not exist, then assume that it's a directory
 535         if (!file.isFile()) {
 536             name = name + "/";
 537         }
 538         try {
 539             return new URL("file", "", name);
 540         } catch (MalformedURLException e) {
 541             throw new IllegalArgumentException("file");
 542         }
 543     }
 544 
 545     private static void setScriptArguments(ScriptEngine se, String[] args) {
 546         se.put("arguments", args);
 547         se.put(ScriptEngine.ARGV, args);
 548     }
 549 
 550     private static String setScriptFilename(ScriptEngine se, String name) {
 551         String oldName = (String) se.get(ScriptEngine.FILENAME);
 552         se.put(ScriptEngine.FILENAME, name);
 553         return oldName;
 554     }
 555 
 556     // exit codes
 557     private static final int EXIT_SUCCESS            = 0;
 558     private static final int EXIT_CMD_NO_CLASSPATH   = 1;
 559     private static final int EXIT_CMD_NO_FILE        = 2;
 560     private static final int EXIT_CMD_NO_SCRIPT      = 3;
 561     private static final int EXIT_CMD_NO_LANG        = 4;
 562     private static final int EXIT_CMD_NO_ENCODING    = 5;
 563     private static final int EXIT_CMD_NO_PROPNAME    = 6;
 564     private static final int EXIT_UNKNOWN_OPTION     = 7;
 565     private static final int EXIT_ENGINE_NOT_FOUND   = 8;
 566     private static final int EXIT_NO_ENCODING_FOUND  = 9;
 567     private static final int EXIT_SCRIPT_ERROR       = 10;
 568     private static final int EXIT_FILE_NOT_FOUND     = 11;
 569     private static final int EXIT_MULTIPLE_STDIN     = 12;
 570 
 571     // default scripting language
 572     private static final String DEFAULT_LANGUAGE = "js";
 573     // list of scripts to process
 574     private static List<Command> scripts;
 575     // the script engine manager
 576     private static ScriptEngineManager engineManager;
 577     // map of engines we loaded
 578     private static Map<String, ScriptEngine> engines;
 579     // error messages resource
 580     private static ResourceBundle msgRes;
 581     private static String BUNDLE_NAME = "com.sun.tools.script.shell.messages";
 582     private static String PROGRAM_NAME = "jrunscript";
 583 
 584     static {
 585         scripts = new ArrayList<Command>();
 586         engines = new HashMap<String, ScriptEngine>();
 587         msgRes = ResourceBundle.getBundle(BUNDLE_NAME, Locale.getDefault());
 588     }
 589 }