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      */
  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("-?") ||
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 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
 402     private static InputStream getIn() {
 403         return System.in;
 404     }
 405 
 406     // stream to print error messages
 407     private static PrintStream getError() {
 408         return System.err;
 409     }
 410 
 411     // get current script engine
 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
 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      */
 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      */
 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      */
 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 
 549     private static void setScriptArguments(ScriptEngine se, String[] args) {
 550         se.put("arguments", args);
 551         se.put(ScriptEngine.ARGV, args);
 552     }
 553 
 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 }