View Javadoc

1   /*
2    $Id: InteractiveShell.java,v 1.28 2005/06/09 21:18:56 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.runtime.InvokerInvocationException;
54  import org.codehaus.groovy.sandbox.ui.Prompt;
55  import org.codehaus.groovy.sandbox.ui.PromptFactory;
56  import org.codehaus.groovy.tools.ErrorReporter;
57  
58  import java.io.IOException;
59  import java.io.InputStream;
60  import java.io.PrintStream;
61  import java.util.HashMap;
62  import java.util.Iterator;
63  import java.util.Map;
64  import java.util.Set;
65  
66  /***
67   * A simple interactive shell for evaluating groovy expressions
68   * on the command line
69   *
70   * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
71   * @author <a href="mailto:cpoirier@dreaming.org"   >Chris Poirier</a>
72   * @author Yuri Schimke
73   * @author Brian McCallistair
74   * @author Guillaume Laforge
75   * @version $Revision: 1.28 $
76   */
77  public class InteractiveShell {
78      private final GroovyShell shell;
79      private final Prompt prompt;
80      private final InputStream in;
81      private final PrintStream out;
82      private final PrintStream err;
83  
84  
85      /***
86       * Entry point when called directly.
87       */
88      public static void main(String args[]) {
89          try {
90              final InteractiveShell groovy = new InteractiveShell();
91              groovy.run(args);
92          }
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) {
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