fixed build console
[groovy.git] / src / main / groovy / ui / InteractiveShell.java
blob537ef78f1de3a9783f5a7ce082fa0e1112c6be60
1 /*
2 * Copyright 2003-2007 the original author or authors.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package groovy.ui;
19 import groovy.lang.Binding;
20 import groovy.lang.Closure;
21 import groovy.lang.GroovyShell;
22 import org.codehaus.groovy.tools.shell.util.MessageSource;
24 import org.codehaus.groovy.control.CompilationFailedException;
25 import org.codehaus.groovy.control.SourceUnit;
26 import org.codehaus.groovy.runtime.InvokerHelper;
27 import org.codehaus.groovy.runtime.InvokerInvocationException;
28 import org.codehaus.groovy.runtime.DefaultGroovyMethods;
29 import org.codehaus.groovy.tools.ErrorReporter;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.PrintStream;
34 import java.io.Writer;
35 import java.io.OutputStreamWriter;
36 import java.io.PrintWriter;
37 import java.lang.reflect.Method;
38 import java.util.HashMap;
39 import java.util.Iterator;
40 import java.util.Map;
41 import java.util.Set;
43 import org.apache.commons.cli.CommandLine;
44 import org.apache.commons.cli.CommandLineParser;
45 import org.apache.commons.cli.OptionBuilder;
46 import org.apache.commons.cli.PosixParser;
47 import org.apache.commons.cli.Options;
48 import org.apache.commons.cli.HelpFormatter;
50 import jline.ConsoleReader;
51 import jline.SimpleCompletor;
54 // TODO: See if there is any reason why this class is implemented in Java instead of Groovy, and if there
55 // is none, then port it over ;-)
59 // NOTE: After GShell becomes a little more mature, this shell could be easily implemented as a set of GShell
60 // commands, and would inherit a lot of functionality and could be extended easily to allow groovysh
61 // to become very, very powerful
64 /**
65 * A simple interactive shell for evaluating groovy expressions on the command line (aka. groovysh).
67 * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
68 * @author <a href="mailto:cpoirier@dreaming.org" >Chris Poirier</a>
69 * @author Yuri Schimke
70 * @author Brian McCallistair
71 * @author Guillaume Laforge
72 * @author Dierk Koenig, include the inspect command, June 2005
73 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
75 * @version $Revision$
77 public class InteractiveShell
78 implements Runnable
80 private static final String NEW_LINE = System.getProperty("line.separator");
81 private static final MessageSource MESSAGES = new MessageSource(InteractiveShell.class);
83 private final GroovyShell shell;
84 private final InputStream in; // FIXME: This doesn't really need to be a field, but hold on to it for now
85 private final PrintStream out;
86 private final PrintStream err;
87 private final ConsoleReader reader;
89 private Object lastResult;
90 private Closure beforeExecution;
91 private Closure afterExecution;
93 /**
94 * Entry point when called directly.
96 public static void main(final String args[]) {
97 try {
98 processCommandLineArguments(args);
100 final InteractiveShell groovy = new InteractiveShell();
101 groovy.run();
103 catch (Exception e) {
104 System.err.println("FATAL: " + e);
105 e.printStackTrace();
106 System.exit(1);
109 System.exit(0);
113 * Process cli args when the shell is invoked via main().
115 * @noinspection AccessStaticViaInstance
117 private static void processCommandLineArguments(final String[] args) throws Exception {
118 assert args != null;
121 // TODO: Let this take a single, optional argument which is a file or URL to run?
124 Options options = new Options();
126 options.addOption(OptionBuilder.withLongOpt("help")
127 .withDescription(MESSAGES.getMessage("cli.option.help.description"))
128 .create('h'));
130 options.addOption(OptionBuilder.withLongOpt("version")
131 .withDescription(MESSAGES.getMessage("cli.option.version.description"))
132 .create('V'));
135 // TODO: Add more options, maybe even add an option to prime the buffer from a URL or File?
139 // FIXME: This does not currently barf on unsupported options short options, though it does for long ones.
140 // Same problem with commons-cli 1.0 and 1.1
143 CommandLineParser parser = new PosixParser();
144 CommandLine line = parser.parse(options, args, true);
145 String[] lineargs = line.getArgs();
147 // Puke if there were arguments, we don't support any right now
148 if (lineargs.length != 0) {
149 System.err.println(MESSAGES.format("cli.info.unexpected_args", new Object[] { DefaultGroovyMethods.join(lineargs, " ") }));
150 System.exit(1);
153 PrintWriter writer = new PrintWriter(System.out);
155 if (line.hasOption('h')) {
156 HelpFormatter formatter = new HelpFormatter();
157 formatter.printHelp(
158 writer,
159 80, // width
160 "groovysh [options]",
162 options,
163 4, // left pad
164 4, // desc pad
166 false); // auto usage
168 writer.flush();
169 System.exit(0);
172 if (line.hasOption('V')) {
173 writer.println(MESSAGES.format("cli.info.version", new Object[] { InvokerHelper.getVersion() }));
174 writer.flush();
175 System.exit(0);
180 * Default constructor, initializes uses new binding and system streams.
182 public InteractiveShell() throws IOException {
183 this(System.in, System.out, System.err);
187 * Constructs a new InteractiveShell instance
189 * @param in The input stream to use
190 * @param out The output stream to use
191 * @param err The error stream to use
193 public InteractiveShell(final InputStream in, final PrintStream out, final PrintStream err) throws IOException {
194 this(null, new Binding(), in, out, err);
198 * Constructs a new InteractiveShell instance
200 * @param binding The binding instance
201 * @param in The input stream to use
202 * @param out The output stream to use
203 * @param err The error stream to use
205 public InteractiveShell(final Binding binding, final InputStream in, final PrintStream out, final PrintStream err) throws IOException {
206 this(null, binding, in, out, err);
210 * Constructs a new InteractiveShell instance
212 * @param parent The parent ClassLoader
213 * @param binding The binding instance
214 * @param in The input stream to use
215 * @param out The output stream to use
216 * @param err The error stream to use
218 public InteractiveShell(final ClassLoader parent, final Binding binding, final InputStream in, final PrintStream out, final PrintStream err) throws IOException {
219 assert binding != null;
220 assert in != null;
221 assert out != null;
222 assert err != null;
224 this.in = in;
225 this.out = out;
226 this.err = err;
228 // Initialize the JLine console input reader
229 Writer writer = new OutputStreamWriter(out);
230 reader = new ConsoleReader(in, writer);
231 reader.setDefaultPrompt("groovy> ");
233 // Add some completors to fancy things up
234 reader.addCompletor(new CommandNameCompletor());
236 if (parent != null) {
237 shell = new GroovyShell(parent, binding);
239 else {
240 shell = new GroovyShell(binding);
243 // Add some default variables to the shell
244 Map map = shell.getContext().getVariables();
247 // FIXME: Um, is this right? Only set the "shell" var in the context if its set already?
250 if (map.get("shell") != null) {
251 map.put("shell", shell);
255 //---------------------------------------------------------------------------
256 // COMMAND LINE PROCESSING LOOP
259 // TODO: Add a general error display handler, and probably add a "ERROR: " prefix to the result for clarity ?
260 // Maybe add one for WARNING's too?
264 * Reads commands and statements from input stream and processes them.
266 public void run() {
267 // Display the startup banner
268 out.println(MESSAGES.format("startup_banner.0", new Object[] { InvokerHelper.getVersion(), System.getProperty("java.version") }));
269 out.println(MESSAGES.getMessage("startup_banner.1"));
271 while (true) {
272 // Read a code block to evaluate; this will deal with basic error handling
273 final String code = read();
275 // If we got a null, then quit
276 if (code == null) {
277 break;
280 reset();
282 // Evaluate the code block if it was parsed
283 if (code.length() > 0) {
284 try {
285 if (beforeExecution != null) {
286 beforeExecution.call();
289 lastResult = shell.evaluate(code);
291 if (afterExecution != null) {
292 afterExecution.call();
295 // Shows the result of the evaluated code
296 out.print("===> ");
297 out.println(lastResult);
299 catch (CompilationFailedException e) {
300 err.println(e);
302 catch (Throwable e) {
303 // Unroll invoker exceptions
304 if (e instanceof InvokerInvocationException) {
305 e = e.getCause();
308 filterAndPrintStackTrace(e);
315 * A closure that is executed before the exection of a given script
317 * @param beforeExecution The closure to execute
319 public void setBeforeExecution(final Closure beforeExecution) {
320 this.beforeExecution = beforeExecution;
324 * A closure that is executed after the execution of the last script. The result of the
325 * execution is passed as the first argument to the closure (the value of 'it')
327 * @param afterExecution The closure to execute
329 public void setAfterExecution(final Closure afterExecution) {
330 this.afterExecution = afterExecution;
334 * Filter stacktraces to show only relevant lines of the exception thrown.
336 * @param cause the throwable whose stacktrace needs to be filtered
338 private void filterAndPrintStackTrace(final Throwable cause) {
339 assert cause != null;
342 // TODO: Use message...
345 err.print("ERROR: ");
346 err.println(cause);
348 cause.printStackTrace(err);
351 // FIXME: What is the point of this? AFAICT, this just produces crappy/corrupt traces and is completely useless
354 // StackTraceElement[] stackTrace = e.getStackTrace();
356 // for (int i = 0; i < stackTrace.length; i++) {
357 // StackTraceElement element = stackTrace[i];
358 // String fileName = element.getFileName();
360 // if ((fileName==null || (!fileName.endsWith(".java")) && (!element.getClassName().startsWith("gjdk")))) {
361 // err.print("\tat ");
362 // err.println(element);
363 // }
364 // }
367 //---------------------------------------------------------------------------
368 // COMMAND LINE PROCESSING MACHINERY
370 /** The statement text accepted to date */
371 private StringBuffer accepted = new StringBuffer();
373 /** A line of statement text not yet accepted */
374 private String pending;
377 // FIXME: Doesn't look like 'line' is really used/needed anywhere... could drop it, or perhaps
378 // could use it to update the prompt er something to show the buffer size?
381 /** The current line number */
382 private int line;
384 /** Set to force clear of accepted */
385 private boolean stale = false;
387 /** A SourceUnit used to check the statement */
388 private SourceUnit parser;
390 /** Any actual syntax error caught during parsing */
391 private Exception error;
394 * Resets the command-line processing machinery after use.
396 protected void reset() {
397 stale = true;
398 pending = null;
399 line = 1;
400 parser = null;
401 error = null;
405 // FIXME: This Javadoc is not correct... read() will return the full code block read until "go"
409 * Reads a single statement from the command line. Also identifies
410 * and processes command shell commands. Returns the command text
411 * on success, or null when command processing is complete.
413 * NOTE: Changed, for now, to read until 'execute' is issued. At
414 * 'execute', the statement must be complete.
416 protected String read() {
417 reset();
419 boolean complete = false;
420 boolean done = false;
422 while (/* !complete && */ !done) {
423 // Read a line. If IOException or null, or command "exit", terminate processing.
424 try {
425 pending = reader.readLine();
427 catch (IOException e) {
429 // FIXME: Shouldn't really eat this exception, may be something we need to see... ?
433 // If result is null then we are shutting down
434 if (pending == null) {
435 return null;
438 // First up, try to process the line as a command and proceed accordingly
439 // Trim what we have for use in command bits, so things like "help " actually show the help screen
440 String command = pending.trim();
442 if (COMMAND_MAPPINGS.containsKey(command)) {
443 int code = ((Integer)COMMAND_MAPPINGS.get(command)).intValue();
445 switch (code) {
446 case COMMAND_ID_EXIT:
447 return null;
449 case COMMAND_ID_HELP:
450 displayHelp();
451 break;
453 case COMMAND_ID_DISCARD:
454 reset();
455 done = true;
456 break;
458 case COMMAND_ID_DISPLAY:
459 displayStatement();
460 break;
462 case COMMAND_ID_EXPLAIN:
463 explainStatement();
464 break;
466 case COMMAND_ID_BINDING:
467 displayBinding();
468 break;
470 case COMMAND_ID_EXECUTE:
471 if (complete) {
472 done = true;
474 else {
475 err.println(MESSAGES.getMessage("command.execute.not_complete"));
477 break;
479 case COMMAND_ID_DISCARD_LOADED_CLASSES:
480 resetLoadedClasses();
481 break;
483 case COMMAND_ID_INSPECT:
484 inspect();
485 break;
487 default:
488 throw new Error("BUG: Unknown command for code: " + code);
491 // Finished processing command bits, continue reading, don't need to process code
492 continue;
495 // Otherwise, it's part of a statement. If it's just whitespace,
496 // we'll just accept it and move on. Otherwise, parsing is attempted
497 // on the cumulated statement text, and errors are reported. The
498 // pending input is accepted or rejected based on that parsing.
500 freshen();
502 if (pending.trim().length() == 0) {
503 accept();
504 continue;
507 // Try to parse the current code buffer
508 final String code = current();
510 if (parse(code)) {
511 // Code parsed fine
512 accept();
513 complete = true;
515 else if (error == null) {
516 // Um... ???
517 accept();
519 else {
520 // Parse failed, spit out something to the user
521 report();
525 // Get and return the statement.
526 return accepted(complete);
529 private void inspect() {
530 if (lastResult == null){
531 err.println(MESSAGES.getMessage("command.inspect.no_result"));
532 return;
536 // FIXME: Update this once we have joint compile happy in the core build?
538 // this should read: groovy.inspect.swingui.ObjectBrowser.inspect(lastResult)
539 // but this doesnt compile since ObjectBrowser.groovy is compiled after this class.
543 // FIXME: When launching this, if the user tries to "exit" and the window is still opened, the shell will
544 // hang... not really nice user experence IMO. Should try to fix this if we can.
547 try {
548 Class type = Class.forName("groovy.inspect.swingui.ObjectBrowser");
549 Method method = type.getMethod("inspect", new Class[]{ Object.class });
550 method.invoke(type, new Object[]{ lastResult });
552 catch (Exception e) {
553 err.println("Cannot invoke ObjectBrowser");
554 e.printStackTrace();
559 * Returns the accepted statement as a string. If not complete, returns empty string.
561 private String accepted(final boolean complete) {
562 if (complete) {
563 return accepted.toString();
565 return "";
569 * Returns the current statement, including pending text.
571 private String current() {
572 return accepted.toString() + pending + NEW_LINE;
576 * Accepts the pending text into the statement.
578 private void accept() {
579 accepted.append(pending).append(NEW_LINE);
580 line += 1;
584 * Clears accepted if stale.
586 private void freshen() {
587 if (stale) {
588 accepted.setLength(0);
589 stale = false;
593 //---------------------------------------------------------------------------
594 // SUPPORT ROUTINES
597 * Attempts to parse the specified code with the specified tolerance.
598 * Updates the <code>parser</code> and <code>error</code> members
599 * appropriately. Returns true if the text parsed, false otherwise.
600 * The attempts to identify and suppress errors resulting from the
601 * unfinished source text.
603 private boolean parse(final String code, final int tolerance) {
604 assert code != null;
606 boolean parsed = false;
607 parser = null;
608 error = null;
610 // Create the parser and attempt to parse the text as a top-level statement.
611 try {
612 parser = SourceUnit.create("groovysh-script", code, tolerance);
613 parser.parse();
614 parsed = true;
617 // We report errors other than unexpected EOF to the user.
618 catch (CompilationFailedException e) {
619 if (parser.getErrorCollector().getErrorCount() > 1 || !parser.failedWithUnexpectedEOF()) {
620 error = e;
623 catch (Exception e) {
624 error = e;
627 return parsed;
630 private boolean parse(final String code) {
631 return parse(code, 1);
635 * Reports the last parsing error to the user.
637 private void report() {
638 err.println("Discarding invalid text:"); // TODO: i18n
639 new ErrorReporter(error, false).write(err);
642 //-----------------------------------------------------------------------
643 // COMMANDS
646 // TODO: Add a simple command to read in a File/URL into the buffer for execution, but need better command
647 // support first (aka GShell) so we can allow commands to take args, etc.
650 private static final int COMMAND_ID_EXIT = 0;
651 private static final int COMMAND_ID_HELP = 1;
652 private static final int COMMAND_ID_DISCARD = 2;
653 private static final int COMMAND_ID_DISPLAY = 3;
654 private static final int COMMAND_ID_EXPLAIN = 4;
655 private static final int COMMAND_ID_EXECUTE = 5;
656 private static final int COMMAND_ID_BINDING = 6;
657 private static final int COMMAND_ID_DISCARD_LOADED_CLASSES = 7;
658 private static final int COMMAND_ID_INSPECT = 8;
659 private static final int LAST_COMMAND_ID = 8;
661 private static final String[] COMMANDS = {
662 "exit",
663 "help",
664 "discard",
665 "display",
666 "explain",
667 "execute",
668 "binding",
669 "discardclasses",
670 "inspect"
673 private static final Map COMMAND_MAPPINGS = new HashMap();
675 static {
676 for (int i = 0; i <= LAST_COMMAND_ID; i++) {
677 COMMAND_MAPPINGS.put(COMMANDS[i], Integer.valueOf(i));
680 // A few synonyms
681 COMMAND_MAPPINGS.put("quit", Integer.valueOf(COMMAND_ID_EXIT));
682 COMMAND_MAPPINGS.put("go", Integer.valueOf(COMMAND_ID_EXECUTE));
685 private static final Map COMMAND_HELP = new HashMap();
687 static {
688 COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXIT], "exit/quit - " + MESSAGES.getMessage("command.exit.descripion"));
689 COMMAND_HELP.put(COMMANDS[COMMAND_ID_HELP], "help - " + MESSAGES.getMessage("command.help.descripion"));
690 COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISCARD], "discard - " + MESSAGES.getMessage("command.discard.descripion"));
691 COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISPLAY], "display - " + MESSAGES.getMessage("command.display.descripion"));
694 // FIXME: If this is disabled, then er comment it out, so it doesn't confuse the user
697 COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXPLAIN], "explain - " + MESSAGES.getMessage("command.explain.descripion"));
698 COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXECUTE], "execute/go - " + MESSAGES.getMessage("command.execute.descripion"));
699 COMMAND_HELP.put(COMMANDS[COMMAND_ID_BINDING], "binding - " + MESSAGES.getMessage("command.binding.descripion"));
700 COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISCARD_LOADED_CLASSES],
701 "discardclasses - " + MESSAGES.getMessage("command.discardclasses.descripion"));
702 COMMAND_HELP.put(COMMANDS[COMMAND_ID_INSPECT], "inspect - " + MESSAGES.getMessage("command.inspect.descripion"));
706 * Displays help text about available commands.
708 private void displayHelp() {
709 out.println(MESSAGES.getMessage("command.help.available_commands"));
711 for (int i = 0; i <= LAST_COMMAND_ID; i++) {
712 out.print(" ");
713 out.println(COMMAND_HELP.get(COMMANDS[i]));
718 * Displays the accepted statement.
720 private void displayStatement() {
721 final String[] lines = accepted.toString().split(NEW_LINE);
723 if (lines.length == 1 && lines[0].trim().equals("")) {
724 out.println(MESSAGES.getMessage("command.display.buffer_empty"));
726 else {
727 // Eh, try to pick a decent pad size... but don't try to hard
728 int padsize = 2;
729 if (lines.length >= 10) padsize++;
730 if (lines.length >= 100) padsize++;
731 if (lines.length >= 1000) padsize++;
733 // Dump the current buffer with a line number prefix
734 for (int i = 0; i < lines.length; i++) {
735 // Normalize the field size of the line number
736 String lineno = DefaultGroovyMethods.padLeft(String.valueOf(i + 1), Integer.valueOf(padsize), " ");
738 out.print(lineno);
739 out.print("> ");
740 out.println(lines[i]);
746 * Displays the current binding used when instanciating the shell.
748 private void displayBinding() {
749 Binding context = shell.getContext();
750 Map variables = context.getVariables();
751 Set set = variables.keySet();
753 if (set.isEmpty()) {
754 out.println(MESSAGES.getMessage("command.binding.binding_empty"));
756 else {
757 out.println(MESSAGES.getMessage("command.binding.available_variables"));
759 Iterator iter = set.iterator();
760 while (iter.hasNext()) {
761 Object key = iter.next();
763 out.print(" ");
764 out.print(key);
765 out.print(" = ");
766 out.println(variables.get(key));
772 * Attempts to parse the accepted statement and display the parse tree for it.
774 private void explainStatement() {
775 if (parse(accepted(true), 10) || error == null) {
776 out.println(MESSAGES.getMessage("command.explain.tree_header"));
777 //out.println(tree);
779 else {
780 out.println(MESSAGES.getMessage("command.explain.unparsable"));
784 private void resetLoadedClasses() {
785 shell.resetLoadedClasses();
787 out.println(MESSAGES.getMessage("command.discardclasses.classdefs_discarded"));
791 // Custom JLine Completors to fancy up the user experence more.
794 private class CommandNameCompletor
795 extends SimpleCompletor
797 public CommandNameCompletor() {
798 super(new String[0]);
800 // Add each command name/alias as a candidate
801 Iterator iter = COMMAND_MAPPINGS.keySet().iterator();
803 while (iter.hasNext()) {
804 addCandidateString((String)iter.next());
810 // TODO: Add local variable completion?
814 // TODO: Add shell method complention?
818 private void findShellMethods(String complete) {
819 List methods = shell.getMetaClass().getMetaMethods();
820 for (Iterator i = methods.iterator(); i.hasNext();) {
821 MetaMethod method = (MetaMethod) i.next();
822 if (method.getName().startsWith(complete)) {
823 if (method.getParameterTypes().length > 0) {
824 completions.add(method.getName() + "(");
826 else {
827 completions.add(method.getName() + "()");
833 private void findLocalVariables(String complete) {
834 Set names = shell.getContext().getVariables().keySet();
836 for (Iterator i = names.iterator(); i.hasNext();) {
837 String name = (String) i.next();
838 if (name.startsWith(complete)) {
839 completions.add(name);