View Javadoc

1   /*
2    $Id: InteractiveShell.java,v 1.32 2006/06/21 14:29:40 graeme Exp $
3   
4    Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
5   
6    Redistribution and use of this software and associated documentation
7    ("Software"), with or without modification, are permitted provided
8    that the following conditions are met:
9   
10   1. Redistributions of source code must retain copyright
11      statements and notices.  Redistributions must also contain a
12      copy of this document.
13  
14   2. Redistributions in binary form must reproduce the
15      above copyright notice, this list of conditions and the
16      following disclaimer in the documentation and/or other
17      materials provided with the distribution.
18  
19   3. The name "groovy" must not be used to endorse or promote
20      products derived from this Software without prior written
21      permission of The Codehaus.  For written permission,
22      please contact info@codehaus.org.
23  
24   4. Products derived from this Software may not be called "groovy"
25      nor may "groovy" appear in their names without prior written
26      permission of The Codehaus. "groovy" is a registered
27      trademark of The Codehaus.
28  
29   5. Due credit should be given to The Codehaus -
30      http://groovy.codehaus.org/
31  
32   THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
33   ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
34   NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
35   FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
36   THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
37   INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
38   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
39   SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
40   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
41   STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
42   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
43   OF THE POSSIBILITY OF SUCH DAMAGE.
44  
45   */
46  package groovy.ui;
47  
48  import groovy.lang.Binding;
49  import groovy.lang.GroovyShell;
50  
51  import java.io.IOException;
52  import java.io.InputStream;
53  import java.io.PrintStream;
54  import java.lang.reflect.Method;
55  import java.util.HashMap;
56  import java.util.Iterator;
57  import java.util.Map;
58  import java.util.Set;
59  
60  import org.codehaus.groovy.control.CompilationFailedException;
61  import org.codehaus.groovy.control.SourceUnit;
62  import org.codehaus.groovy.runtime.InvokerHelper;
63  import org.codehaus.groovy.runtime.InvokerInvocationException;
64  import org.codehaus.groovy.sandbox.ui.Prompt;
65  import org.codehaus.groovy.sandbox.ui.PromptFactory;
66  import org.codehaus.groovy.tools.ErrorReporter;
67  
68  /***
69   * A simple interactive shell for evaluating groovy expressions
70   * on the command line
71   *
72   * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
73   * @author <a href="mailto:cpoirier@dreaming.org"   >Chris Poirier</a>
74   * @author Yuri Schimke
75   * @author Brian McCallistair
76   * @author Guillaume Laforge
77   * @author Dierk Koenig, include the inspect command, June 2005
78   * @version $Revision: 1.32 $
79   */
80  public class InteractiveShell {
81      private final GroovyShell shell;
82      private final Prompt prompt;
83      private final InputStream in;
84      private final PrintStream out;
85      private final PrintStream err;
86      private Object lastResult;
87  
88  
89      /***
90       * Entry point when called directly.
91       */
92      public static void main(String args[]) {
93          try {
94              final InteractiveShell groovy = new InteractiveShell();
95              groovy.run(args);
96          }
97          catch (Exception e) {
98              System.err.println("Caught: " + e);
99              e.printStackTrace();
100         }
101     }
102 
103 
104     /***
105      * Default constructor.
106      */
107     public InteractiveShell() {
108         this(System.in, System.out, System.err);
109     }
110 
111 
112     public InteractiveShell(final InputStream in, final PrintStream out, final PrintStream err) {
113         this(null,new Binding(), in, out, err);
114     }
115 
116     /***
117      * Constructs a new InteractiveShell instance
118      * 
119      * @param binding The binding instance
120      * @param in The input stream to use
121      * @param out The output stream to use
122      * @param err The error stream to use
123      */    
124     public InteractiveShell(Binding binding, final InputStream in, final PrintStream out, final PrintStream err) {
125     	this(null,binding,in,out,err);
126     }
127     
128     /***
129      * Constructs a new InteractiveShell instance
130      * 
131      * @param parent The parent ClassLoader
132      * @param binding The binding instance
133      * @param in The input stream to use
134      * @param out The output stream to use
135      * @param err The error stream to use
136      */
137     public InteractiveShell(ClassLoader parent,Binding binding, final InputStream in, final PrintStream out, 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         }
146         else {
147         	shell = new GroovyShell(binding);
148         }        
149         Map map = shell.getContext().getVariables();
150         if (map.get("shell") != null) {
151             map.put("shell", shell);
152         }
153     }    
154 
155     //---------------------------------------------------------------------------
156     // COMMAND LINE PROCESSING LOOP
157 
158     /***
159      * Reads commands and statements from input stream and processes them.
160      */
161     public void run(String[] args) throws Exception {
162         final String version = InvokerHelper.getVersion();
163 
164         out.println("Lets get Groovy!");
165         out.println("================");
166         out.println("Version: " + version + " JVM: " + 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, "CommandLine.groovy");
187                     out.println("\n===> " + lastResult);
188                 } catch (CompilationFailedException e) {
189                     err.println(e);
190                 } catch (Throwable e) {
191                     if (e instanceof InvokerInvocationException) {
192                         InvokerInvocationException iie = (InvokerInvocationException) e;
193                         e = iie.getCause();
194                     }
195                     filterAndPrintStackTrace(e);
196                 }
197             }
198         }
199     }
200 
201     /***
202      * Filter stacktraces to show only relevant lines of the exception thrown.
203      *
204      * @param e the throwable whose stacktrace needs to be filtered
205      */
206     private void filterAndPrintStackTrace(Throwable e) {
207         err.println("Caught: " + e);
208         StackTraceElement[] stackTrace = e.getStackTrace();
209         for (int i = 0; i < stackTrace.length; i++) {
210             StackTraceElement element = stackTrace[i];
211             String fileName = element.getFileName();
212             if ((fileName==null || (!fileName.endsWith(".java")) && (!element.getClassName().startsWith("gjdk")))) {
213                 err.println("\tat " + element);
214             }
215         }
216     }
217 
218     protected void close() {
219         prompt.close();
220     }
221 
222 
223     //---------------------------------------------------------------------------
224     // COMMAND LINE PROCESSING MACHINERY
225 
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     /***
238      * Resets the command-line processing machinery after use.
239      */
240 
241     protected void reset() {
242         stale = true;
243         pending = null;
244         line = 1;
245 
246         parser = null;
247         error = null;
248     }
249 
250 
251     /***
252      * Reads a single statement from the command line.  Also identifies
253      * and processes command shell commands.  Returns the command text
254      * on success, or null when command processing is complete.
255      * <p/>
256      * NOTE: Changed, for now, to read until 'execute' is issued.  At
257      * 'execute', the statement must be complete.
258      */
259 
260     protected String read() {
261         reset();
262         out.println("");
263 
264         boolean complete = false;
265         boolean done = false;
266 
267         while (/* !complete && */ !done) {
268 
269             // Read a line.  If IOException or null, or command "exit", terminate
270             // processing.
271 
272             try {
273                 pending = prompt.readLine();
274             }
275             catch (IOException e) {
276             }
277 
278             if (pending == null || (COMMAND_MAPPINGS.containsKey(pending) && ((Integer) COMMAND_MAPPINGS.get(pending)).intValue() == COMMAND_ID_EXIT)) {
279                 return null;                                  // <<<< FLOW CONTROL <<<<<<<<
280             }
281 
282             // First up, try to process the line as a command and proceed accordingly.
283             if (COMMAND_MAPPINGS.containsKey(pending)) {
284                 int code = ((Integer) COMMAND_MAPPINGS.get(pending)).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                         }
311                         else {
312                             err.println("statement not complete");
313                         }
314                         break;
315                     case COMMAND_ID_DISCARD_LOADED_CLASSES:
316                         resetLoadedClasses();
317                         break;
318                     case COMMAND_ID_INSPECT:
319                         inspect();
320                         break;
321                 }
322 
323                 continue;                                     // <<<< LOOP CONTROL <<<<<<<<
324             }
325 
326             // Otherwise, it's part of a statement.  If it's just whitespace,
327             // we'll just accept it and move on.  Otherwise, parsing is attempted
328             // on the cumulated statement text, and errors are reported.  The
329             // pending input is accepted or rejected based on that parsing.
330 
331             freshen();
332 
333             if (pending.trim().length() == 0) {
334                 accept();
335                 continue;                                     // <<<< LOOP CONTROL <<<<<<<<
336             }
337 
338             final String code = current();
339 
340             if (parse(code, 1)) {
341                 accept();
342                 complete = true;
343             }
344             else if (error == null) {
345                 accept();
346             }
347             else {
348                 report();
349             }
350 
351         }
352 
353         // Get and return the statement.
354         return accepted(complete);
355     }
356 
357     private void inspect() {
358         if (null == lastResult){
359             err.println("nothing to inspect (preceding \"go\" missing?)");
360             return;
361         }
362         // this should read: groovy.inspect.swingui.ObjectBrowser.inspect(lastResult)
363         // but this doesnt compile since ObjectBrowser.groovy is compiled after this class.
364         try {
365             Class browserClass = Class.forName("groovy.inspect.swingui.ObjectBrowser");
366             Method inspectMethod = browserClass.getMethod("inspect", new Class[]{Object.class});
367             inspectMethod.invoke(browserClass, new Object[]{lastResult});
368         } catch (Exception e) {
369             err.println("cannot invoke ObjectBrowser");
370             e.printStackTrace();
371         }
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     /***
388      * Returns the current statement, including pending text.
389      */
390     private String current() {
391         return accepted.toString() + pending + "\n";
392     }
393 
394 
395     /***
396      * Accepts the pending text into the statement.
397      */
398     private void accept() {
399         accepted.append(pending).append("\n");
400         line += 1;
401     }
402 
403 
404     /***
405      * Clears accepted if stale.
406      */
407     private void freshen() {
408         if (stale) {
409             accepted.setLength(0);
410             stale = false;
411         }
412     }
413 
414 
415     //---------------------------------------------------------------------------
416     // SUPPORT ROUTINES
417 
418 
419     /***
420      * Attempts to parse the specified code with the specified tolerance.
421      * Updates the <code>parser</code> and <code>error</code> members
422      * appropriately.  Returns true if the text parsed, false otherwise.
423      * The attempts to identify and suppress errors resulting from the
424      * unfinished source text.
425      */
426     private boolean parse(String code, int tolerance) {
427         boolean parsed = false;
428 
429         parser = null;
430         error = null;
431 
432         // Create the parser and attempt to parse the text as a top-level statement.
433         try {
434             parser = SourceUnit.create("groovysh script", code, tolerance);
435             parser.parse();
436 
437             parsed = true;
438         }
439 
440         // We report errors other than unexpected EOF to the user.
441         catch (CompilationFailedException e) {
442             if (parser.getErrorCollector().getErrorCount() > 1 || !parser.failedWithUnexpectedEOF()) {
443                 error = e;
444             }
445         }
446         catch (Exception e) {
447             error = e;
448         }
449 
450         return parsed;
451     }
452 
453 
454     /***
455      * Reports the last parsing error to the user.
456      */
457 
458     private void report() {
459         err.println("Discarding invalid text:");
460         new ErrorReporter(error, false).write(err);
461     }
462 
463     //-----------------------------------------------------------------------
464     // COMMANDS
465 
466     private static final int COMMAND_ID_EXIT = 0;
467     private static final int COMMAND_ID_HELP = 1;
468     private static final int COMMAND_ID_DISCARD = 2;
469     private static final int COMMAND_ID_DISPLAY = 3;
470     private static final int COMMAND_ID_EXPLAIN = 4;
471     private static final int COMMAND_ID_EXECUTE = 5;
472     private static final int COMMAND_ID_BINDING = 6;
473     private static final int COMMAND_ID_DISCARD_LOADED_CLASSES = 7;
474     private static final int COMMAND_ID_INSPECT = 8;
475 
476     private static final int LAST_COMMAND_ID = 8;
477 
478     private static final String[] COMMANDS = { "exit", "help", "discard", "display", "explain", "execute", "binding", "discardclasses", "inspect" };
479 
480     private static final Map COMMAND_MAPPINGS = new HashMap();
481 
482     static {
483         for (int i = 0; i <= LAST_COMMAND_ID; i++) {
484             COMMAND_MAPPINGS.put(COMMANDS[i], new Integer(i));
485         }
486 
487         // A few synonyms
488 
489         COMMAND_MAPPINGS.put("quit", new Integer(COMMAND_ID_EXIT));
490         COMMAND_MAPPINGS.put("go", new Integer(COMMAND_ID_EXECUTE));
491     }
492 
493     private static final Map COMMAND_HELP = new HashMap();
494 
495     static {
496         COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXIT],    "exit/quit         - terminates processing");
497         COMMAND_HELP.put(COMMANDS[COMMAND_ID_HELP],    "help              - displays this help text");
498         COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISCARD], "discard           - discards the current statement");
499         COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISPLAY], "display           - displays the current statement");
500         COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXPLAIN], "explain           - explains the parsing of the current statement (currently disabled)");
501         COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXECUTE], "execute/go        - temporary command to cause statement execution");
502         COMMAND_HELP.put(COMMANDS[COMMAND_ID_BINDING], "binding           - shows the binding used by this interactive shell");
503         COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISCARD_LOADED_CLASSES],
504                                                        "discardclasses    - discards all former unbound class definitions");
505         COMMAND_HELP.put(COMMANDS[COMMAND_ID_INSPECT], "inspect           - opens ObjectBrowser on expression returned from previous \"go\"");
506     }
507 
508 
509     /***
510      * Displays help text about available commands.
511      */
512     private void displayHelp() {
513         out.println("Available commands (must be entered without extraneous characters):");
514         for (int i = 0; i <= LAST_COMMAND_ID; i++) {
515             out.println((String) COMMAND_HELP.get(COMMANDS[i]));
516         }
517     }
518 
519 
520     /***
521      * Displays the accepted statement.
522      */
523     private void displayStatement() {
524         final String[] lines = accepted.toString().split("\n");
525         for (int i = 0; i < lines.length; i++) {
526             out.println((i + 1) + "> " + lines[i]);
527         }
528     }
529 
530     /***
531      * Displays the current binding used when instanciating the shell.
532      */
533     private void displayBinding() {
534         out.println("Available variables in the current binding");
535         Binding context = shell.getContext();
536         Map variables = context.getVariables();
537         Set set = variables.keySet();
538         if (set.isEmpty()) {
539             out.println("The current binding is empty.");
540         }
541         else {
542             for (Iterator it = set.iterator(); it.hasNext();) {
543                 String key = (String) it.next();
544                 out.println(key + " = " + variables.get(key));
545             }
546         }
547     }
548 
549 
550     /***
551      * Attempts to parse the accepted statement and display the
552      * parse tree for it.
553      */
554     private void explainStatement() {
555         if (parse(accepted(true), 10) || error == null) {
556             out.println("Parse tree:");
557             //out.println(tree);
558         }
559         else {
560             out.println("Statement does not parse");
561         }
562     }
563 
564     private void resetLoadedClasses() {
565         shell.resetLoadedClasses();
566         out.println("all former unbound class definitions are discarded");
567     }
568 }
569