001 /* 002 $Id: InteractiveShell.java 3948 2006-08-01 09:50:46Z glaforge $ 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: 3948 $ 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(null,new Binding(), in, out, err); 114 } 115 116 /** 117 * Constructs a new InteractiveShell instance 118 * 119 * @param binding The binding instance 120 * @param in The input stream to use 121 * @param out The output stream to use 122 * @param err The error stream to use 123 */ 124 public InteractiveShell(Binding binding, final InputStream in, final PrintStream out, final PrintStream err) { 125 this(null,binding,in,out,err); 126 } 127 128 /** 129 * Constructs a new InteractiveShell instance 130 * 131 * @param parent The parent ClassLoader 132 * @param binding The binding instance 133 * @param in The input stream to use 134 * @param out The output stream to use 135 * @param err The error stream to use 136 */ 137 public InteractiveShell(ClassLoader parent,Binding binding, final InputStream in, final PrintStream out, final PrintStream err) { 138 this.in = in; 139 this.out = out; 140 this.err = err; 141 prompt = PromptFactory.buildPrompt(in, out, err); 142 prompt.setPrompt("groovy> "); 143 if(parent!= null) { 144 shell = new GroovyShell(parent,binding); 145 } 146 else { 147 shell = new GroovyShell(binding); 148 } 149 Map map = shell.getContext().getVariables(); 150 if (map.get("shell") != null) { 151 map.put("shell", shell); 152 } 153 } 154 155 //--------------------------------------------------------------------------- 156 // COMMAND LINE PROCESSING LOOP 157 158 /** 159 * Reads commands and statements from input stream and processes them. 160 */ 161 public void run(String[] args) throws Exception { 162 final String version = InvokerHelper.getVersion(); 163 164 out.println("Let's get Groovy!"); 165 out.println("================"); 166 out.println("Version: " + version + " JVM: " + System.getProperty("java.vm.version")); 167 out.println("Type 'exit' to terminate the shell"); 168 out.println("Type 'help' for command help"); 169 out.println("Type 'go' to execute the statements"); 170 171 boolean running = true; 172 while (running) { 173 // Read a single top-level statement from the command line, 174 // trapping errors as they happen. We quit on null. 175 final String command = read(); 176 if (command == null) { 177 close(); 178 break; 179 } 180 181 reset(); 182 183 if (command.length() > 0) { 184 // We have a command that parses, so evaluate it. 185 try { 186 lastResult = shell.evaluate(command, "CommandLine.groovy"); 187 out.println("\n===> " + lastResult); 188 } catch (CompilationFailedException e) { 189 err.println(e); 190 } catch (Throwable e) { 191 if (e instanceof InvokerInvocationException) { 192 InvokerInvocationException iie = (InvokerInvocationException) e; 193 e = iie.getCause(); 194 } 195 filterAndPrintStackTrace(e); 196 } 197 } 198 } 199 } 200 201 /** 202 * Filter stacktraces to show only relevant lines of the exception thrown. 203 * 204 * @param e the throwable whose stacktrace needs to be filtered 205 */ 206 private void filterAndPrintStackTrace(Throwable e) { 207 err.println("Caught: " + e); 208 StackTraceElement[] stackTrace = e.getStackTrace(); 209 for (int i = 0; i < stackTrace.length; i++) { 210 StackTraceElement element = stackTrace[i]; 211 String fileName = element.getFileName(); 212 if ((fileName==null || (!fileName.endsWith(".java")) && (!element.getClassName().startsWith("gjdk")))) { 213 err.println("\tat " + element); 214 } 215 } 216 } 217 218 protected void close() { 219 prompt.close(); 220 } 221 222 223 //--------------------------------------------------------------------------- 224 // COMMAND LINE PROCESSING MACHINERY 225 226 227 private StringBuffer accepted = new StringBuffer(); // The statement text accepted to date 228 private String pending = null; // A line of statement text not yet accepted 229 private int line = 1; // The current line number 230 231 private boolean stale = false; // Set to force clear of accepted 232 233 private SourceUnit parser = null; // A SourceUnit used to check the statement 234 private Exception error = null; // Any actual syntax error caught during parsing 235 236 237 /** 238 * Resets the command-line processing machinery after use. 239 */ 240 241 protected void reset() { 242 stale = true; 243 pending = null; 244 line = 1; 245 246 parser = null; 247 error = null; 248 } 249 250 251 /** 252 * Reads a single statement from the command line. Also identifies 253 * and processes command shell commands. Returns the command text 254 * on success, or null when command processing is complete. 255 * <p/> 256 * NOTE: Changed, for now, to read until 'execute' is issued. At 257 * 'execute', the statement must be complete. 258 */ 259 260 protected String read() { 261 reset(); 262 out.println(""); 263 264 boolean complete = false; 265 boolean done = false; 266 267 while (/* !complete && */ !done) { 268 269 // Read a line. If IOException or null, or command "exit", terminate 270 // processing. 271 272 try { 273 pending = prompt.readLine(); 274 } 275 catch (IOException e) { 276 } 277 278 if (pending == null || (COMMAND_MAPPINGS.containsKey(pending) && ((Integer) COMMAND_MAPPINGS.get(pending)).intValue() == COMMAND_ID_EXIT)) { 279 return null; // <<<< FLOW CONTROL <<<<<<<< 280 } 281 282 // First up, try to process the line as a command and proceed accordingly. 283 if (COMMAND_MAPPINGS.containsKey(pending)) { 284 int code = ((Integer) COMMAND_MAPPINGS.get(pending)).intValue(); 285 switch (code) { 286 case COMMAND_ID_HELP: 287 displayHelp(); 288 break; 289 290 case COMMAND_ID_DISCARD: 291 reset(); 292 done = true; 293 break; 294 295 case COMMAND_ID_DISPLAY: 296 displayStatement(); 297 break; 298 299 case COMMAND_ID_EXPLAIN: 300 explainStatement(); 301 break; 302 303 case COMMAND_ID_BINDING: 304 displayBinding(); 305 break; 306 307 case COMMAND_ID_EXECUTE: 308 if (complete) { 309 done = true; 310 } 311 else { 312 err.println("statement not complete"); 313 } 314 break; 315 case COMMAND_ID_DISCARD_LOADED_CLASSES: 316 resetLoadedClasses(); 317 break; 318 case COMMAND_ID_INSPECT: 319 inspect(); 320 break; 321 } 322 323 continue; // <<<< LOOP CONTROL <<<<<<<< 324 } 325 326 // Otherwise, it's part of a statement. If it's just whitespace, 327 // we'll just accept it and move on. Otherwise, parsing is attempted 328 // on the cumulated statement text, and errors are reported. The 329 // pending input is accepted or rejected based on that parsing. 330 331 freshen(); 332 333 if (pending.trim().length() == 0) { 334 accept(); 335 continue; // <<<< LOOP CONTROL <<<<<<<< 336 } 337 338 final String code = current(); 339 340 if (parse(code, 1)) { 341 accept(); 342 complete = true; 343 } 344 else if (error == null) { 345 accept(); 346 } 347 else { 348 report(); 349 } 350 351 } 352 353 // Get and return the statement. 354 return accepted(complete); 355 } 356 357 private void inspect() { 358 if (null == lastResult){ 359 err.println("nothing to inspect (preceding \"go\" missing?)"); 360 return; 361 } 362 // this should read: groovy.inspect.swingui.ObjectBrowser.inspect(lastResult) 363 // but this doesnt compile since ObjectBrowser.groovy is compiled after this class. 364 try { 365 Class browserClass = Class.forName("groovy.inspect.swingui.ObjectBrowser"); 366 Method inspectMethod = browserClass.getMethod("inspect", new Class[]{Object.class}); 367 inspectMethod.invoke(browserClass, new Object[]{lastResult}); 368 } catch (Exception e) { 369 err.println("cannot invoke ObjectBrowser"); 370 e.printStackTrace(); 371 } 372 } 373 374 375 /** 376 * Returns the accepted statement as a string. If not <code>complete</code>, 377 * returns the empty string. 378 */ 379 private String accepted(boolean complete) { 380 if (complete) { 381 return accepted.toString(); 382 } 383 return ""; 384 } 385 386 387 /** 388 * Returns the current statement, including pending text. 389 */ 390 private String current() { 391 return accepted.toString() + pending + "\n"; 392 } 393 394 395 /** 396 * Accepts the pending text into the statement. 397 */ 398 private void accept() { 399 accepted.append(pending).append("\n"); 400 line += 1; 401 } 402 403 404 /** 405 * Clears accepted if stale. 406 */ 407 private void freshen() { 408 if (stale) { 409 accepted.setLength(0); 410 stale = false; 411 } 412 } 413 414 415 //--------------------------------------------------------------------------- 416 // SUPPORT ROUTINES 417 418 419 /** 420 * Attempts to parse the specified code with the specified tolerance. 421 * Updates the <code>parser</code> and <code>error</code> members 422 * appropriately. Returns true if the text parsed, false otherwise. 423 * The attempts to identify and suppress errors resulting from the 424 * unfinished source text. 425 */ 426 private boolean parse(String code, int tolerance) { 427 boolean parsed = false; 428 429 parser = null; 430 error = null; 431 432 // Create the parser and attempt to parse the text as a top-level statement. 433 try { 434 parser = SourceUnit.create("groovysh script", code, tolerance); 435 parser.parse(); 436 437 parsed = true; 438 } 439 440 // We report errors other than unexpected EOF to the user. 441 catch (CompilationFailedException e) { 442 if (parser.getErrorCollector().getErrorCount() > 1 || !parser.failedWithUnexpectedEOF()) { 443 error = e; 444 } 445 } 446 catch (Exception e) { 447 error = e; 448 } 449 450 return parsed; 451 } 452 453 454 /** 455 * Reports the last parsing error to the user. 456 */ 457 458 private void report() { 459 err.println("Discarding invalid text:"); 460 new ErrorReporter(error, false).write(err); 461 } 462 463 //----------------------------------------------------------------------- 464 // COMMANDS 465 466 private static final int COMMAND_ID_EXIT = 0; 467 private static final int COMMAND_ID_HELP = 1; 468 private static final int COMMAND_ID_DISCARD = 2; 469 private static final int COMMAND_ID_DISPLAY = 3; 470 private static final int COMMAND_ID_EXPLAIN = 4; 471 private static final int COMMAND_ID_EXECUTE = 5; 472 private static final int COMMAND_ID_BINDING = 6; 473 private static final int COMMAND_ID_DISCARD_LOADED_CLASSES = 7; 474 private static final int COMMAND_ID_INSPECT = 8; 475 476 private static final int LAST_COMMAND_ID = 8; 477 478 private static final String[] COMMANDS = { "exit", "help", "discard", "display", "explain", "execute", "binding", "discardclasses", "inspect" }; 479 480 private static final Map COMMAND_MAPPINGS = new HashMap(); 481 482 static { 483 for (int i = 0; i <= LAST_COMMAND_ID; i++) { 484 COMMAND_MAPPINGS.put(COMMANDS[i], new Integer(i)); 485 } 486 487 // A few synonyms 488 489 COMMAND_MAPPINGS.put("quit", new Integer(COMMAND_ID_EXIT)); 490 COMMAND_MAPPINGS.put("go", new Integer(COMMAND_ID_EXECUTE)); 491 } 492 493 private static final Map COMMAND_HELP = new HashMap(); 494 495 static { 496 COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXIT], "exit/quit - terminates processing"); 497 COMMAND_HELP.put(COMMANDS[COMMAND_ID_HELP], "help - displays this help text"); 498 COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISCARD], "discard - discards the current statement"); 499 COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISPLAY], "display - displays the current statement"); 500 COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXPLAIN], "explain - explains the parsing of the current statement (currently disabled)"); 501 COMMAND_HELP.put(COMMANDS[COMMAND_ID_EXECUTE], "execute/go - temporary command to cause statement execution"); 502 COMMAND_HELP.put(COMMANDS[COMMAND_ID_BINDING], "binding - shows the binding used by this interactive shell"); 503 COMMAND_HELP.put(COMMANDS[COMMAND_ID_DISCARD_LOADED_CLASSES], 504 "discardclasses - discards all former unbound class definitions"); 505 COMMAND_HELP.put(COMMANDS[COMMAND_ID_INSPECT], "inspect - opens ObjectBrowser on expression returned from previous \"go\""); 506 } 507 508 509 /** 510 * Displays help text about available commands. 511 */ 512 private void displayHelp() { 513 out.println("Available commands (must be entered without extraneous characters):"); 514 for (int i = 0; i <= LAST_COMMAND_ID; i++) { 515 out.println((String) COMMAND_HELP.get(COMMANDS[i])); 516 } 517 } 518 519 520 /** 521 * Displays the accepted statement. 522 */ 523 private void displayStatement() { 524 final String[] lines = accepted.toString().split("\n"); 525 for (int i = 0; i < lines.length; i++) { 526 out.println((i + 1) + "> " + lines[i]); 527 } 528 } 529 530 /** 531 * Displays the current binding used when instanciating the shell. 532 */ 533 private void displayBinding() { 534 out.println("Available variables in the current binding"); 535 Binding context = shell.getContext(); 536 Map variables = context.getVariables(); 537 Set set = variables.keySet(); 538 if (set.isEmpty()) { 539 out.println("The current binding is empty."); 540 } 541 else { 542 for (Iterator it = set.iterator(); it.hasNext();) { 543 String key = (String) it.next(); 544 out.println(key + " = " + variables.get(key)); 545 } 546 } 547 } 548 549 550 /** 551 * Attempts to parse the accepted statement and display the 552 * parse tree for it. 553 */ 554 private void explainStatement() { 555 if (parse(accepted(true), 10) || error == null) { 556 out.println("Parse tree:"); 557 //out.println(tree); 558 } 559 else { 560 out.println("Statement does not parse"); 561 } 562 } 563 564 private void resetLoadedClasses() { 565 shell.resetLoadedClasses(); 566 out.println("all former unbound class definitions are discarded"); 567 } 568 } 569