2 * Copyright 2003-2007 the original author or authors.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
19 import groovy
.lang
.Binding
;
20 import groovy
.lang
.Closure
;
21 import groovy
.lang
.GroovyShell
;
22 import org
.codehaus
.groovy
.tools
.shell
.util
.MessageSource
;
24 import org
.codehaus
.groovy
.control
.CompilationFailedException
;
25 import org
.codehaus
.groovy
.control
.SourceUnit
;
26 import org
.codehaus
.groovy
.runtime
.InvokerHelper
;
27 import org
.codehaus
.groovy
.runtime
.InvokerInvocationException
;
28 import org
.codehaus
.groovy
.runtime
.DefaultGroovyMethods
;
29 import org
.codehaus
.groovy
.tools
.ErrorReporter
;
31 import java
.io
.IOException
;
32 import java
.io
.InputStream
;
33 import java
.io
.PrintStream
;
34 import java
.io
.Writer
;
35 import java
.io
.OutputStreamWriter
;
36 import java
.io
.PrintWriter
;
37 import java
.lang
.reflect
.Method
;
38 import java
.util
.HashMap
;
39 import java
.util
.Iterator
;
43 import org
.apache
.commons
.cli
.CommandLine
;
44 import org
.apache
.commons
.cli
.CommandLineParser
;
45 import org
.apache
.commons
.cli
.OptionBuilder
;
46 import org
.apache
.commons
.cli
.PosixParser
;
47 import org
.apache
.commons
.cli
.Options
;
48 import org
.apache
.commons
.cli
.HelpFormatter
;
50 import jline
.ConsoleReader
;
51 import jline
.SimpleCompletor
;
54 // TODO: See if there is any reason why this class is implemented in Java instead of Groovy, and if there
55 // is none, then port it over ;-)
59 // NOTE: After GShell becomes a little more mature, this shell could be easily implemented as a set of GShell
60 // commands, and would inherit a lot of functionality and could be extended easily to allow groovysh
61 // to become very, very powerful
65 * A simple interactive shell for evaluating groovy expressions on the command line (aka. groovysh).
67 * @author <a href="mailto:james@coredevelopers.net">James Strachan</a>
68 * @author <a href="mailto:cpoirier@dreaming.org" >Chris Poirier</a>
69 * @author Yuri Schimke
70 * @author Brian McCallistair
71 * @author Guillaume Laforge
72 * @author Dierk Koenig, include the inspect command, June 2005
73 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
77 public class InteractiveShell
80 private static final String NEW_LINE
= System
.getProperty("line.separator");
81 private static final MessageSource MESSAGES
= new MessageSource(InteractiveShell
.class);
83 private final GroovyShell shell
;
84 private final InputStream in
; // FIXME: This doesn't really need to be a field, but hold on to it for now
85 private final PrintStream out
;
86 private final PrintStream err
;
87 private final ConsoleReader reader
;
89 private Object lastResult
;
90 private Closure beforeExecution
;
91 private Closure afterExecution
;
94 * Entry point when called directly.
96 public static void main(final String args
[]) {
98 processCommandLineArguments(args
);
100 final InteractiveShell groovy
= new InteractiveShell();
103 catch (Exception e
) {
104 System
.err
.println("FATAL: " + e
);
113 * Process cli args when the shell is invoked via main().
115 * @noinspection AccessStaticViaInstance
117 private static void processCommandLineArguments(final String
[] args
) throws Exception
{
121 // TODO: Let this take a single, optional argument which is a file or URL to run?
124 Options options
= new Options();
126 options
.addOption(OptionBuilder
.withLongOpt("help")
127 .withDescription(MESSAGES
.getMessage("cli.option.help.description"))
130 options
.addOption(OptionBuilder
.withLongOpt("version")
131 .withDescription(MESSAGES
.getMessage("cli.option.version.description"))
135 // TODO: Add more options, maybe even add an option to prime the buffer from a URL or File?
139 // FIXME: This does not currently barf on unsupported options short options, though it does for long ones.
140 // Same problem with commons-cli 1.0 and 1.1
143 CommandLineParser parser
= new PosixParser();
144 CommandLine line
= parser
.parse(options
, args
, true);
145 String
[] lineargs
= line
.getArgs();
147 // Puke if there were arguments, we don't support any right now
148 if (lineargs
.length
!= 0) {
149 System
.err
.println(MESSAGES
.format("cli.info.unexpected_args", new Object
[] { DefaultGroovyMethods
.join(lineargs
, " ") }));
153 PrintWriter writer
= new PrintWriter(System
.out
);
155 if (line
.hasOption('h')) {
156 HelpFormatter formatter
= new HelpFormatter();
160 "groovysh [options]",
166 false); // auto usage
172 if (line
.hasOption('V')) {
173 writer
.println(MESSAGES
.format("cli.info.version", new Object
[] { InvokerHelper
.getVersion() }));
180 * Default constructor, initializes uses new binding and system streams.
182 public InteractiveShell() throws IOException
{
183 this(System
.in
, System
.out
, System
.err
);
187 * Constructs a new InteractiveShell instance
189 * @param in The input stream to use
190 * @param out The output stream to use
191 * @param err The error stream to use
193 public InteractiveShell(final InputStream in
, final PrintStream out
, final PrintStream err
) throws IOException
{
194 this(null, new Binding(), in
, out
, err
);
198 * Constructs a new InteractiveShell instance
200 * @param binding The binding instance
201 * @param in The input stream to use
202 * @param out The output stream to use
203 * @param err The error stream to use
205 public InteractiveShell(final Binding binding
, final InputStream in
, final PrintStream out
, final PrintStream err
) throws IOException
{
206 this(null, binding
, in
, out
, err
);
210 * Constructs a new InteractiveShell instance
212 * @param parent The parent ClassLoader
213 * @param binding The binding instance
214 * @param in The input stream to use
215 * @param out The output stream to use
216 * @param err The error stream to use
218 public InteractiveShell(final ClassLoader parent
, final Binding binding
, final InputStream in
, final PrintStream out
, final PrintStream err
) throws IOException
{
219 assert binding
!= null;
228 // Initialize the JLine console input reader
229 Writer writer
= new OutputStreamWriter(out
);
230 reader
= new ConsoleReader(in
, writer
);
231 reader
.setDefaultPrompt("groovy> ");
233 // Add some completors to fancy things up
234 reader
.addCompletor(new CommandNameCompletor());
236 if (parent
!= null) {
237 shell
= new GroovyShell(parent
, binding
);
240 shell
= new GroovyShell(binding
);
243 // Add some default variables to the shell
244 Map map
= shell
.getContext().getVariables();
247 // FIXME: Um, is this right? Only set the "shell" var in the context if its set already?
250 if (map
.get("shell") != null) {
251 map
.put("shell", shell
);
255 //---------------------------------------------------------------------------
256 // COMMAND LINE PROCESSING LOOP
259 // TODO: Add a general error display handler, and probably add a "ERROR: " prefix to the result for clarity ?
260 // Maybe add one for WARNING's too?
264 * Reads commands and statements from input stream and processes them.
267 // Display the startup banner
268 out
.println(MESSAGES
.format("startup_banner.0", new Object
[] { InvokerHelper
.getVersion(), System
.getProperty("java.version") }));
269 out
.println(MESSAGES
.getMessage("startup_banner.1"));
272 // Read a code block to evaluate; this will deal with basic error handling
273 final String code
= read();
275 // If we got a null, then quit
282 // Evaluate the code block if it was parsed
283 if (code
.length() > 0) {
285 if (beforeExecution
!= null) {
286 beforeExecution
.call();
289 lastResult
= shell
.evaluate(code
);
291 if (afterExecution
!= null) {
292 afterExecution
.call();
295 // Shows the result of the evaluated code
297 out
.println(lastResult
);
299 catch (CompilationFailedException e
) {
302 catch (Throwable e
) {
303 // Unroll invoker exceptions
304 if (e
instanceof InvokerInvocationException
) {
308 filterAndPrintStackTrace(e
);
315 * A closure that is executed before the exection of a given script
317 * @param beforeExecution The closure to execute
319 public void setBeforeExecution(final Closure beforeExecution
) {
320 this.beforeExecution
= beforeExecution
;
324 * A closure that is executed after the execution of the last script. The result of the
325 * execution is passed as the first argument to the closure (the value of 'it')
327 * @param afterExecution The closure to execute
329 public void setAfterExecution(final Closure afterExecution
) {
330 this.afterExecution
= afterExecution
;
334 * Filter stacktraces to show only relevant lines of the exception thrown.
336 * @param cause the throwable whose stacktrace needs to be filtered
338 private void filterAndPrintStackTrace(final Throwable cause
) {
339 assert cause
!= null;
342 // TODO: Use message...
345 err
.print("ERROR: ");
348 cause
.printStackTrace(err
);
351 // FIXME: What is the point of this? AFAICT, this just produces crappy/corrupt traces and is completely useless
354 // StackTraceElement[] stackTrace = e.getStackTrace();
356 // for (int i = 0; i < stackTrace.length; i++) {
357 // StackTraceElement element = stackTrace[i];
358 // String fileName = element.getFileName();
360 // if ((fileName==null || (!fileName.endsWith(".java")) && (!element.getClassName().startsWith("gjdk")))) {
361 // err.print("\tat ");
362 // err.println(element);
367 //---------------------------------------------------------------------------
368 // COMMAND LINE PROCESSING MACHINERY
370 /** The statement text accepted to date */
371 private StringBuffer accepted
= new StringBuffer();
373 /** A line of statement text not yet accepted */
374 private String pending
;
377 // FIXME: Doesn't look like 'line' is really used/needed anywhere... could drop it, or perhaps
378 // could use it to update the prompt er something to show the buffer size?
381 /** The current line number */
384 /** Set to force clear of accepted */
385 private boolean stale
= false;
387 /** A SourceUnit used to check the statement */
388 private SourceUnit parser
;
390 /** Any actual syntax error caught during parsing */
391 private Exception error
;
394 * Resets the command-line processing machinery after use.
396 protected void reset() {
405 // FIXME: This Javadoc is not correct... read() will return the full code block read until "go"
409 * Reads a single statement from the command line. Also identifies
410 * and processes command shell commands. Returns the command text
411 * on success, or null when command processing is complete.
413 * NOTE: Changed, for now, to read until 'execute' is issued. At
414 * 'execute', the statement must be complete.
416 protected String
read() {
419 boolean complete
= false;
420 boolean done
= false;
422 while (/* !complete && */ !done
) {
423 // Read a line. If IOException or null, or command "exit", terminate processing.
425 pending
= reader
.readLine();
427 catch (IOException e
) {
429 // FIXME: Shouldn't really eat this exception, may be something we need to see... ?
433 // If result is null then we are shutting down
434 if (pending
== null) {
438 // First up, try to process the line as a command and proceed accordingly
439 // Trim what we have for use in command bits, so things like "help " actually show the help screen
440 String command
= pending
.trim();
442 if (COMMAND_MAPPINGS
.containsKey(command
)) {
443 int code
= ((Integer
)COMMAND_MAPPINGS
.get(command
)).intValue();
446 case COMMAND_ID_EXIT
:
449 case COMMAND_ID_HELP
:
453 case COMMAND_ID_DISCARD
:
458 case COMMAND_ID_DISPLAY
:
462 case COMMAND_ID_EXPLAIN
:
466 case COMMAND_ID_BINDING
:
470 case COMMAND_ID_EXECUTE
:
475 err
.println(MESSAGES
.getMessage("command.execute.not_complete"));
479 case COMMAND_ID_DISCARD_LOADED_CLASSES
:
480 resetLoadedClasses();
483 case COMMAND_ID_INSPECT
:
488 throw new Error("BUG: Unknown command for code: " + code
);
491 // Finished processing command bits, continue reading, don't need to process code
495 // Otherwise, it's part of a statement. If it's just whitespace,
496 // we'll just accept it and move on. Otherwise, parsing is attempted
497 // on the cumulated statement text, and errors are reported. The
498 // pending input is accepted or rejected based on that parsing.
502 if (pending
.trim().length() == 0) {
507 // Try to parse the current code buffer
508 final String code
= current();
515 else if (error
== null) {
520 // Parse failed, spit out something to the user
525 // Get and return the statement.
526 return accepted(complete
);
529 private void inspect() {
530 if (lastResult
== null){
531 err
.println(MESSAGES
.getMessage("command.inspect.no_result"));
536 // FIXME: Update this once we have joint compile happy in the core build?
538 // this should read: groovy.inspect.swingui.ObjectBrowser.inspect(lastResult)
539 // but this doesnt compile since ObjectBrowser.groovy is compiled after this class.
543 // FIXME: When launching this, if the user tries to "exit" and the window is still opened, the shell will
544 // hang... not really nice user experence IMO. Should try to fix this if we can.
548 Class type
= Class
.forName("groovy.inspect.swingui.ObjectBrowser");
549 Method method
= type
.getMethod("inspect", new Class
[]{ Object
.class });
550 method
.invoke(type
, new Object
[]{ lastResult
});
552 catch (Exception e
) {
553 err
.println("Cannot invoke ObjectBrowser");
559 * Returns the accepted statement as a string. If not complete, returns empty string.
561 private String
accepted(final boolean complete
) {
563 return accepted
.toString();
569 * Returns the current statement, including pending text.
571 private String
current() {
572 return accepted
.toString() + pending
+ NEW_LINE
;
576 * Accepts the pending text into the statement.
578 private void accept() {
579 accepted
.append(pending
).append(NEW_LINE
);
584 * Clears accepted if stale.
586 private void freshen() {
588 accepted
.setLength(0);
593 //---------------------------------------------------------------------------
597 * Attempts to parse the specified code with the specified tolerance.
598 * Updates the <code>parser</code> and <code>error</code> members
599 * appropriately. Returns true if the text parsed, false otherwise.
600 * The attempts to identify and suppress errors resulting from the
601 * unfinished source text.
603 private boolean parse(final String code
, final int tolerance
) {
606 boolean parsed
= false;
610 // Create the parser and attempt to parse the text as a top-level statement.
612 parser
= SourceUnit
.create("groovysh-script", code
, tolerance
);
617 // We report errors other than unexpected EOF to the user.
618 catch (CompilationFailedException e
) {
619 if (parser
.getErrorCollector().getErrorCount() > 1 || !parser
.failedWithUnexpectedEOF()) {
623 catch (Exception e
) {
630 private boolean parse(final String code
) {
631 return parse(code
, 1);
635 * Reports the last parsing error to the user.
637 private void report() {
638 err
.println("Discarding invalid text:"); // TODO: i18n
639 new ErrorReporter(error
, false).write(err
);
642 //-----------------------------------------------------------------------
646 // TODO: Add a simple command to read in a File/URL into the buffer for execution, but need better command
647 // support first (aka GShell) so we can allow commands to take args, etc.
650 private static final int COMMAND_ID_EXIT
= 0;
651 private static final int COMMAND_ID_HELP
= 1;
652 private static final int COMMAND_ID_DISCARD
= 2;
653 private static final int COMMAND_ID_DISPLAY
= 3;
654 private static final int COMMAND_ID_EXPLAIN
= 4;
655 private static final int COMMAND_ID_EXECUTE
= 5;
656 private static final int COMMAND_ID_BINDING
= 6;
657 private static final int COMMAND_ID_DISCARD_LOADED_CLASSES
= 7;
658 private static final int COMMAND_ID_INSPECT
= 8;
659 private static final int LAST_COMMAND_ID
= 8;
661 private static final String
[] COMMANDS
= {
673 private static final Map COMMAND_MAPPINGS
= new HashMap();
676 for (int i
= 0; i
<= LAST_COMMAND_ID
; i
++) {
677 COMMAND_MAPPINGS
.put(COMMANDS
[i
], Integer
.valueOf(i
));
681 COMMAND_MAPPINGS
.put("quit", Integer
.valueOf(COMMAND_ID_EXIT
));
682 COMMAND_MAPPINGS
.put("go", Integer
.valueOf(COMMAND_ID_EXECUTE
));
685 private static final Map COMMAND_HELP
= new HashMap();
688 COMMAND_HELP
.put(COMMANDS
[COMMAND_ID_EXIT
], "exit/quit - " + MESSAGES
.getMessage("command.exit.descripion"));
689 COMMAND_HELP
.put(COMMANDS
[COMMAND_ID_HELP
], "help - " + MESSAGES
.getMessage("command.help.descripion"));
690 COMMAND_HELP
.put(COMMANDS
[COMMAND_ID_DISCARD
], "discard - " + MESSAGES
.getMessage("command.discard.descripion"));
691 COMMAND_HELP
.put(COMMANDS
[COMMAND_ID_DISPLAY
], "display - " + MESSAGES
.getMessage("command.display.descripion"));
694 // FIXME: If this is disabled, then er comment it out, so it doesn't confuse the user
697 COMMAND_HELP
.put(COMMANDS
[COMMAND_ID_EXPLAIN
], "explain - " + MESSAGES
.getMessage("command.explain.descripion"));
698 COMMAND_HELP
.put(COMMANDS
[COMMAND_ID_EXECUTE
], "execute/go - " + MESSAGES
.getMessage("command.execute.descripion"));
699 COMMAND_HELP
.put(COMMANDS
[COMMAND_ID_BINDING
], "binding - " + MESSAGES
.getMessage("command.binding.descripion"));
700 COMMAND_HELP
.put(COMMANDS
[COMMAND_ID_DISCARD_LOADED_CLASSES
],
701 "discardclasses - " + MESSAGES
.getMessage("command.discardclasses.descripion"));
702 COMMAND_HELP
.put(COMMANDS
[COMMAND_ID_INSPECT
], "inspect - " + MESSAGES
.getMessage("command.inspect.descripion"));
706 * Displays help text about available commands.
708 private void displayHelp() {
709 out
.println(MESSAGES
.getMessage("command.help.available_commands"));
711 for (int i
= 0; i
<= LAST_COMMAND_ID
; i
++) {
713 out
.println(COMMAND_HELP
.get(COMMANDS
[i
]));
718 * Displays the accepted statement.
720 private void displayStatement() {
721 final String
[] lines
= accepted
.toString().split(NEW_LINE
);
723 if (lines
.length
== 1 && lines
[0].trim().equals("")) {
724 out
.println(MESSAGES
.getMessage("command.display.buffer_empty"));
727 // Eh, try to pick a decent pad size... but don't try to hard
729 if (lines
.length
>= 10) padsize
++;
730 if (lines
.length
>= 100) padsize
++;
731 if (lines
.length
>= 1000) padsize
++;
733 // Dump the current buffer with a line number prefix
734 for (int i
= 0; i
< lines
.length
; i
++) {
735 // Normalize the field size of the line number
736 String lineno
= DefaultGroovyMethods
.padLeft(String
.valueOf(i
+ 1), Integer
.valueOf(padsize
), " ");
740 out
.println(lines
[i
]);
746 * Displays the current binding used when instanciating the shell.
748 private void displayBinding() {
749 Binding context
= shell
.getContext();
750 Map variables
= context
.getVariables();
751 Set set
= variables
.keySet();
754 out
.println(MESSAGES
.getMessage("command.binding.binding_empty"));
757 out
.println(MESSAGES
.getMessage("command.binding.available_variables"));
759 Iterator iter
= set
.iterator();
760 while (iter
.hasNext()) {
761 Object key
= iter
.next();
766 out
.println(variables
.get(key
));
772 * Attempts to parse the accepted statement and display the parse tree for it.
774 private void explainStatement() {
775 if (parse(accepted(true), 10) || error
== null) {
776 out
.println(MESSAGES
.getMessage("command.explain.tree_header"));
780 out
.println(MESSAGES
.getMessage("command.explain.unparsable"));
784 private void resetLoadedClasses() {
785 shell
.resetLoadedClasses();
787 out
.println(MESSAGES
.getMessage("command.discardclasses.classdefs_discarded"));
791 // Custom JLine Completors to fancy up the user experence more.
794 private class CommandNameCompletor
795 extends SimpleCompletor
797 public CommandNameCompletor() {
798 super(new String
[0]);
800 // Add each command name/alias as a candidate
801 Iterator iter
= COMMAND_MAPPINGS
.keySet().iterator();
803 while (iter
.hasNext()) {
804 addCandidateString((String
)iter
.next());
810 // TODO: Add local variable completion?
814 // TODO: Add shell method complention?
818 private void findShellMethods(String complete) {
819 List methods = shell.getMetaClass().getMetaMethods();
820 for (Iterator i = methods.iterator(); i.hasNext();) {
821 MetaMethod method = (MetaMethod) i.next();
822 if (method.getName().startsWith(complete)) {
823 if (method.getParameterTypes().length > 0) {
824 completions.add(method.getName() + "(");
827 completions.add(method.getName() + "()");
833 private void findLocalVariables(String complete) {
834 Set names = shell.getContext().getVariables().keySet();
836 for (Iterator i = names.iterator(); i.hasNext();) {
837 String name = (String) i.next();
838 if (name.startsWith(complete)) {
839 completions.add(name);