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