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        /**
090         * Entry point when called directly.
091         */
092        public static void main(String args[]) {
093            try {
094                final InteractiveShell groovy = new InteractiveShell();
095                groovy.run(args);
096            }
097            catch (Exception e) {
098                System.err.println("Caught: " + e);
099                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("Let's 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