| /* |
| * 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) |
| } |
| } |