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