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