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