View Javadoc

    1   /*
    2    $Id: InteractiveShell.java 3948 2006-08-01 09:50:46Z glaforge $
    3
    4    Copyright 2003 (C) James Strachan and Bob Mcwhirter. All Rights Reserved.
    5
    6    Redistribution and use of this software and associated documentation
    7    ("Software"), with or without modification, are permitted provided
    8    that the following conditions are met:
    9
    10   1. Redistributions of source code must retain copyright
    11      statements and notices.  Redistributions must also contain a
    12      copy of this document.
    13
    14   2. Redistributions in binary form must reproduce the
    15      above copyright notice, this list of conditions and the
    16      following disclaimer in the documentation and/or other
    17      materials provided with the distribution.
    18
    19   3. The name "groovy" must not be used to endorse or promote
    20      products derived from this Software without prior written
    21      permission of The Codehaus.  For written permission,
    22      please contact info@codehaus.org.
    23
    24   4. Products derived from this Software may not be called "groovy"
    25      nor may "groovy" appear in their names without prior written
    26      permission of The Codehaus. "groovy" is a registered
    27      trademark of The Codehaus.
    28
    29   5. Due credit should be given to The Codehaus -
    30      http://groovy.codehaus.org/
    31
    32   THIS SOFTWARE IS PROVIDED BY THE CODEHAUS AND CONTRIBUTORS
    33   ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
    34   NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
    35   FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
    36   THE CODEHAUS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
    37   INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    38   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    39   SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
    40   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
    41   STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    42   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
    43   OF THE POSSIBILITY OF SUCH DAMAGE.
    44
    45   */
    46  package groovy.ui;
    47
    48  import groovy.lang.Binding;
    49  import groovy.lang.GroovyShell;
    50
    51  import java.io.IOException;
    52  import java.io.InputStream;
    53  import java.io.PrintStream;
    54  import java.lang.reflect.Method;
    55  import java.util.HashMap;
    56  import java.util.Iterator;
    57  import java.util.Map;
    58  import java.util.Set;
    59
    60  import org.codehaus.groovy.control.CompilationFailedException;
    61  import org.codehaus.groovy.control.SourceUnit;
    62  import org.codehaus.groovy.runtime.InvokerHelper;
    63  import org.codehaus.groovy.runtime.InvokerInvocationException;
    64  import org.codehaus.groovy.sandbox.ui.Prompt;
    65  import org.codehaus.groovy.sandbox.ui.PromptFactory;
    66  import org.codehaus.groovy.tools.ErrorReporter;
    67
    68  /***
    69   * A simple interactive shell for evaluating groovy expressions
    70   * on the command line
    71   *
    72   * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
    73   * @author <a href="mailto:cpoirier@dreaming.org"   >Chris Poirier</a>
    74   * @author Yuri Schimke
    75   * @author Brian McCallistair
    76   * @author Guillaume Laforge
    77   * @author Dierk Koenig, include the inspect command, June 2005
    78   * @version $Revision: 3948 $
    79   */
    80  public class InteractiveShell {
    81      private final GroovyShell shell;
    82      private final Prompt prompt;
    83      private final InputStream in;
    84      private final PrintStream out;
    85      private final PrintStream err;
    86      private Object lastResult;
    87
    88
    89      /***
    90       * Entry point when called directly.
    91       */
    92      public static void main(String args[]) {
    93          try {
    94              final InteractiveShell groovy = new InteractiveShell();
    95              groovy.run(args);
    96          }
    97          catch (Exception e) {
    98              System.err.println("Caught: " + e);
    99              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