001: /*
002: $Id: InteractiveShell.java 3948 2006-08-01 09:50:46Z glaforge $
003:
004: Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
005:
006: Redistribution and use of this software and associated documentation
007: ("Software"), with or without modification, are permitted provided
008: that the following conditions are met:
009:
010: 1. Redistributions of source code must retain copyright
011: statements and notices. Redistributions must also contain a
012: copy of this document.
013:
014: 2. Redistributions in binary form must reproduce the
015: above copyright notice, this list of conditions and the
016: following disclaimer in the documentation and/or other
017: materials provided with the distribution.
018:
019: 3. The name "groovy" must not be used to endorse or promote
020: products derived from this Software without prior written
021: permission of The Codehaus. For written permission,
022: please contact info@codehaus.org.
023:
024: 4. Products derived from this Software may not be called "groovy"
025: nor may "groovy" appear in their names without prior written
026: permission of The Codehaus. "groovy" is a registered
027: trademark of The Codehaus.
028:
029: 5. Due credit should be given to The Codehaus -
030: http://groovy.codehaus.org/
031:
032: THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
033: ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
034: NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
035: FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
036: THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
037: INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
038: (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
039: SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
040: HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
041: STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
042: ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
043: OF THE POSSIBILITY OF SUCH DAMAGE.
044:
045: */
046: package groovy.ui;
047:
048: import groovy.lang.Binding;
049: import groovy.lang.GroovyShell;
050:
051: import java.io.IOException;
052: import java.io.InputStream;
053: import java.io.PrintStream;
054: import java.lang.reflect.Method;
055: import java.util.HashMap;
056: import java.util.Iterator;
057: import java.util.Map;
058: import java.util.Set;
059:
060: import org.codehaus.groovy.control.CompilationFailedException;
061: import org.codehaus.groovy.control.SourceUnit;
062: import org.codehaus.groovy.runtime.InvokerHelper;
063: import org.codehaus.groovy.runtime.InvokerInvocationException;
064: import org.codehaus.groovy.sandbox.ui.Prompt;
065: import org.codehaus.groovy.sandbox.ui.PromptFactory;
066: import org.codehaus.groovy.tools.ErrorReporter;
067:
068: /**
069: * A simple interactive shell for evaluating groovy expressions
070: * on the command line
071: *
072: * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
073: * @author <a href="mailto:cpoirier@dreaming.org" >Chris Poirier</a>
074: * @author Yuri Schimke
075: * @author Brian McCallistair
076: * @author Guillaume Laforge
077: * @author Dierk Koenig, include the inspect command, June 2005
078: * @version $Revision: 3948 $
079: */
080: public class InteractiveShell {
081: private final GroovyShell shell;
082: private final Prompt prompt;
083: private final InputStream in;
084: private final PrintStream out;
085: private final PrintStream err;
086: private Object lastResult;
087:
088: /**
089: * Entry point when called directly.
090: */
091: public static void main(String args[]) {
092: try {
093: final InteractiveShell groovy = new InteractiveShell();
094: groovy.run(args);
095: } catch (Exception e) {
096: System.err.println("Caught: " + e);
097: e.printStackTrace();
098: }
099: }
100:
101: /**
102: * Default constructor.
103: */
104: public InteractiveShell() {
105: this (System.in, System.out, System.err);
106: }
107:
108: public InteractiveShell(final InputStream in,
109: final PrintStream out, final PrintStream err) {
110: this (null, new Binding(), in, out, err);
111: }
112:
113: /**
114: * Constructs a new InteractiveShell instance
115: *
116: * @param binding The binding instance
117: * @param in The input stream to use
118: * @param out The output stream to use
119: * @param err The error stream to use
120: */
121: public InteractiveShell(Binding binding, final InputStream in,
122: final PrintStream out, final PrintStream err) {
123: this (null, binding, in, out, err);
124: }
125:
126: /**
127: * Constructs a new InteractiveShell instance
128: *
129: * @param parent The parent ClassLoader
130: * @param binding The binding instance
131: * @param in The input stream to use
132: * @param out The output stream to use
133: * @param err The error stream to use
134: */
135: public InteractiveShell(ClassLoader parent, Binding binding,
136: final InputStream in, final PrintStream out,
137: final PrintStream err) {
138: this .in = in;
139: this .out = out;
140: this .err = err;
141: prompt = PromptFactory.buildPrompt(in, out, err);
142: prompt.setPrompt("groovy> ");
143: if (parent != null) {
144: shell = new GroovyShell(parent, binding);
145: } else {
146: shell = new GroovyShell(binding);
147: }
148: Map map = shell.getContext().getVariables();
149: if (map.get("shell") != null) {
150: map.put("shell", shell);
151: }
152: }
153:
154: //---------------------------------------------------------------------------
155: // COMMAND LINE PROCESSING LOOP
156:
157: /**
158: * Reads commands and statements from input stream and processes them.
159: */
160: public void run(String[] args) throws Exception {
161: final String version = InvokerHelper.getVersion();
162:
163: out.println("Let's get Groovy!");
164: out.println("================");
165: out.println("Version: " + version + " JVM: "
166: + System.getProperty("java.vm.version"));
167: out.println("Type 'exit' to terminate the shell");
168: out.println("Type 'help' for command help");
169: out.println("Type 'go' to execute the statements");
170:
171: boolean running = true;
172: while (running) {
173: // Read a single top-level statement from the command line,
174: // trapping errors as they happen. We quit on null.
175: final String command = read();
176: if (command == null) {
177: close();
178: break;
179: }
180:
181: reset();
182:
183: if (command.length() > 0) {
184: // We have a command that parses, so evaluate it.
185: try {
186: lastResult = shell.evaluate(command,
187: "CommandLine.groovy");
188: out.println("\n===> " + lastResult);
189: } catch (CompilationFailedException e) {
190: err.println(e);
191: } catch (Throwable e) {
192: if (e instanceof InvokerInvocationException) {
193: InvokerInvocationException iie = (InvokerInvocationException) e;
194: e = iie.getCause();
195: }
196: filterAndPrintStackTrace(e);
197: }
198: }
199: }
200: }
201:
202: /**
203: * Filter stacktraces to show only relevant lines of the exception thrown.
204: *
205: * @param e the throwable whose stacktrace needs to be filtered
206: */
207: private void filterAndPrintStackTrace(Throwable e) {
208: err.println("Caught: " + e);
209: StackTraceElement[] stackTrace = e.getStackTrace();
210: for (int i = 0; i < stackTrace.length; i++) {
211: StackTraceElement element = stackTrace[i];
212: String fileName = element.getFileName();
213: if ((fileName == null || (!fileName.endsWith(".java"))
214: && (!element.getClassName().startsWith("gjdk")))) {
215: err.println("\tat " + element);
216: }
217: }
218: }
219:
220: protected void close() {
221: prompt.close();
222: }
223:
224: //---------------------------------------------------------------------------
225: // COMMAND LINE PROCESSING MACHINERY
226:
227: private StringBuffer accepted = new StringBuffer(); // The statement text accepted to date
228: private String pending = null; // A line of statement text not yet accepted
229: private int line = 1; // The current line number
230:
231: private boolean stale = false; // Set to force clear of accepted
232:
233: private SourceUnit parser = null; // A SourceUnit used to check the statement
234: private Exception error = null; // Any actual syntax error caught during parsing
235:
236: /**
237: * Resets the command-line processing machinery after use.
238: */
239:
240: protected void reset() {
241: stale = true;
242: pending = null;
243: line = 1;
244:
245: parser = null;
246: error = null;
247: }
248:
249: /**
250: * Reads a single statement from the command line. Also identifies
251: * and processes command shell commands. Returns the command text
252: * on success, or null when command processing is complete.
253: * <p/>
254: * NOTE: Changed, for now, to read until 'execute' is issued. At
255: * 'execute', the statement must be complete.
256: */
257:
258: protected String read() {
259: reset();
260: out.println("");
261:
262: boolean complete = false;
263: boolean done = false;
264:
265: while (/* !complete && */!done) {
266:
267: // Read a line. If IOException or null, or command "exit", terminate
268: // processing.
269:
270: try {
271: pending = prompt.readLine();
272: } catch (IOException e) {
273: }
274:
275: if (pending == null
276: || (COMMAND_MAPPINGS.containsKey(pending) && ((Integer) COMMAND_MAPPINGS
277: .get(pending)).intValue() == COMMAND_ID_EXIT)) {
278: return null; // <<<< FLOW CONTROL <<<<<<<<
279: }
280:
281: // First up, try to process the line as a command and proceed accordingly.
282: if (COMMAND_MAPPINGS.containsKey(pending)) {
283: int code = ((Integer) COMMAND_MAPPINGS.get(pending))
284: .intValue();
285: switch (code) {
286: case COMMAND_ID_HELP:
287: displayHelp();
288: break;
289:
290: case COMMAND_ID_DISCARD:
291: reset();
292: done = true;
293: break;
294:
295: case COMMAND_ID_DISPLAY:
296: displayStatement();
297: break;
298:
299: case COMMAND_ID_EXPLAIN:
300: explainStatement();
301: break;
302:
303: case COMMAND_ID_BINDING:
304: displayBinding();
305: break;
306:
307: case COMMAND_ID_EXECUTE:
308: if (complete) {
309: done = true;
310: } else {
311: err.println("statement not complete");
312: }
313: break;
314: case COMMAND_ID_DISCARD_LOADED_CLASSES:
315: resetLoadedClasses();
316: break;
317: case COMMAND_ID_INSPECT:
318: inspect();
319: break;
320: }
321:
322: continue; // <<<< LOOP CONTROL <<<<<<<<
323: }
324:
325: // Otherwise, it's part of a statement. If it's just whitespace,
326: // we'll just accept it and move on. Otherwise, parsing is attempted
327: // on the cumulated statement text, and errors are reported. The
328: // pending input is accepted or rejected based on that parsing.
329:
330: freshen();
331:
332: if (pending.trim().length() == 0) {
333: accept();
334: continue; // <<<< LOOP CONTROL <<<<<<<<
335: }
336:
337: final String code = current();
338:
339: if (parse(code, 1)) {
340: accept();
341: complete = true;
342: } else if (error == null) {
343: accept();
344: } else {
345: report();
346: }
347:
348: }
349:
350: // Get and return the statement.
351: return accepted(complete);
352: }
353:
354: private void inspect() {
355: if (null == lastResult) {
356: err
357: .println("nothing to inspect (preceding \"go\" missing?)");
358: return;
359: }
360: // this should read: groovy.inspect.swingui.ObjectBrowser.inspect(lastResult)
361: // but this doesnt compile since ObjectBrowser.groovy is compiled after this class.
362: try {
363: Class browserClass = Class
364: .forName("groovy.inspect.swingui.ObjectBrowser");
365: Method inspectMethod = browserClass.getMethod("inspect",
366: new Class[] { Object.class });
367: inspectMethod.invoke(browserClass,
368: new Object[] { lastResult });
369: } catch (Exception e) {
370: err.println("cannot invoke ObjectBrowser");
371: e.printStackTrace();
372: }
373: }
374:
375: /**
376: * Returns the accepted statement as a string. If not <code>complete</code>,
377: * returns the empty string.
378: */
379: private String accepted(boolean complete) {
380: if (complete) {
381: return accepted.toString();
382: }
383: return "";
384: }
385:
386: /**
387: * Returns the current statement, including pending text.
388: */
389: private String current() {
390: return accepted.toString() + pending + "\n";
391: }
392:
393: /**
394: * Accepts the pending text into the statement.
395: */
396: private void accept() {
397: accepted.append(pending).append("\n");
398: line += 1;
399: }
400:
401: /**
402: * Clears accepted if stale.
403: */
404: private void freshen() {
405: if (stale) {
406: accepted.setLength(0);
407: stale = false;
408: }
409: }
410:
411: //---------------------------------------------------------------------------
412: // SUPPORT ROUTINES
413:
414: /**
415: * Attempts to parse the specified code with the specified tolerance.
416: * Updates the <code>parser</code> and <code>error</code> members
417: * appropriately. Returns true if the text parsed, false otherwise.
418: * The attempts to identify and suppress errors resulting from the
419: * unfinished source text.
420: */
421: private boolean parse(String code, int tolerance) {
422: boolean parsed = false;
423:
424: parser = null;
425: error = null;
426:
427: // Create the parser and attempt to parse the text as a top-level statement.
428: try {
429: parser = SourceUnit.create("groovysh script", code,
430: tolerance);
431: parser.parse();
432:
433: parsed = true;
434: }
435:
436: // We report errors other than unexpected EOF to the user.
437: catch (CompilationFailedException e) {
438: if (parser.getErrorCollector().getErrorCount() > 1
439: || !parser.failedWithUnexpectedEOF()) {
440: error = e;
441: }
442: } catch (Exception e) {
443: error = e;
444: }
445:
446: return parsed;
447: }
448:
449: /**
450: * Reports the last parsing error to the user.
451: */
452:
453: private void report() {
454: err.println("Discarding invalid text:");
455: new ErrorReporter(error, false).write(err);
456: }
457:
458: //-----------------------------------------------------------------------
459: // COMMANDS
460:
461: private static final int COMMAND_ID_EXIT = 0;
462: private static final int COMMAND_ID_HELP = 1;
463: private static final int COMMAND_ID_DISCARD = 2;
464: private static final int COMMAND_ID_DISPLAY = 3;
465: private static final int COMMAND_ID_EXPLAIN = 4;
466: private static final int COMMAND_ID_EXECUTE = 5;
467: private static final int COMMAND_ID_BINDING = 6;
468: private static final int COMMAND_ID_DISCARD_LOADED_CLASSES = 7;
469: private static final int COMMAND_ID_INSPECT = 8;
470:
471: private static final int LAST_COMMAND_ID = 8;
472:
473: private static final String[] COMMANDS = { "exit", "help",
474: "discard", "display", "explain", "execute", "binding",
475: "discardclasses", "inspect" };
476:
477: private static final Map COMMAND_MAPPINGS = new HashMap();
478:
479: static {
480: for (int i = 0; i <= LAST_COMMAND_ID; i++) {
481: COMMAND_MAPPINGS.put(COMMANDS[i], new Integer(i));
482: }
483:
484: // A few synonyms
485:
486: COMMAND_MAPPINGS.put("quit", new Integer(COMMAND_ID_EXIT));
487: COMMAND_MAPPINGS.put("go", new Integer(COMMAND_ID_EXECUTE));
488: }
489:
490: private static final Map COMMAND_HELP = new HashMap();
491:
492: static {
493: COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXIT],
494: "exit/quit - terminates processing");
495: COMMAND_HELP.put(COMMANDS[COMMAND_ID_HELP],
496: "help - displays this help text");
497: COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISCARD],
498: "discard - discards the current statement");
499: COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISPLAY],
500: "display - displays the current statement");
501: COMMAND_HELP
502: .put(
503: COMMANDS[COMMAND_ID_EXPLAIN],
504: "explain - explains the parsing of the current statement (currently disabled)");
505: COMMAND_HELP
506: .put(COMMANDS[COMMAND_ID_EXECUTE],
507: "execute/go - temporary command to cause statement execution");
508: COMMAND_HELP
509: .put(COMMANDS[COMMAND_ID_BINDING],
510: "binding - shows the binding used by this interactive shell");
511: COMMAND_HELP
512: .put(COMMANDS[COMMAND_ID_DISCARD_LOADED_CLASSES],
513: "discardclasses - discards all former unbound class definitions");
514: COMMAND_HELP
515: .put(
516: COMMANDS[COMMAND_ID_INSPECT],
517: "inspect - opens ObjectBrowser on expression returned from previous \"go\"");
518: }
519:
520: /**
521: * Displays help text about available commands.
522: */
523: private void displayHelp() {
524: out
525: .println("Available commands (must be entered without extraneous characters):");
526: for (int i = 0; i <= LAST_COMMAND_ID; i++) {
527: out.println((String) COMMAND_HELP.get(COMMANDS[i]));
528: }
529: }
530:
531: /**
532: * Displays the accepted statement.
533: */
534: private void displayStatement() {
535: final String[] lines = accepted.toString().split("\n");
536: for (int i = 0; i < lines.length; i++) {
537: out.println((i + 1) + "> " + lines[i]);
538: }
539: }
540:
541: /**
542: * Displays the current binding used when instanciating the shell.
543: */
544: private void displayBinding() {
545: out.println("Available variables in the current binding");
546: Binding context = shell.getContext();
547: Map variables = context.getVariables();
548: Set set = variables.keySet();
549: if (set.isEmpty()) {
550: out.println("The current binding is empty.");
551: } else {
552: for (Iterator it = set.iterator(); it.hasNext();) {
553: String key = (String) it.next();
554: out.println(key + " = " + variables.get(key));
555: }
556: }
557: }
558:
559: /**
560: * Attempts to parse the accepted statement and display the
561: * parse tree for it.
562: */
563: private void explainStatement() {
564: if (parse(accepted(true), 10) || error == null) {
565: out.println("Parse tree:");
566: //out.println(tree);
567: } else {
568: out.println("Statement does not parse");
569: }
570: }
571:
572: private void resetLoadedClasses() {
573: shell.resetLoadedClasses();
574: out
575: .println("all former unbound class definitions are discarded");
576: }
577: }
|