blob: a6b19a6ca067fb458894de78c9150f486264e97a [file] [log] [blame]
/*
* Copyright (c) 2005, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.tools.script.shell;
import java.io.*;
import java.net.*;
import java.text.*;
import java.util.*;
import javax.script.*;
/**
* This is the main class for Java script shell.
*/
public class Main {
/**
* main entry point to the command line tool
* @param args command line argument array
*/
public static void main(String[] args) {
// parse command line options
String[] scriptArgs = processOptions(args);
// process each script command
for (Command cmd : scripts) {
cmd.run(scriptArgs);
}
System.exit(EXIT_SUCCESS);
}
// Each -e or -f or interactive mode is represented
// by an instance of Command.
private static interface Command {
public void run(String[] arguments);
}
/**
* Parses and processes command line options.
* @param args command line argument array
*/
private static String[] processOptions(String[] args) {
// current scripting language selected
String currentLanguage = DEFAULT_LANGUAGE;
// current script file encoding selected
String currentEncoding = null;
// check for -classpath or -cp first
checkClassPath(args);
// have we seen -e or -f ?
boolean seenScript = false;
// have we seen -f - already?
boolean seenStdin = false;
for (int i=0; i < args.length; i++) {
String arg = args[i];
if (arg.equals("-classpath") ||
arg.equals("-cp")) {
// handled already, just continue
i++;
continue;
}
// collect non-option arguments and pass these as script arguments
if (!arg.startsWith("-")) {
int numScriptArgs;
int startScriptArg;
if (seenScript) {
// if we have seen -e or -f already all non-option arguments
// are passed as script arguments
numScriptArgs = args.length - i;
startScriptArg = i;
} else {
// if we have not seen -e or -f, first non-option argument
// is treated as script file name and rest of the non-option
// arguments are passed to script as script arguments
numScriptArgs = args.length - i - 1;
startScriptArg = i + 1;
ScriptEngine se = getScriptEngine(currentLanguage);
addFileSource(se, args[i], currentEncoding);
}
// collect script arguments and return to main
String[] result = new String[numScriptArgs];
System.arraycopy(args, startScriptArg, result, 0, numScriptArgs);
return result;
}
if (arg.startsWith("-D")) {
String value = arg.substring(2);
int eq = value.indexOf('=');
if (eq != -1) {
System.setProperty(value.substring(0, eq),
value.substring(eq + 1));
} else {
if (!value.equals("")) {
System.setProperty(value, "");
} else {
// do not allow empty property name
usage(EXIT_CMD_NO_PROPNAME);
}
}
continue;
} else if (arg.equals("-?") || arg.equals("-help")) {
usage(EXIT_SUCCESS);
} else if (arg.equals("-e")) {
seenScript = true;
if (++i == args.length)
usage(EXIT_CMD_NO_SCRIPT);
ScriptEngine se = getScriptEngine(currentLanguage);
addStringSource(se, args[i]);
continue;
} else if (arg.equals("-encoding")) {
if (++i == args.length)
usage(EXIT_CMD_NO_ENCODING);
currentEncoding = args[i];
continue;
} else if (arg.equals("-f")) {
seenScript = true;
if (++i == args.length)
usage(EXIT_CMD_NO_FILE);
ScriptEngine se = getScriptEngine(currentLanguage);
if (args[i].equals("-")) {
if (seenStdin) {
usage(EXIT_MULTIPLE_STDIN);
} else {
seenStdin = true;
}
addInteractiveMode(se);
} else {
addFileSource(se, args[i], currentEncoding);
}
continue;
} else if (arg.equals("-l")) {
if (++i == args.length)
usage(EXIT_CMD_NO_LANG);
currentLanguage = args[i];
continue;
} else if (arg.equals("-q")) {
listScriptEngines();
}
// some unknown option...
usage(EXIT_UNKNOWN_OPTION);
}
if (! seenScript) {
ScriptEngine se = getScriptEngine(currentLanguage);
addInteractiveMode(se);
}
return new String[0];
}
/**
* Adds interactive mode Command
* @param se ScriptEngine to use in interactive mode.
*/
private static void addInteractiveMode(final ScriptEngine se) {
scripts.add(new Command() {
public void run(String[] args) {
setScriptArguments(se, args);
processSource(se, "-", null);
}
});
}
/**
* Adds script source file Command
* @param se ScriptEngine used to evaluate the script file
* @param fileName script file name
* @param encoding script file encoding
*/
private static void addFileSource(final ScriptEngine se,
final String fileName,
final String encoding) {
scripts.add(new Command() {
public void run(String[] args) {
setScriptArguments(se, args);
processSource(se, fileName, encoding);
}
});
}
/**
* Adds script string source Command
* @param se ScriptEngine to be used to evaluate the script string
* @param source Script source string
*/
private static void addStringSource(final ScriptEngine se,
final String source) {
scripts.add(new Command() {
public void run(String[] args) {
setScriptArguments(se, args);
String oldFile = setScriptFilename(se, "<string>");
try {
evaluateString(se, source);
} finally {
setScriptFilename(se, oldFile);
}
}
});
}
/**
* Prints list of script engines available and exits.
*/
private static void listScriptEngines() {
List<ScriptEngineFactory> factories = engineManager.getEngineFactories();
for (ScriptEngineFactory factory: factories) {
getError().println(getMessage("engine.info",
new Object[] { factory.getLanguageName(),
factory.getLanguageVersion(),
factory.getEngineName(),
factory.getEngineVersion()
}));
}
System.exit(EXIT_SUCCESS);
}
/**
* Processes a given source file or standard input.
* @param se ScriptEngine to be used to evaluate
* @param filename file name, can be null
* @param encoding script file encoding, can be null
*/
private static void processSource(ScriptEngine se, String filename,
String encoding) {
if (filename.equals("-")) {
BufferedReader in = new BufferedReader
(new InputStreamReader(getIn()));
boolean hitEOF = false;
String prompt = getPrompt(se);
se.put(ScriptEngine.FILENAME, "<STDIN>");
while (!hitEOF) {
getError().print(prompt);
String source = "";
try {
source = in.readLine();
} catch (IOException ioe) {
getError().println(ioe.toString());
}
if (source == null) {
hitEOF = true;
break;
}
Object res = evaluateString(se, source, false);
if (res != null) {
res = res.toString();
if (res == null) {
res = "null";
}
getError().println(res);
}
}
} else {
FileInputStream fis = null;
try {
fis = new FileInputStream(filename);
} catch (FileNotFoundException fnfe) {
getError().println(getMessage("file.not.found",
new Object[] { filename }));
System.exit(EXIT_FILE_NOT_FOUND);
}
evaluateStream(se, fis, filename, encoding);
}
}
/**
* Evaluates given script source
* @param se ScriptEngine to evaluate the string
* @param script Script source string
* @param exitOnError whether to exit the process on script error
*/
private static Object evaluateString(ScriptEngine se,
String script, boolean exitOnError) {
try {
return se.eval(script);
} catch (ScriptException sexp) {
getError().println(getMessage("string.script.error",
new Object[] { sexp.getMessage() }));
if (exitOnError)
System.exit(EXIT_SCRIPT_ERROR);
} catch (Exception exp) {
exp.printStackTrace(getError());
if (exitOnError)
System.exit(EXIT_SCRIPT_ERROR);
}
return null;
}
/**
* Evaluate script string source and exit on script error
* @param se ScriptEngine to evaluate the string
* @param script Script source string
*/
private static void evaluateString(ScriptEngine se, String script) {
evaluateString(se, script, true);
}
/**
* Evaluates script from given reader
* @param se ScriptEngine to evaluate the string
* @param reader Reader from which is script is read
* @param name file name to report in error.
*/
private static Object evaluateReader(ScriptEngine se,
Reader reader, String name) {
String oldFilename = setScriptFilename(se, name);
try {
return se.eval(reader);
} catch (ScriptException sexp) {
getError().println(getMessage("file.script.error",
new Object[] { name, sexp.getMessage() }));
System.exit(EXIT_SCRIPT_ERROR);
} catch (Exception exp) {
exp.printStackTrace(getError());
System.exit(EXIT_SCRIPT_ERROR);
} finally {
setScriptFilename(se, oldFilename);
}
return null;
}
/**
* Evaluates given input stream
* @param se ScriptEngine to evaluate the string
* @param is InputStream from which script is read
* @param name file name to report in error
*/
private static Object evaluateStream(ScriptEngine se,
InputStream is, String name,
String encoding) {
BufferedReader reader = null;
if (encoding != null) {
try {
reader = new BufferedReader(new InputStreamReader(is,
encoding));
} catch (UnsupportedEncodingException uee) {
getError().println(getMessage("encoding.unsupported",
new Object[] { encoding }));
System.exit(EXIT_NO_ENCODING_FOUND);
}
} else {
reader = new BufferedReader(new InputStreamReader(is));
}
return evaluateReader(se, reader, name);
}
/**
* Prints usage message and exits
* @param exitCode process exit code
*/
private static void usage(int exitCode) {
getError().println(getMessage("main.usage",
new Object[] { PROGRAM_NAME }));
System.exit(exitCode);
}
/**
* Gets prompt for interactive mode
* @return prompt string to use
*/
private static String getPrompt(ScriptEngine se) {
List<String> names = se.getFactory().getNames();
return names.get(0) + "> ";
}
/**
* Get formatted, localized error message
*/
private static String getMessage(String key, Object[] params) {
return MessageFormat.format(msgRes.getString(key), params);
}
// input stream from where we will read
private static InputStream getIn() {
return System.in;
}
// stream to print error messages
private static PrintStream getError() {
return System.err;
}
// get current script engine
private static ScriptEngine getScriptEngine(String lang) {
ScriptEngine se = engines.get(lang);
if (se == null) {
se = engineManager.getEngineByName(lang);
if (se == null) {
getError().println(getMessage("engine.not.found",
new Object[] { lang }));
System.exit(EXIT_ENGINE_NOT_FOUND);
}
// initialize the engine
initScriptEngine(se);
// to avoid re-initialization of engine, store it in a map
engines.put(lang, se);
}
return se;
}
// initialize a given script engine
private static void initScriptEngine(ScriptEngine se) {
// put engine global variable
se.put("engine", se);
// load init.<ext> file from resource
List<String> exts = se.getFactory().getExtensions();
InputStream sysIn = null;
ClassLoader cl = Thread.currentThread().getContextClassLoader();
for (String ext : exts) {
sysIn = cl.getResourceAsStream("com/sun/tools/script/shell/init." +
ext);
if (sysIn != null) break;
}
if (sysIn != null) {
evaluateStream(se, sysIn, "<system-init>", null);
}
}
/**
* Checks for -classpath, -cp in command line args. Creates a ClassLoader
* and sets it as Thread context loader for current thread.
*
* @param args command line argument array
*/
private static void checkClassPath(String[] args) {
String classPath = null;
for (int i = 0; i < args.length; i++) {
if (args[i].equals("-classpath") ||
args[i].equals("-cp")) {
if (++i == args.length) {
// just -classpath or -cp with no value
usage(EXIT_CMD_NO_CLASSPATH);
} else {
classPath = args[i];
}
}
}
if (classPath != null) {
/* We create a class loader, configure it with specified
* classpath values and set the same as context loader.
* Note that ScriptEngineManager uses context loader to
* load script engines. So, this ensures that user defined
* script engines will be loaded. For classes referred
* from scripts, Rhino engine uses thread context loader
* but this is script engine dependent. We don't have
* script engine independent solution anyway. Unless we
* know the class loader used by a specific engine, we
* can't configure correct loader.
*/
ClassLoader parent = Main.class.getClassLoader();
URL[] urls = pathToURLs(classPath);
URLClassLoader loader = new URLClassLoader(urls, parent);
Thread.currentThread().setContextClassLoader(loader);
}
// now initialize script engine manager. Note that this has to
// be done after setting the context loader so that manager
// will see script engines from user specified classpath
engineManager = new ScriptEngineManager();
}
/**
* Utility method for converting a search path string to an array
* of directory and JAR file URLs.
*
* @param path the search path string
* @return the resulting array of directory and JAR file URLs
*/
private static URL[] pathToURLs(String path) {
String[] components = path.split(File.pathSeparator);
URL[] urls = new URL[components.length];
int count = 0;
while(count < components.length) {
URL url = fileToURL(new File(components[count]));
if (url != null) {
urls[count++] = url;
}
}
if (urls.length != count) {
URL[] tmp = new URL[count];
System.arraycopy(urls, 0, tmp, 0, count);
urls = tmp;
}
return urls;
}
/**
* Returns the directory or JAR file URL corresponding to the specified
* local file name.
*
* @param file the File object
* @return the resulting directory or JAR file URL, or null if unknown
*/
private static URL fileToURL(File file) {
String name;
try {
name = file.getCanonicalPath();
} catch (IOException e) {
name = file.getAbsolutePath();
}
name = name.replace(File.separatorChar, '/');
if (!name.startsWith("/")) {
name = "/" + name;
}
// If the file does not exist, then assume that it's a directory
if (!file.isFile()) {
name = name + "/";
}
try {
return new URL("file", "", name);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("file");
}
}
private static void setScriptArguments(ScriptEngine se, String[] args) {
se.put("arguments", args);
se.put(ScriptEngine.ARGV, args);
}
private static String setScriptFilename(ScriptEngine se, String name) {
String oldName = (String) se.get(ScriptEngine.FILENAME);
se.put(ScriptEngine.FILENAME, name);
return oldName;
}
// exit codes
private static final int EXIT_SUCCESS = 0;
private static final int EXIT_CMD_NO_CLASSPATH = 1;
private static final int EXIT_CMD_NO_FILE = 2;
private static final int EXIT_CMD_NO_SCRIPT = 3;
private static final int EXIT_CMD_NO_LANG = 4;
private static final int EXIT_CMD_NO_ENCODING = 5;
private static final int EXIT_CMD_NO_PROPNAME = 6;
private static final int EXIT_UNKNOWN_OPTION = 7;
private static final int EXIT_ENGINE_NOT_FOUND = 8;
private static final int EXIT_NO_ENCODING_FOUND = 9;
private static final int EXIT_SCRIPT_ERROR = 10;
private static final int EXIT_FILE_NOT_FOUND = 11;
private static final int EXIT_MULTIPLE_STDIN = 12;
// default scripting language
private static final String DEFAULT_LANGUAGE = "js";
// list of scripts to process
private static List<Command> scripts;
// the script engine manager
private static ScriptEngineManager engineManager;
// map of engines we loaded
private static Map<String, ScriptEngine> engines;
// error messages resource
private static ResourceBundle msgRes;
private static String BUNDLE_NAME = "com.sun.tools.script.shell.messages";
private static String PROGRAM_NAME = "jrunscript";
static {
scripts = new ArrayList<Command>();
engines = new HashMap<String, ScriptEngine>();
msgRes = ResourceBundle.getBundle(BUNDLE_NAME, Locale.getDefault());
}
}