blob: 00262358d633d70a6d578df5fe4e356023749d23 [file] [log] [blame]
/*
* 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 jline.Terminal
import jline.History
import org.codehaus.groovy.runtime.InvokerHelper
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.Renderer as AnsiRenderer
import org.codehaus.groovy.tools.shell.util.XmlCommandRegistrar
import org.codehaus.groovy.runtime.StackTraceUtils
import org.codehaus.groovy.tools.shell.util.Preferences
/**
* 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 BufferManager buffers = new BufferManager()
private final GroovyShell interp
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)
//
// TODO: Change this to be more embed/test friendly
//
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())
}
//
// Execution
//
/**
* 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
}
maybeRecordInput(line)
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
lastResult = 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:
throw status.cause
default:
// Should never happen
throw new Error("Invalid parse status: $status.code")
}
return result
}
protected Object executeCommand(final String line) {
return super.execute(line)
}
/**
* 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 '{' or '[' 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 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)
}
//
// HACK: Fix for GROOVY-2213. Insert a runnable statement (ie. 'true') after imports so that we can
// always run the buffer and get any class/enum/whatever defs defined.
//
def source = (imports + [ 'true' ] + 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()
}
// Need to use String.valueOf() here to avoid icky exceptions causes by GString coercion
log.debug("Evaluation result: ${String.valueOf(result)} (${result?.getClass()})")
// 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)
}
}
}
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
}
/**
* 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")
}
}
//
// Prompt
//
private AnsiRenderer prompt = new AnsiRenderer()
private String renderPrompt() {
def lineNum = formatLineNumber(buffers.current().size())
return prompt.render("@|bold groovy:|${lineNum}@|bold >| ")
}
/**
* 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')
}
//
// User Profile Scripts
//
File getUserStateDirectory() {
def userHome = new File(System.getProperty('user.home'))
def dir = new File(userHome, '.groovy')
return dir.canonicalFile
}
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")
// Disable showLastResult for profile scripts
boolean tmp = Preferences.showLastResult
Preferences.showLastResult = false
try {
command.load(file.toURI().toURL())
}
finally {
Preferences.showLastResult = tmp
}
}
else {
log.error("Unable to load user-script, missing 'load' command")
}
}
}
//
// Recording
//
private void maybeRecordInput(final String line) {
def record = registry['record']
if (record != null) {
record.recordInput(line)
}
}
private void maybeRecordResult(final Object result) {
def record = registry['record']
if (record != null) {
record.recordResult(result)
}
}
private void maybeRecordError(Throwable cause) {
def record = registry['record']
if (record != null) {
boolean sanitize = Preferences.sanitizeStackTrace
if (sanitize) {
cause = StackTraceUtils.deepSanitize(cause);
}
record.recordError(cause)
}
}
//
// Hooks
//
final Closure defaultResultHook = { result ->
boolean showLastResult = !io.quiet && (io.verbose || Preferences.showLastResult)
if (showLastResult) {
// Need to use String.valueOf() here to avoid icky exceptions causes by GString coercion
io.out.println("@|bold ===>| ${String.valueOf(result)}")
}
}
Closure resultHook = defaultResultHook
private void setLastResult(final Object result) {
if (resultHook == null) {
throw new IllegalStateException("Result hook is not set")
}
resultHook.call(result)
interp.context['_'] = result
maybeRecordResult(result)
}
private Object getLastResult() {
return interp.context['_']
}
final Closure defaultErrorHook = { Throwable cause ->
assert cause != null
io.err.println("@|bold,red ERROR| ${cause.class.name}: @|bold,red ${cause.message}|")
maybeRecordError(cause)
if (log.debug) {
// If we have debug enabled then skip the fancy bits below
log.debug(cause)
}
else {
boolean sanitize = Preferences.sanitizeStackTrace
// Sanitize the stack trace unless we are inverbose mode, or the user has request otherwise
if (!io.verbose && sanitize) {
cause = StackTraceUtils.deepSanitize(cause);
}
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
// 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
}
}
}
}
Closure errorHook = defaultErrorHook
private void displayError(final Throwable cause) {
if (errorHook == null) {
throw new IllegalStateException("Error hook is not set")
}
errorHook.call(cause)
}
//
// Interactive Shell
//
int run(final String[] args) {
String commandLine = null
if (args != null && args.length > 0) {
commandLine = args.join(' ')
}
return run(commandLine as String)
}
int run(final String commandLine) {
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 (commandLine != null) {
// Run the given commands
execute(commandLine)
}
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
}
}
/**
* Container for the parse code.
*/
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.
*/
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)
}
}