/* * Copyright 2003-2007 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package groovy.ui; import groovy.lang.Binding; import groovy.lang.Closure; import groovy.lang.GroovyShell; import groovy.lang.GroovySystem; import org.codehaus.groovy.tools.shell.util.MessageSource; import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.runtime.InvokerInvocationException; import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.tools.ErrorReporter; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.io.Writer; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.PosixParser; import org.apache.commons.cli.Options; import org.apache.commons.cli.HelpFormatter; import jline.ConsoleReader; import jline.SimpleCompletor; /** * A simple interactive shell for evaluating groovy expressions on the command line (aka. groovysh). * * @author <a href="mailto:james@coredevelopers.net">James Strachan</a> * @author <a href="mailto:cpoirier@dreaming.org" >Chris Poirier</a> * @author Yuri Schimke * @author Brian McCallistair * @author Guillaume Laforge * @author Dierk Koenig, include the inspect command, June 2005 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a> * * @version $Revision$ */ @Deprecated public class InteractiveShell implements Runnable { private static final String NEW_LINE = System.getProperty("line.separator"); private static final MessageSource MESSAGES = new MessageSource(InteractiveShell.class); private final GroovyShell shell; private final InputStream in; // FIXME: This doesn't really need to be a field, but hold on to it for now private final PrintStream out; private final PrintStream err; private final ConsoleReader reader; private Object lastResult; private Closure beforeExecution; private Closure afterExecution; /** * Entry point when called directly. */ public static void main(final String args[]) { try { processCommandLineArguments(args); final InteractiveShell groovy = new InteractiveShell(); groovy.run(); } catch (Exception e) { System.err.println("FATAL: " + e); e.printStackTrace(); System.exit(1); } System.exit(0); } /** * Process cli args when the shell is invoked via main(). * * @noinspection AccessStaticViaInstance */ private static void processCommandLineArguments(final String[] args) throws Exception { assert args != null; // // TODO: Let this take a single, optional argument which is a file or URL to run? // Options options = new Options(); options.addOption(OptionBuilder.withLongOpt("help") .withDescription(MESSAGES.getMessage("cli.option.help.description")) .create('h')); options.addOption(OptionBuilder.withLongOpt("version") .withDescription(MESSAGES.getMessage("cli.option.version.description")) .create('V')); // // TODO: Add more options, maybe even add an option to prime the buffer from a URL or File? // // // FIXME: This does not currently barf on unsupported options short options, though it does for long ones. // Same problem with commons-cli 1.0 and 1.1 // CommandLineParser parser = new PosixParser(); CommandLine line = parser.parse(options, args, true); String[] lineargs = line.getArgs(); // Puke if there were arguments, we don't support any right now if (lineargs.length != 0) { System.err.println(MESSAGES.format("cli.info.unexpected_args", new Object[] { DefaultGroovyMethods.join(lineargs, " ") })); System.exit(1); } PrintWriter writer = new PrintWriter(System.out); if (line.hasOption('h')) { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp( writer, 80, // width "groovysh [options]", "", options, 4, // left pad 4, // desc pad "", false); // auto usage writer.flush(); System.exit(0); } if (line.hasOption('V')) { writer.println(MESSAGES.format("cli.info.version", new Object[] { GroovySystem.getVersion() })); writer.flush(); System.exit(0); } } /** * Default constructor, initializes uses new binding and system streams. */ public InteractiveShell() throws IOException { this(System.in, System.out, System.err); } /** * Constructs a new InteractiveShell instance * * @param in The input stream to use * @param out The output stream to use * @param err The error stream to use */ public InteractiveShell(final InputStream in, final PrintStream out, final PrintStream err) throws IOException { this(null, new Binding(), in, out, err); } /** * Constructs a new InteractiveShell instance * * @param binding The binding instance * @param in The input stream to use * @param out The output stream to use * @param err The error stream to use */ public InteractiveShell(final Binding binding, final InputStream in, final PrintStream out, final PrintStream err) throws IOException { this(null, binding, in, out, err); } /** * Constructs a new InteractiveShell instance * * @param parent The parent ClassLoader * @param binding The binding instance * @param in The input stream to use * @param out The output stream to use * @param err The error stream to use */ public InteractiveShell(final ClassLoader parent, final Binding binding, final InputStream in, final PrintStream out, final PrintStream err) throws IOException { assert binding != null; assert in != null; assert out != null; assert err != null; this.in = in; this.out = out; this.err = err; // Initialize the JLine console input reader Writer writer = new OutputStreamWriter(out); reader = new ConsoleReader(in, writer); reader.setDefaultPrompt("groovy> "); // Add some completors to fancy things up reader.addCompletor(new CommandNameCompletor()); if (parent != null) { shell = new GroovyShell(parent, binding); } else { shell = new GroovyShell(binding); } // Add some default variables to the shell Map map = shell.getContext().getVariables(); // // FIXME: Um, is this right? Only set the "shell" var in the context if its set already? // if (map.get("shell") != null) { map.put("shell", shell); } } //--------------------------------------------------------------------------- // COMMAND LINE PROCESSING LOOP // // TODO: Add a general error display handler, and probably add a "ERROR: " prefix to the result for clarity ? // Maybe add one for WARNING's too? // /** * Reads commands and statements from input stream and processes them. */ public void run() { // Display the startup banner out.println(MESSAGES.format("startup_banner.0", new Object[] { GroovySystem.getVersion(), System.getProperty("java.version") })); out.println(MESSAGES.getMessage("startup_banner.1")); while (true) { // Read a code block to evaluate; this will deal with basic error handling final String code = read(); // If we got a null, then quit if (code == null) { break; } reset(); // Evaluate the code block if it was parsed if (code.length() > 0) { try { if (beforeExecution != null) { beforeExecution.call(); } lastResult = shell.evaluate(code); if (afterExecution != null) { afterExecution.call(); } // Shows the result of the evaluated code out.print("===> "); out.println(lastResult); } catch (CompilationFailedException e) { err.println(e); } catch (Throwable e) { // Unroll invoker exceptions if (e instanceof InvokerInvocationException) { e = e.getCause(); } filterAndPrintStackTrace(e); } } } } /** * A closure that is executed before the execution of a given script * * @param beforeExecution The closure to execute */ public void setBeforeExecution(final Closure beforeExecution) { this.beforeExecution = beforeExecution; } /** * A closure that is executed after the execution of the last script. The result of the * execution is passed as the first argument to the closure (the value of 'it') * * @param afterExecution The closure to execute */ public void setAfterExecution(final Closure afterExecution) { this.afterExecution = afterExecution; } /** * Filter stacktraces to show only relevant lines of the exception thrown. * * @param cause the throwable whose stacktrace needs to be filtered */ private void filterAndPrintStackTrace(final Throwable cause) { assert cause != null; // // TODO: Use message... // err.print("ERROR: "); err.println(cause); cause.printStackTrace(err); // // FIXME: What is the point of this? AFAICT, this just produces crappy/corrupt traces and is completely useless // // StackTraceElement[] stackTrace = e.getStackTrace(); // // for (int i = 0; i < stackTrace.length; i++) { // StackTraceElement element = stackTrace[i]; // String fileName = element.getFileName(); // // if ((fileName==null || (!fileName.endsWith(".java")) && (!element.getClassName().startsWith("gjdk")))) { // err.print("\tat "); // err.println(element); // } // } } //--------------------------------------------------------------------------- // COMMAND LINE PROCESSING MACHINERY /** The statement text accepted to date */ private StringBuffer accepted = new StringBuffer(); /** A line of statement text not yet accepted */ private String pending; // // FIXME: Doesn't look like 'line' is really used/needed anywhere... could drop it, or perhaps // could use it to update the prompt er something to show the buffer size? // /** The current line number */ private int line; /** Set to force clear of accepted */ private boolean stale = false; /** A SourceUnit used to check the statement */ private SourceUnit parser; /** Any actual syntax error caught during parsing */ private Exception error; /** * Resets the command-line processing machinery after use. */ protected void reset() { stale = true; pending = null; line = 1; parser = null; error = null; } // // FIXME: This Javadoc is not correct... read() will return the full code block read until "go" // /** * Reads a single statement from the command line. Also identifies * and processes command shell commands. Returns the command text * on success, or null when command processing is complete. * * NOTE: Changed, for now, to read until 'execute' is issued. At * 'execute', the statement must be complete. */ protected String read() { reset(); boolean complete = false; boolean done = false; while (/* !complete && */ !done) { // Read a line. If IOException or null, or command "exit", terminate processing. try { pending = reader.readLine(); } catch (IOException e) { // // FIXME: Shouldn't really eat this exception, may be something we need to see... ? // } // If result is null then we are shutting down if (pending == null) { return null; } // First up, try to process the line as a command and proceed accordingly // Trim what we have for use in command bits, so things like "help " actually show the help screen String command = pending.trim(); if (COMMAND_MAPPINGS.containsKey(command)) { int code = ((Integer)COMMAND_MAPPINGS.get(command)).intValue(); switch (code) { case COMMAND_ID_EXIT: return null; case COMMAND_ID_HELP: displayHelp(); break; case COMMAND_ID_DISCARD: reset(); done = true; break; case COMMAND_ID_DISPLAY: displayStatement(); break; case COMMAND_ID_EXPLAIN: explainStatement(); break; case COMMAND_ID_BINDING: displayBinding(); break; case COMMAND_ID_EXECUTE: if (complete) { done = true; } else { err.println(MESSAGES.getMessage("command.execute.not_complete")); } break; case COMMAND_ID_DISCARD_LOADED_CLASSES: resetLoadedClasses(); break; case COMMAND_ID_INSPECT: inspect(); break; default: throw new Error("BUG: Unknown command for code: " + code); } // Finished processing command bits, continue reading, don't need to process code continue; } // Otherwise, it's part of a statement. If it's just whitespace, // we'll just accept it and move on. Otherwise, parsing is attempted // on the accumulated statement text, and errors are reported. The // pending input is accepted or rejected based on that parsing. freshen(); if (pending.trim().length() == 0) { accept(); continue; } // Try to parse the current code buffer final String code = current(); if (parse(code)) { // Code parsed fine accept(); complete = true; } else if (error == null) { // Um... ??? accept(); } else { // Parse failed, spit out something to the user report(); } } // Get and return the statement. return accepted(complete); } private void inspect() { if (lastResult == null){ err.println(MESSAGES.getMessage("command.inspect.no_result")); return; } // // FIXME: Update this once we have joint compile happy in the core build? // // this should read: groovy.inspect.swingui.ObjectBrowser.inspect(lastResult) // but this doesn't compile since ObjectBrowser.groovy is compiled after this class. // // // FIXME: When launching this, if the user tries to "exit" and the window is still opened, the shell will // hang... not really nice user experience IMO. Should try to fix this if we can. // try { Class type = Class.forName("groovy.inspect.swingui.ObjectBrowser"); Method method = type.getMethod("inspect", new Class[]{ Object.class }); method.invoke(type, new Object[]{ lastResult }); } catch (Exception e) { err.println("Cannot invoke ObjectBrowser"); e.printStackTrace(); } } /** * Returns the accepted statement as a string. If not complete, returns empty string. */ private String accepted(final boolean complete) { if (complete) { return accepted.toString(); } return ""; } /** * Returns the current statement, including pending text. */ private String current() { return accepted.toString() + pending + NEW_LINE; } /** * Accepts the pending text into the statement. */ private void accept() { accepted.append(pending).append(NEW_LINE); line += 1; } /** * Clears accepted if stale. */ private void freshen() { if (stale) { accepted.setLength(0); stale = false; } } //--------------------------------------------------------------------------- // SUPPORT ROUTINES /** * Attempts to parse the specified code with the specified tolerance. * Updates the <code>parser</code> and <code>error</code> members * appropriately. Returns true if the text parsed, false otherwise. * The attempts to identify and suppress errors resulting from the * unfinished source text. */ private boolean parse(final String code, final int tolerance) { assert code != null; boolean parsed = false; parser = null; error = null; // Create the parser and attempt to parse the text as a top-level statement. try { parser = SourceUnit.create("groovysh-script", code, tolerance); parser.parse(); parsed = true; } // We report errors other than unexpected EOF to the user. catch (CompilationFailedException e) { if (parser.getErrorCollector().getErrorCount() > 1 || !parser.failedWithUnexpectedEOF()) { error = e; } } catch (Exception e) { error = e; } return parsed; } private boolean parse(final String code) { return parse(code, 1); } /** * Reports the last parsing error to the user. */ private void report() { err.println("Discarding invalid text:"); // TODO: i18n new ErrorReporter(error, false).write(err); } //----------------------------------------------------------------------- // COMMANDS // // TODO: Add a simple command to read in a File/URL into the buffer for execution, but need better command // support first (aka GShell) so we can allow commands to take args, etc. // private static final int COMMAND_ID_EXIT = 0; private static final int COMMAND_ID_HELP = 1; private static final int COMMAND_ID_DISCARD = 2; private static final int COMMAND_ID_DISPLAY = 3; private static final int COMMAND_ID_EXPLAIN = 4; private static final int COMMAND_ID_EXECUTE = 5; private static final int COMMAND_ID_BINDING = 6; private static final int COMMAND_ID_DISCARD_LOADED_CLASSES = 7; private static final int COMMAND_ID_INSPECT = 8; private static final int LAST_COMMAND_ID = 8; private static final String[] COMMANDS = { "exit", "help", "discard", "display", "explain", "execute", "binding", "discardclasses", "inspect" }; private static final Map<String, Integer> COMMAND_MAPPINGS = new HashMap<String, Integer>(); static { for (int i = 0; i <= LAST_COMMAND_ID; i++) { COMMAND_MAPPINGS.put(COMMANDS[i], i); } // A few synonyms COMMAND_MAPPINGS.put("quit", COMMAND_ID_EXIT); COMMAND_MAPPINGS.put("go", COMMAND_ID_EXECUTE); } private static final Map<String, String> COMMAND_HELP = new HashMap<String, String>(); static { COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXIT], "exit/quit - " + MESSAGES.getMessage("command.exit.descripion")); COMMAND_HELP.put(COMMANDS[COMMAND_ID_HELP], "help - " + MESSAGES.getMessage("command.help.descripion")); COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISCARD], "discard - " + MESSAGES.getMessage("command.discard.descripion")); COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISPLAY], "display - " + MESSAGES.getMessage("command.display.descripion")); // // FIXME: If this is disabled, then er comment it out, so it doesn't confuse the user // COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXPLAIN], "explain - " + MESSAGES.getMessage("command.explain.descripion")); COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXECUTE], "execute/go - " + MESSAGES.getMessage("command.execute.descripion")); COMMAND_HELP.put(COMMANDS[COMMAND_ID_BINDING], "binding - " + MESSAGES.getMessage("command.binding.descripion")); COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISCARD_LOADED_CLASSES], "discardclasses - " + MESSAGES.getMessage("command.discardclasses.descripion")); COMMAND_HELP.put(COMMANDS[COMMAND_ID_INSPECT], "inspect - " + MESSAGES.getMessage("command.inspect.descripion")); } /** * Displays help text about available commands. */ private void displayHelp() { out.println(MESSAGES.getMessage("command.help.available_commands")); for (int i = 0; i <= LAST_COMMAND_ID; i++) { out.print(" "); out.println(COMMAND_HELP.get(COMMANDS[i])); } } /** * Displays the accepted statement. */ private void displayStatement() { final String[] lines = accepted.toString().split(NEW_LINE); if (lines.length == 1 && lines[0].trim().equals("")) { out.println(MESSAGES.getMessage("command.display.buffer_empty")); } else { // Eh, try to pick a decent pad size... but don't try to hard int padsize = 2; if (lines.length >= 10) padsize++; if (lines.length >= 100) padsize++; if (lines.length >= 1000) padsize++; // Dump the current buffer with a line number prefix for (int i = 0; i < lines.length; i++) { // Normalize the field size of the line number String lineno = DefaultGroovyMethods.padLeft(String.valueOf(i + 1), padsize, " "); out.print(lineno); out.print("> "); out.println(lines[i]); } } } /** * Displays the current binding used when instantiating the shell. */ private void displayBinding() { Binding context = shell.getContext(); Map variables = context.getVariables(); Set set = variables.keySet(); if (set.isEmpty()) { out.println(MESSAGES.getMessage("command.binding.binding_empty")); } else { out.println(MESSAGES.getMessage("command.binding.available_variables")); for (Object key : set) { out.print(" "); out.print(key); out.print(" = "); out.println(variables.get(key)); } } } /** * Attempts to parse the accepted statement and display the parse tree for it. */ private void explainStatement() { if (parse(accepted(true), 10) || error == null) { out.println(MESSAGES.getMessage("command.explain.tree_header")); //out.println(tree); } else { out.println(MESSAGES.getMessage("command.explain.unparsable")); } } private void resetLoadedClasses() { shell.resetLoadedClasses(); out.println(MESSAGES.getMessage("command.discardclasses.classdefs_discarded")); } // // Custom JLine Completors to fancy up the user experience more. // private class CommandNameCompletor extends SimpleCompletor { public CommandNameCompletor() { super(new String[0]); // Add each command name/alias as a candidate Iterator iter = COMMAND_MAPPINGS.keySet().iterator(); while (iter.hasNext()) { addCandidateString((String)iter.next()); } } } // // TODO: Add local variable completion? // // // TODO: Add shell method completion? // /* private void findShellMethods(String complete) { List methods = shell.getMetaClass().getMetaMethods(); for (Iterator i = methods.iterator(); i.hasNext();) { MetaMethod method = (MetaMethod) i.next(); if (method.getName().startsWith(complete)) { if (method.getParameterTypes().length > 0) { completions.add(method.getName() + "("); } else { completions.add(method.getName() + "()"); } } } } private void findLocalVariables(String complete) { Set names = shell.getContext().getVariables().keySet(); for (Iterator i = names.iterator(); i.hasNext();) { String name = (String) i.next(); if (name.startsWith(complete)) { completions.add(name); } } } */ }