001    /*
002     $Id: InteractiveShell.java,v 1.30 2005/07/13 19:28:07 cstein Exp $
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: 1.30 $
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(new Binding(), in, out, err);
114        }
115    
116        public InteractiveShell(Binding binding, final InputStream in, final PrintStream out, final PrintStream err) {
117            this.in = in;
118            this.out = out;
119            this.err = err;
120            prompt = PromptFactory.buildPrompt(in, out, err);
121            prompt.setPrompt("groovy> ");
122            shell = new GroovyShell(binding);
123            Map map = shell.getContext().getVariables();
124            if (map.get("shell") != null) {
125                map.put("shell", shell);
126            }
127        }
128    
129        //---------------------------------------------------------------------------
130        // COMMAND LINE PROCESSING LOOP
131    
132        /**
133         * Reads commands and statements from input stream and processes them.
134         */
135        public void run(String[] args) throws Exception {
136            final String version = InvokerHelper.getVersion();
137    
138            out.println("Lets get Groovy!");
139            out.println("================");
140            out.println("Version: " + version + " JVM: " + System.getProperty("java.vm.version"));
141            out.println("Type 'exit' to terminate the shell");
142            out.println("Type 'help' for command help");
143            out.println("Type 'go' to execute the statements");
144    
145            boolean running = true;
146            while (running) {
147                // Read a single top-level statement from the command line,
148                // trapping errors as they happen.  We quit on null.
149                final String command = read();
150                if (command == null) {
151                    close();
152                    break;
153                }
154    
155                reset();
156    
157                if (command.length() > 0) {
158                    // We have a command that parses, so evaluate it.
159                    try {
160                        lastResult = shell.evaluate(command, "CommandLine.groovy");
161                    } catch (CompilationFailedException e) {
162                        err.println(e);
163                    } catch (Throwable e) {
164                        if (e instanceof InvokerInvocationException) {
165                            InvokerInvocationException iie = (InvokerInvocationException) e;
166                            e = iie.getCause();
167                        }
168                        err.println("Caught: " + e);
169                        StackTraceElement[] stackTrace = e.getStackTrace();
170                        for (int i = 0; i < stackTrace.length; i++) {
171                            StackTraceElement element = stackTrace[i];
172                            String fileName = element.getFileName();
173                            if (fileName==null || (!fileName.endsWith(".java"))) {
174                                err.println("\tat " + element);
175                            }
176                        }
177                    }
178                }
179            }
180        }
181    
182    
183        protected void close() {
184            prompt.close();
185        }
186    
187    
188        //---------------------------------------------------------------------------
189        // COMMAND LINE PROCESSING MACHINERY
190    
191    
192        private StringBuffer accepted = new StringBuffer(); // The statement text accepted to date
193        private String pending = null;                      // A line of statement text not yet accepted
194        private int line = 1;                               // The current line number
195    
196        private boolean stale = false;                      // Set to force clear of accepted
197    
198        private SourceUnit parser = null;                   // A SourceUnit used to check the statement
199        private Exception error = null;                     // Any actual syntax error caught during parsing
200    
201    
202        /**
203         * Resets the command-line processing machinery after use.
204         */
205    
206        protected void reset() {
207            stale = true;
208            pending = null;
209            line = 1;
210    
211            parser = null;
212            error = null;
213        }
214    
215    
216        /**
217         * Reads a single statement from the command line.  Also identifies
218         * and processes command shell commands.  Returns the command text
219         * on success, or null when command processing is complete.
220         * <p/>
221         * NOTE: Changed, for now, to read until 'execute' is issued.  At
222         * 'execute', the statement must be complete.
223         */
224    
225        protected String read() {
226            reset();
227            out.println("");
228    
229            boolean complete = false;
230            boolean done = false;
231    
232            while (/* !complete && */ !done) {
233    
234                // Read a line.  If IOException or null, or command "exit", terminate
235                // processing.
236    
237                try {
238                    pending = prompt.readLine();
239                }
240                catch (IOException e) {
241                }
242    
243                if (pending == null || (COMMAND_MAPPINGS.containsKey(pending) && ((Integer) COMMAND_MAPPINGS.get(pending)).intValue() == COMMAND_ID_EXIT)) {
244                    return null;                                  // <<<< FLOW CONTROL <<<<<<<<
245                }
246    
247                // First up, try to process the line as a command and proceed accordingly.
248                if (COMMAND_MAPPINGS.containsKey(pending)) {
249                    int code = ((Integer) COMMAND_MAPPINGS.get(pending)).intValue();
250                    switch (code) {
251                        case COMMAND_ID_HELP:
252                            displayHelp();
253                            break;
254    
255                        case COMMAND_ID_DISCARD:
256                            reset();
257                            done = true;
258                            break;
259    
260                        case COMMAND_ID_DISPLAY:
261                            displayStatement();
262                            break;
263    
264                        case COMMAND_ID_EXPLAIN:
265                            explainStatement();
266                            break;
267    
268                        case COMMAND_ID_BINDING:
269                            displayBinding();
270                            break;
271    
272                        case COMMAND_ID_EXECUTE:
273                            if (complete) {
274                                done = true;
275                            }
276                            else {
277                                err.println("statement not complete");
278                            }
279                            break;
280                        case COMMAND_ID_DISCARD_LOADED_CLASSES:
281                            resetLoadedClasses();
282                            break;
283                        case COMMAND_ID_INSPECT:
284                            inspect();
285                            break;
286                    }
287    
288                    continue;                                     // <<<< LOOP CONTROL <<<<<<<<
289                }
290    
291                // Otherwise, it's part of a statement.  If it's just whitespace,
292                // we'll just accept it and move on.  Otherwise, parsing is attempted
293                // on the cumulated statement text, and errors are reported.  The
294                // pending input is accepted or rejected based on that parsing.
295    
296                freshen();
297    
298                if (pending.trim().equals("")) {
299                    accept();
300                    continue;                                     // <<<< LOOP CONTROL <<<<<<<<
301                }
302    
303                final String code = current();
304    
305                if (parse(code, 1)) {
306                    accept();
307                    complete = true;
308                }
309                else if (error == null) {
310                    accept();
311                }
312                else {
313                    report();
314                }
315    
316            }
317    
318            // Get and return the statement.
319            return accepted(complete);
320        }
321    
322        private void inspect() {
323            if (null == lastResult){
324                err.println("nothing to inspect (preceding \"go\" missing?)");
325                return;
326            }
327            // this should read: groovy.inspect.swingui.ObjectBrowser.inspect(lastResult)
328            // but this doesnt compile since ObjectBrowser.groovy is compiled after this class.
329            try {
330                Class browserClass = Class.forName("groovy.inspect.swingui.ObjectBrowser");
331                Method inspectMethod = browserClass.getMethod("inspect", new Class[]{Object.class});
332                inspectMethod.invoke(browserClass, new Object[]{lastResult});
333            } catch (Exception e) {
334                err.println("cannot invoke ObjectBrowser");
335                e.printStackTrace();
336            }
337        }
338    
339    
340        /**
341         * Returns the accepted statement as a string.  If not <code>complete</code>,
342         * returns the empty string.
343         */
344        private String accepted(boolean complete) {
345            if (complete) {
346                return accepted.toString();
347            }
348            return "";
349        }
350    
351    
352        /**
353         * Returns the current statement, including pending text.
354         */
355        private String current() {
356            return accepted.toString() + pending + "\n";
357        }
358    
359    
360        /**
361         * Accepts the pending text into the statement.
362         */
363        private void accept() {
364            accepted.append(pending).append("\n");
365            line += 1;
366        }
367    
368    
369        /**
370         * Clears accepted if stale.
371         */
372        private void freshen() {
373            if (stale) {
374                accepted.setLength(0);
375                stale = false;
376            }
377        }
378    
379    
380        //---------------------------------------------------------------------------
381        // SUPPORT ROUTINES
382    
383    
384        /**
385         * Attempts to parse the specified code with the specified tolerance.
386         * Updates the <code>parser</code> and <code>error</code> members
387         * appropriately.  Returns true if the text parsed, false otherwise.
388         * The attempts to identify and suppress errors resulting from the
389         * unfinished source text.
390         */
391        private boolean parse(String code, int tolerance) {
392            boolean parsed = false;
393    
394            parser = null;
395            error = null;
396    
397            // Create the parser and attempt to parse the text as a top-level statement.
398            try {
399                parser = SourceUnit.create("groovysh script", code, tolerance);
400                parser.parse();
401    
402                /* see note on read():
403                 * tree = parser.topLevelStatement();
404                 *
405                 * if( stream.atEnd() ) {
406                 *     parsed = true;
407                 * }
408                 */
409                parsed = true;
410            }
411    
412            // We report errors other than unexpected EOF to the user.
413            catch (CompilationFailedException e) {
414                if (parser.getErrorCollector().getErrorCount() > 1 || !parser.failedWithUnexpectedEOF()) {
415                    error = e;
416                }
417            }
418            catch (Exception e) {
419                error = e;
420            }
421    
422            return parsed;
423        }
424    
425    
426        /**
427         * Reports the last parsing error to the user.
428         */
429    
430        private void report() {
431            err.println("Discarding invalid text:");
432            new ErrorReporter(error, false).write(err);
433        }
434    
435        //-----------------------------------------------------------------------
436        // COMMANDS
437    
438        private static final int COMMAND_ID_EXIT = 0;
439        private static final int COMMAND_ID_HELP = 1;
440        private static final int COMMAND_ID_DISCARD = 2;
441        private static final int COMMAND_ID_DISPLAY = 3;
442        private static final int COMMAND_ID_EXPLAIN = 4;
443        private static final int COMMAND_ID_EXECUTE = 5;
444        private static final int COMMAND_ID_BINDING = 6;
445        private static final int COMMAND_ID_DISCARD_LOADED_CLASSES = 7;
446        private static final int COMMAND_ID_INSPECT = 8;
447    
448        private static final int LAST_COMMAND_ID = 8;
449    
450        private static final String[] COMMANDS = {"exit", "help", "discard", "display", "explain", "execute", "binding", "discardclasses", "inspect"};
451    
452        private static final Map COMMAND_MAPPINGS = new HashMap();
453    
454        static {
455            for (int i = 0; i <= LAST_COMMAND_ID; i++) {
456                COMMAND_MAPPINGS.put(COMMANDS[i], new Integer(i));
457            }
458    
459            // A few synonyms
460    
461            COMMAND_MAPPINGS.put("quit", new Integer(COMMAND_ID_EXIT));
462            COMMAND_MAPPINGS.put("go", new Integer(COMMAND_ID_EXECUTE));
463        }
464    
465        private static final Map COMMAND_HELP = new HashMap();
466    
467        static {
468            COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXIT], "exit/quit        - terminates processing");
469            COMMAND_HELP.put(COMMANDS[COMMAND_ID_HELP], "help             - displays this help text");
470            COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISCARD], "discard           - discards the current statement");
471            COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISPLAY], "display           - displays the current statement");
472            COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXPLAIN], "explain           - explains the parsing of the current statement (currently disabled)");
473            COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXECUTE], "execute/go        - temporary command to cause statement execution");
474            COMMAND_HELP.put(COMMANDS[COMMAND_ID_BINDING], "binding           - shows the binding used by this interactive shell");
475            COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISCARD_LOADED_CLASSES], "discardclasses    - discards all former unbound class definitions");
476            COMMAND_HELP.put(COMMANDS[COMMAND_ID_INSPECT], "inspect           - opens ObjectBrowser on expression returned from previous \"go\"");
477        }
478    
479    
480        /**
481         * Displays help text about available commands.
482         */
483        private void displayHelp() {
484            out.println("Available commands (must be entered without extraneous characters):");
485            for (int i = 0; i <= LAST_COMMAND_ID; i++) {
486                out.println((String) COMMAND_HELP.get(COMMANDS[i]));
487            }
488        }
489    
490    
491        /**
492         * Displays the accepted statement.
493         */
494        private void displayStatement() {
495            final String[] lines = accepted.toString().split("\n");
496            for (int i = 0; i < lines.length; i++) {
497                out.println((i + 1) + "> " + lines[i]);
498            }
499        }
500    
501        /**
502         * Displays the current binding used when instanciating the shell.
503         */
504        private void displayBinding() {
505            out.println("Available variables in the current binding");
506            Binding context = shell.getContext();
507            Map variables = context.getVariables();
508            Set set = variables.keySet();
509            if (set.isEmpty()) {
510                out.println("The current binding is empty.");
511            }
512            else {
513                for (Iterator it = set.iterator(); it.hasNext();) {
514                    String key = (String) it.next();
515                    out.println(key + " = " + variables.get(key));
516                }
517            }
518        }
519    
520    
521        /**
522         * Attempts to parse the accepted statement and display the
523         * parse tree for it.
524         */
525        private void explainStatement() {
526            if (parse(accepted(true), 10) || error == null) {
527                out.println("Parse tree:");
528                //out.println(tree);
529            }
530            else {
531                out.println("Statement does not parse");
532            }
533        }
534    
535        private void resetLoadedClasses() {
536            shell.resetLoadedClasses();
537            out.println("all former unbound class definitions are discarded");
538        }
539    }
540