View Javadoc

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