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