| /* |
| * Copyright 2003-2007 the original author or authors. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.codehaus.groovy.tools.shell |
| |
| import java.lang.reflect.Method |
| |
| import java.util.prefs.Preferences |
| |
| import jline.Terminal |
| import jline.History |
| |
| import org.codehaus.groovy.runtime.InvokerHelper |
| import org.codehaus.groovy.runtime.InvokerInvocationException |
| import org.codehaus.groovy.runtime.MethodClosure |
| |
| import org.codehaus.groovy.control.SourceUnit |
| import org.codehaus.groovy.control.CompilationFailedException |
| |
| import org.codehaus.groovy.tools.shell.util.MessageSource |
| import org.codehaus.groovy.tools.shell.util.ANSI |
| import org.codehaus.groovy.tools.shell.util.HelpFormatter |
| import org.codehaus.groovy.tools.shell.util.Logger |
| import org.codehaus.groovy.tools.shell.util.XmlCommandRegistrar |
| |
| /** |
| * An interactive shell for evaluating Groovy code from the command-line (aka. groovysh). |
| * |
| * @version $Id$ |
| * @author <a href="mailto:jason@planet57.com">Jason Dillon</a> |
| */ |
| class Groovysh |
| extends Shell |
| { |
| private static final String NEWLINE = System.properties['line.separator'] |
| |
| private static final MessageSource messages = new MessageSource(Groovysh.class) |
| |
| private final Preferences prefs = Preferences.userNodeForPackage(Groovysh.class) |
| |
| private final GroovyShell interp |
| |
| private final BufferManager buffers = new BufferManager() |
| |
| private final List imports = [] |
| |
| private InteractiveShellRunner runner |
| |
| private History history |
| |
| Groovysh(final ClassLoader classLoader, final Binding binding, final IO io) { |
| super(io) |
| |
| assert classLoader |
| assert binding |
| |
| interp = new GroovyShell(classLoader, binding) |
| |
| def registrar = new XmlCommandRegistrar(this, classLoader) |
| registrar.register(getClass().getResource('commands.xml')) |
| } |
| |
| Groovysh(final Binding binding, final IO io) { |
| this(Thread.currentThread().contextClassLoader, binding, io) |
| } |
| |
| Groovysh(final IO io) { |
| this(new Binding(), io) |
| } |
| |
| Groovysh() { |
| this(new IO()) |
| } |
| |
| private void setLastResult(final Object obj) { |
| boolean showLastResult = io.verbose || prefs.getBoolean('show-last-result', false) |
| |
| if (showLastResult) { |
| io.out.println("@|bold ===>| $obj") |
| } |
| |
| interp.context['_'] = obj |
| } |
| |
| File getUserStateDirectory() { |
| def userHome = new File(System.properties['user.home']) |
| def dir = new File(userHome, '.groovy') |
| return dir.canonicalFile |
| } |
| |
| private Object getLastResult() { |
| return interp.context['_'] |
| } |
| |
| private ANSI.Renderer prompt = new ANSI.Renderer() |
| |
| private String renderPrompt() { |
| def lineNum = formatLineNumber(buffers.current().size()) |
| |
| return prompt.render("@|bold groovy:|(${buffers.selected})@|bold :|${lineNum}@|bold >| ") |
| } |
| |
| protected Object executeCommand(final String line) { |
| return super.execute(line) |
| } |
| |
| /** |
| * Execute a single line, where the line may be a command or Groovy code (complete or incomplete). |
| */ |
| Object execute(final String line) { |
| assert line != null |
| |
| // Ignore empty lines |
| if (line.trim().size() == 0) { |
| return null |
| } |
| |
| def result |
| |
| // First try normal command execution |
| if (isExecutable(line)) { |
| result = executeCommand(line) |
| |
| // For commands, only set the last result when its non-null/true |
| if (result) { |
| lastResult = result |
| } |
| |
| return result |
| } |
| |
| // Otherwise treat the line as Groovy |
| def current = [] |
| current += buffers.current() |
| |
| // Append the line to the current buffer |
| current << line |
| |
| // Attempt to parse the current buffer |
| def status = parse(current, 1) |
| |
| switch (status.code) { |
| case ParseCode.COMPLETE: |
| // Evaluate the current buffer |
| result = evaluate(current) |
| buffers.clearSelected() |
| break |
| |
| case ParseCode.INCOMPLETE: |
| // Save the current buffer so user can build up complex muli-line code blocks |
| buffers.updateSelected(current) |
| break |
| |
| case ParseCode.ERROR: |
| // Show a simple compilation error, otherwise dump the full details |
| if (status.cause instanceof CompilationFailedException) { |
| io.err.println(messages.format('info.error', status.cause.message)) |
| } |
| else { |
| displayError(status.cause) |
| } |
| break |
| |
| default: |
| // Should never happen |
| throw new Error("Invalid parse status: $status.code") |
| } |
| |
| return (lastResult = result) |
| } |
| |
| /** |
| * Attempt to parse the given buffer. |
| */ |
| private ParseStatus parse(final List buffer, final int tolerance) { |
| assert buffer |
| |
| String source = (imports + buffer).join(NEWLINE) |
| |
| log.debug("Parsing: $source") |
| |
| SourceUnit parser |
| Throwable error |
| |
| try { |
| parser = SourceUnit.create('groovysh_parse', source, tolerance) |
| parser.parse() |
| |
| log.debug('Parse complete') |
| |
| return new ParseStatus(ParseCode.COMPLETE) |
| } |
| catch (CompilationFailedException e) { |
| // |
| // FIXME: Seems like failedWithUnexpectedEOF() is not always set as expected, as in: |
| // |
| // class a { <--- is true here |
| // def b() { <--- is false here :-( |
| // |
| |
| if (parser.errorCollector.errorCount > 1 || !parser.failedWithUnexpectedEOF()) { |
| // |
| // HACK: Super insane hack... if we detect a syntax error, but the last line of the |
| // buffer ends with a '{', then ignore... and pretend its okay, cause it might be... |
| // |
| // This seems to get around the problem with things like: |
| // |
| // class a { def b() { |
| // |
| |
| if (buffer[-1].trim().endsWith('{')) { |
| // ignore, this blows |
| } |
| else { |
| error = e |
| } |
| } |
| } |
| catch (Throwable e) { |
| error = e |
| } |
| |
| if (error) { |
| log.debug("Parse error: $error") |
| |
| return new ParseStatus(error) |
| } |
| else { |
| log.debug('Parse incomplete') |
| |
| return new ParseStatus(ParseCode.INCOMPLETE) |
| } |
| } |
| |
| private static final String EVAL_SCRIPT_FILENAME = 'groovysh_evaluate' |
| |
| /** |
| * Evaluate the given buffer. The buffer is assumed to be complete. |
| */ |
| private Object evaluate(final List buffer) { |
| assert buffer |
| |
| log.debug("Evaluating buffer...") |
| |
| if (io.verbose) { |
| displayBuffer(buffer) |
| } |
| |
| def source = (imports + buffer).join(NEWLINE) |
| def result |
| |
| Class type |
| try { |
| Script script = interp.parse(source, EVAL_SCRIPT_FILENAME) |
| type = script.getClass() |
| |
| log.debug("Compiled script: $script") |
| |
| if (type.declaredMethods.any { it.name == 'main' }) { |
| result = script.run() |
| } |
| |
| log.debug("Evaluation result: $result") |
| |
| // Keep only the methods that have been defined in the script |
| type.declaredMethods.each { Method m -> |
| if (!(m.name in [ 'main', 'run' ] || m.name.startsWith('super$') || m.name.startsWith('class$'))) { |
| log.debug("Saving method definition: $m") |
| interp.context["${m.name}"] = new MethodClosure(type.newInstance(), m.name) |
| } |
| } |
| } |
| /* |
| catch (Throwable t) { |
| // Unroll invoker exceptions |
| if (t instanceof InvokerInvocationException) { |
| t = t.cause |
| } |
| |
| throw t |
| } |
| */ |
| finally { |
| def cache = interp.classLoader.classCache |
| |
| // Remove the script class generated |
| cache.remove(type?.name) |
| |
| // Remove the inline closures from the cache as well |
| cache.remove('$_run_closure') |
| } |
| |
| return result |
| } |
| |
| /** |
| * Format the given number suitable for rendering as a line number column. |
| */ |
| private String formatLineNumber(final int num) { |
| assert num >= 0 |
| |
| // Make a %03d-like string for the line number |
| return num.toString().padLeft(3, '0') |
| } |
| |
| /** |
| * Display the given buffer. |
| */ |
| private void displayBuffer(final List buffer) { |
| assert buffer |
| |
| buffer.eachWithIndex { line, index -> |
| def lineNum = formatLineNumber(index + 1) |
| |
| io.out.println(" ${lineNum}@|bold >| $line") |
| } |
| } |
| |
| private void loadUserScript(final String filename) { |
| assert filename |
| |
| def file = new File(userStateDirectory, filename) |
| |
| if (file.exists()) { |
| def command = registry['load'] |
| |
| if (command) { |
| log.debug("Loading user-script: $file") |
| |
| command.load(file.toURI().toURL()) |
| } |
| else { |
| log.error("Unable to load user-script, missing 'load' command") |
| } |
| } |
| } |
| |
| private void displayError(final Throwable cause) { |
| assert cause != null |
| |
| io.err.println("@|bold,red ERROR| ${cause.class.name}: @|bold,red ${cause.message}|") |
| |
| if (log.debug) { |
| // If we have debug enabled then skip the fancy bits below |
| log.debug(cause) |
| } |
| else if (io.verbose) { |
| def trace = cause.stackTrace |
| def buff = new StringBuffer() |
| |
| for (e in trace) { |
| buff << " @|bold at| ${e.className}.${e.methodName} (@|bold " |
| |
| buff << (e.nativeMethod ? 'Native Method' : |
| (e.fileName != null && e.lineNumber != -1 ? "${e.fileName}:${e.lineNumber}" : |
| (e.fileName != null ? e.fileName : 'Unknown Source'))) |
| |
| buff << '|)' |
| |
| io.err.println(buff) |
| |
| buff.setLength(0) // Reset the buffer |
| |
| // |
| // FIXME: Need to make sure this doesn't eat up other muck... |
| // |
| |
| // Stop the trace once we find the root of the evaluated script |
| if (e.className == EVAL_SCRIPT_FILENAME && e.methodName == 'run') { |
| io.err.println(' @|bold ...|') |
| break |
| } |
| } |
| } |
| } |
| |
| int run(final String[] args) { |
| def term = Terminal.terminal |
| |
| if (log.debug) { |
| log.debug("Terminal ($term)") |
| log.debug(" Supported: $term.supported") |
| log.debug(" ECHO: $term.echo (enabled: $term.echoEnabled)") |
| log.debug(" H x W: $term.terminalHeight x $term.terminalWidth") |
| log.debug(" ANSI: ${term.isANSISupported()}") |
| |
| if (term instanceof jline.WindowsTerminal) { |
| log.debug(" Direct: ${term.directConsole}") |
| } |
| } |
| |
| def code |
| |
| try { |
| loadUserScript('groovysh.profile') |
| |
| if (args != null && args.length > 0) { |
| // Run the given commands |
| execute(args.join(' ')) |
| } |
| else { |
| loadUserScript('groovysh.rc') |
| |
| // Setup the interactive runner |
| runner = new InteractiveShellRunner(this, this.&renderPrompt as Closure) |
| |
| // Setup the history |
| runner.history = history = new History() |
| runner.historyFile = new File(userStateDirectory, 'groovysh.history') |
| |
| // Setup the error handler |
| runner.errorHandler = this.&displayError |
| |
| // |
| // TODO: See if we want to add any more language specific completions, like for println for example? |
| // |
| |
| // Display the welcome banner |
| if (!io.quiet) { |
| def width = term.terminalWidth |
| |
| // If we can't tell, or have something bogus then use a reasonable default |
| if (width < 1) { |
| width = 80 |
| } |
| |
| io.out.println(messages.format('startup_banner.0', InvokerHelper.version, System.properties['java.vm.version'])) |
| io.out.println(messages['startup_banner.1']) |
| io.out.println('-' * (width - 1)) |
| } |
| |
| // And let 'er rip... :-) |
| runner.run() |
| } |
| |
| code = 0 |
| } |
| catch (ExitNotification n) { |
| log.debug("Exiting w/code: ${n.code}") |
| |
| code = n.code |
| } |
| catch (Throwable t) { |
| io.err.println(messages.format('info.fatal', t)) |
| t.printStackTrace(io.err) |
| |
| code = 1 |
| } |
| |
| assert code != null // This should never happen |
| |
| return code |
| } |
| |
| static void main(final String[] args) { |
| def io = new IO() |
| Logger.io = io |
| |
| def cli = new CliBuilder(usage : 'groovysh [options] [...]', formatter: new HelpFormatter(), writer: io.out) |
| |
| cli.h(longOpt: 'help', messages['cli.option.help.description']) |
| cli.V(longOpt: 'version', messages['cli.option.version.description']) |
| cli.v(longOpt: 'verbose', messages['cli.option.verbose.description']) |
| cli.q(longOpt: 'quiet', messages['cli.option.quiet.description']) |
| cli.d(longOpt: 'debug', messages['cli.option.debug.description']) |
| cli.C(longOpt: 'color', args: 1, argName: 'FLAG', optionalArg: true, messages['cli.option.color.description']) |
| cli.D(longOpt: 'define', args: 1, argName: 'NAME=VALUE', messages['cli.option.define.description']) |
| cli.T(longOpt: 'terminal', args: 1, argName: 'TYPE', messages['cli.option.terminal.description']) |
| |
| def options = cli.parse(args) |
| |
| if (options.h) { |
| cli.usage() |
| System.exit(0) |
| } |
| |
| if (options.V) { |
| io.out.println(messages.format('cli.info.version', InvokerHelper.version)) |
| System.exit(0) |
| } |
| |
| if (options.hasOption('T')) { |
| def type = options.getOptionValue('T') |
| switch (type) { |
| case 'unix': |
| type = jline.UnixTerminal.class.name |
| break |
| |
| case 'win': |
| case 'windows': |
| type = jline.WindowsTerminal.class.name |
| break |
| |
| case 'false': |
| case 'off': |
| case 'none': |
| type = jline.UnsupportedTerminal.class.name |
| } |
| |
| System.setProperty('jline.terminal', type) |
| |
| // |
| // HACK: Disable ANSI, for some reason UnsupportedTerminal reports ANSI as enabled, when it shouldn't |
| // |
| ANSI.enabled = false |
| } |
| |
| if (options.hasOption('D')) { |
| def values = options.getOptionValues('D') |
| |
| values.each { |
| def name |
| def value |
| |
| if (it.indexOf('=') > 0) { |
| def tmp = it.split('=', 2) |
| name = tmp[0] |
| value = tmp[1] |
| } |
| else { |
| name = it |
| value = true |
| } |
| |
| System.setProperty(name, value) |
| } |
| } |
| |
| if (options.v) { |
| io.verbose = true |
| } |
| |
| if (options.d) { |
| Logger.debug = true |
| io.verbose = true // --debug implies --verbose |
| } |
| |
| if (options.q) { |
| io.quiet = true |
| io.verbose = false // --quiet implies !--verbose |
| Logger.debug = false // --quiet implies !--debug |
| } |
| |
| if (options.hasOption('C')) { |
| def value = options.getOptionValue('C') |
| |
| if (value == null) { |
| value = true // --color is the same as --color=true |
| } |
| else { |
| value = Boolean.valueOf(value).booleanValue(); // For JDK 1.4 compat |
| } |
| |
| ANSI.enabled = value |
| } |
| |
| def code |
| |
| // Add a hook to display some status when shutting down... |
| addShutdownHook { |
| // |
| // FIXME: We need to configure JLine to catch CTRL-C for us... if that is possible |
| // |
| |
| if (code == null) { |
| // Give the user a warning when the JVM shutdown abnormally, normal shutdown |
| // will set an exit code through the proper channels |
| |
| io.err.println() |
| io.err.println('@|red WARNING:| Abnormal JVM shutdown detected') |
| } |
| |
| io.flush() |
| } |
| |
| // Boot up the shell... :-) |
| |
| code = new Groovysh(io).run(options.arguments() as String[]) |
| |
| // Force the JVM to exit at this point, since shell could have created threads or |
| // popped up Swing components that will cause the JVM to linger after we have been |
| // asked to shutdown |
| |
| System.exit(code) |
| } |
| } |
| |
| /* |
| // |
| // FIXME: We have to have the { on the same line right now for this to work... |
| // |
| |
| enum ParseCode { |
| COMPLETE, |
| INCOMPLETE, |
| ERROR |
| ; |
| } |
| */ |
| |
| // |
| // FIXME: This new enum stuff only works on Java 5 :-( |
| // |
| |
| class ParseCode { |
| static final ParseCode COMPLETE = new ParseCode(code: 0) |
| static final ParseCode INCOMPLETE = new ParseCode(code: 1) |
| static final ParseCode ERROR = new ParseCode(code: 2) |
| |
| int code |
| |
| String toString() { |
| return code |
| } |
| } |
| |
| /** |
| * Container for parse status details. |
| * |
| * @version $Id$ |
| * @author <a href="mailto:jason@planet57.com">Jason Dillon</a> |
| */ |
| class ParseStatus |
| { |
| final ParseCode code |
| |
| final Throwable cause |
| |
| ParseStatus(final ParseCode code, final Throwable cause) { |
| this.code = code |
| this.cause = cause |
| } |
| |
| ParseStatus(final ParseCode code) { |
| this(code, null) |
| } |
| |
| ParseStatus(final Throwable cause) { |
| this(ParseCode.ERROR, cause) |
| } |
| } |