View Javadoc

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