jsr223 +编写脚本解释器

发布于 2024-10-30 17:57:26 字数 1471 浏览 0 评论 0原文

好的。 ScriptEngine.eval(String string) 评估整个字符串,并且 ScriptEngine.eval(Reader reader) 评估来自 Reader 完整地。

因此,如果我有一个文件,我可以打开一个 FileInputStream,在其周围包装一个 Reader,然后调用 scriptEngine.eval(reader)。

如果我有一个完整的字符串语句,我可以调用 scriptEngine.eval(string)。

如果我需要实现交互式解释器,我该怎么办?我有一个用户以交互方式输入多行语句,例如,

 function f() {
     return 3;
 }

如果我逐行读取输入,并使用 eval() 的字符串形式,我最终会传递不完整的语句,例如 function f() { 并得到一个错误。

如果我传入 Reader,ScriptEngine 将永远等待,直到输入完成,并且它不是交互式的。

帮助!


只是为了澄清:这里的问题是我只能传递 ScriptEngine.eval() 完整的语句,并且作为 ScriptEngine 的客户,如果没有帮助,我不知道输入行何时完成脚本引擎本身。


Rhino 的交互式 shell 使用 Rhino 的 Context.stringIsCompilableUnit() (请参阅 LXR 的 用法实现)。

OK. ScriptEngine.eval(String string) evaluates a string in its entirety, and ScriptEngine.eval(Reader reader) evaluates the input from a Reader in its entirety.

So if I have a file, I can open a FileInputStream, wrap a Reader around it, and call scriptEngine.eval(reader).

If I have a complete statement as a string, I can call scriptEngine.eval(string).

What do I do if I need to implement an interactive interpreter? I have a user who is interactively typing in a multiline statement, e.g.

 function f() {
     return 3;
 }

If I read the input line by line, and use the String form of eval(), I'll end up passing it incomplete statements, e.g. function f() { and get an error.

If I pass in a Reader, the ScriptEngine will wait forever until the input is complete, and it's not interactive.

Help!


Just to clarify: the problem here is that I can only pass ScriptEngine.eval() complete statements, and as the customer of ScriptEngine, I don't know when an input line is complete without some help from the ScriptEngine itself.


Rhino's interactive shell uses Rhino's Context.stringIsCompilableUnit() (see LXR for usage and implementation).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

回忆凄美了谁 2024-11-06 17:57:26

我实现了一些可以与 Java SE 6 Rhino (Javascript) 和 Jython 1.5.2 (Python) 配合使用的东西,使用了一种相当简单的方法,类似于 Rhino 的交互式 shell(请参阅我在问题末尾的评论)

  • :输入行尚未评估。
  • 尝试编译(但不评估)待处理的输入行。
    • 如果编译正常,我们也许能够执行待处理的输入行。
    • 如果编译抛出异常,并且指示错误的位置(行+列号),并且这与待处理输入的末尾匹配,那么这就是我们期待更多输入的线索,因此,请接受异常并等待下一行。
    • 否则,我们要么不知道错误在哪里,要么发生在待处理输入结束之前,因此重新抛出异常。
  • 如果我们不需要更多的输入行,并且只有一行待处理的输入,则对其进行评估并重新启动。
  • 如果我们不期望有更多的输入行,并且最后一行是空白行(根据 @karakuricoder 的答案),并且我们有不止一行待处理的输入,则对其进行评估并重新启动。 Python 的交互式 shell 似乎就是这样做的。
  • 否则,继续读取输入行。

想要发生的是:

  • 用户在单行输入后必须输入额外的空行而感到恼火
  • 用户输入很长的多行语句并且只有在事实之后才发现第二行有语法错误。

这是我编写的一个实现我的方法的辅助类:

import java.lang.reflect.Method;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

public class ScriptEngineInterpreter
{
    private static final boolean DEBUG = false;
    final private ScriptEngine engine;
    final private Bindings bindings;
    final private StringBuilder sb;
    private int lineNumber;
    private int pendingLineCount;
    private boolean expectingMoreInput;

    /**
     * @param engine ScriptingEngine to use in this interpreter
     * @param bindings Bindings to use in this interpreter
     */
    public ScriptEngineInterpreter(ScriptEngine engine, Bindings bindings) 
    { 
        this.engine = engine; 
        this.bindings = bindings;
        this.sb = new StringBuilder();
        this.lineNumber = 0;
        reset();
    }       
    /** @return ScriptEngine used by this interpreter */
    public ScriptEngine getEngine() { return this.engine; }
    protected void reset() { 
        this.sb.setLength(0);
        this.pendingLineCount = 0;
        setExpectingMoreInput(false);
    }
    /** @return whether the interpreter is ready for a brand new statement. */
    public boolean isReady() { return this.sb.length() == 0; }
    /**
     * @return whether the interpreter expects more input
     * 
     * A true value means there is definitely more input needed.
     * A false value means no more input is needed, but it may not yet
     * be appropriate to evaluate all the pending lines.
     * (there's some ambiguity depending on the language)
     */
    public boolean isExpectingMoreInput() { return this.expectingMoreInput; }
    protected void setExpectingMoreInput(boolean b) { this.expectingMoreInput = b; }
    /**
     * @return number of lines pending execution
     */
    protected int getPendingLineCount() { return this.pendingLineCount; }
    /**
     * @param lineIsEmpty whether the last line is empty
     * @return whether we should evaluate the pending input
     * The default behavior is to evaluate if we only have one line of input,
     * or if the user enters a blank line.
     * This behavior should be overridden where appropriate.
     */
    protected boolean shouldEvaluatePendingInput(boolean lineIsEmpty) 
    {
        if (isExpectingMoreInput())
            return false;
        else
            return (getPendingLineCount() == 1) || lineIsEmpty; 
    } 
    /**
     * @param line line to interpret
     * @return value of the line (or null if there is still pending input)
     * @throws ScriptException in case of an exception
     */
    public Object interpret(String line) throws ScriptException
    {
        ++this.lineNumber;
        if (line.isEmpty())
        {
            if (!shouldEvaluatePendingInput(true))
                return null;
        }

        ++this.pendingLineCount;        
        this.sb.append(line);
        this.sb.append("\n");
        CompiledScript cs = tryCompiling(this.sb.toString(), getPendingLineCount(), line.length());

        if (cs == null)
        {
            return null;
        }
        else if (shouldEvaluatePendingInput(line.isEmpty()))
        {
            try
            {
                Object result = cs.eval(this.bindings);
                return result;
            }
            finally
            {
                reset();
            }
        }
        else
        {
            return null;
        }
    }
    private CompiledScript tryCompiling(String string, int lineCount, int lastLineLength)
        throws ScriptException 
    {
        CompiledScript result = null;
        try
        {
            Compilable c = (Compilable)this.engine;
            result = c.compile(string);
        }
        catch (ScriptException se) {
            boolean rethrow = true;
            if (se.getCause() != null)
            {
                Integer col = columnNumber(se);
                Integer line = lineNumber(se);
                /* swallow the exception if it occurs at the last character
                 * of the input (we may need to wait for more lines)
                 */
                if (col != null
                 && line != null 
                 && line.intValue() == lineCount 
                 && col.intValue() == lastLineLength)
                {
                    rethrow = false;
                }
                else if (DEBUG)
                {
                    String msg = se.getCause().getMessage();
                    System.err.println("L"+line+" C"+col+"("+lineCount+","+lastLineLength+"): "+msg);
                    System.err.println("in '"+string+"'");
                }
            }

            if (rethrow)
            {
                reset();
                throw se;
            }
        }

        setExpectingMoreInput(result == null);
        return result;
    }
    private Integer columnNumber(ScriptException se)
    {       
        if (se.getColumnNumber() >= 0)
            return se.getColumnNumber();
        return callMethod(se.getCause(), "columnNumber", Integer.class);
    }
    private Integer lineNumber(ScriptException se)
    {       
        if (se.getLineNumber() >= 0)
            return se.getLineNumber();
        return callMethod(se.getCause(), "lineNumber", Integer.class);
    }
    static private Method getMethod(Object object, String methodName)
    {
        try {
            return object.getClass().getMethod(methodName);
        }
        catch (NoSuchMethodException e) {
            return null;
            /* gulp */ 
        }
    }
    static private <T> T callMethod(Object object, String methodName, Class<T> cl) {
        try {
            Method m = getMethod(object, methodName);
            if (m != null)
            {
                Object result = m.invoke(object); 
                return cl.cast(result);
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

I implemented something that works OK with Java SE 6 Rhino (Javascript) and Jython 1.5.2 (Python), using a fairly simple approach similar to Rhino's interactive shell (see my remark at the end of the question):

  • Keep a pending list of input lines not yet evaluated.
  • Try compiling (but not evaluating) the pending input lines.
    • If the compilation is OK, we may be able to execute pending input lines.
    • If the compilation throws an exception, and there is an indication of the position (line + column number) of the error, and this matches the end of the pending input, then that's a clue that we're expecting more input, so swallow the exception and wait for the next line.
    • Otherwise, we either don't know where the error is, or it happened prior to the end of the pending input, so rethrow the exception.
  • If we are not expecting any more input lines, and we only have one line of pending input, then evaluate it and restart.
  • If we are not expecting any more input lines, and the last one is a blank one (per @karakuricoder's answer) and we have more than one line of pending input, then evaluate it and restart. Python's interactive shell seems to do this.
  • Otherwise, keep reading input lines.

What I didn't want to happen is either:

  • users get annoyed having to enter extra blank lines after single-line inputs
  • users enter a long multi-line statement and only find out after the fact that there was a syntax error in the 2nd line.

Here's a helper class I wrote that implements my approach:

import java.lang.reflect.Method;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

public class ScriptEngineInterpreter
{
    private static final boolean DEBUG = false;
    final private ScriptEngine engine;
    final private Bindings bindings;
    final private StringBuilder sb;
    private int lineNumber;
    private int pendingLineCount;
    private boolean expectingMoreInput;

    /**
     * @param engine ScriptingEngine to use in this interpreter
     * @param bindings Bindings to use in this interpreter
     */
    public ScriptEngineInterpreter(ScriptEngine engine, Bindings bindings) 
    { 
        this.engine = engine; 
        this.bindings = bindings;
        this.sb = new StringBuilder();
        this.lineNumber = 0;
        reset();
    }       
    /** @return ScriptEngine used by this interpreter */
    public ScriptEngine getEngine() { return this.engine; }
    protected void reset() { 
        this.sb.setLength(0);
        this.pendingLineCount = 0;
        setExpectingMoreInput(false);
    }
    /** @return whether the interpreter is ready for a brand new statement. */
    public boolean isReady() { return this.sb.length() == 0; }
    /**
     * @return whether the interpreter expects more input
     * 
     * A true value means there is definitely more input needed.
     * A false value means no more input is needed, but it may not yet
     * be appropriate to evaluate all the pending lines.
     * (there's some ambiguity depending on the language)
     */
    public boolean isExpectingMoreInput() { return this.expectingMoreInput; }
    protected void setExpectingMoreInput(boolean b) { this.expectingMoreInput = b; }
    /**
     * @return number of lines pending execution
     */
    protected int getPendingLineCount() { return this.pendingLineCount; }
    /**
     * @param lineIsEmpty whether the last line is empty
     * @return whether we should evaluate the pending input
     * The default behavior is to evaluate if we only have one line of input,
     * or if the user enters a blank line.
     * This behavior should be overridden where appropriate.
     */
    protected boolean shouldEvaluatePendingInput(boolean lineIsEmpty) 
    {
        if (isExpectingMoreInput())
            return false;
        else
            return (getPendingLineCount() == 1) || lineIsEmpty; 
    } 
    /**
     * @param line line to interpret
     * @return value of the line (or null if there is still pending input)
     * @throws ScriptException in case of an exception
     */
    public Object interpret(String line) throws ScriptException
    {
        ++this.lineNumber;
        if (line.isEmpty())
        {
            if (!shouldEvaluatePendingInput(true))
                return null;
        }

        ++this.pendingLineCount;        
        this.sb.append(line);
        this.sb.append("\n");
        CompiledScript cs = tryCompiling(this.sb.toString(), getPendingLineCount(), line.length());

        if (cs == null)
        {
            return null;
        }
        else if (shouldEvaluatePendingInput(line.isEmpty()))
        {
            try
            {
                Object result = cs.eval(this.bindings);
                return result;
            }
            finally
            {
                reset();
            }
        }
        else
        {
            return null;
        }
    }
    private CompiledScript tryCompiling(String string, int lineCount, int lastLineLength)
        throws ScriptException 
    {
        CompiledScript result = null;
        try
        {
            Compilable c = (Compilable)this.engine;
            result = c.compile(string);
        }
        catch (ScriptException se) {
            boolean rethrow = true;
            if (se.getCause() != null)
            {
                Integer col = columnNumber(se);
                Integer line = lineNumber(se);
                /* swallow the exception if it occurs at the last character
                 * of the input (we may need to wait for more lines)
                 */
                if (col != null
                 && line != null 
                 && line.intValue() == lineCount 
                 && col.intValue() == lastLineLength)
                {
                    rethrow = false;
                }
                else if (DEBUG)
                {
                    String msg = se.getCause().getMessage();
                    System.err.println("L"+line+" C"+col+"("+lineCount+","+lastLineLength+"): "+msg);
                    System.err.println("in '"+string+"'");
                }
            }

            if (rethrow)
            {
                reset();
                throw se;
            }
        }

        setExpectingMoreInput(result == null);
        return result;
    }
    private Integer columnNumber(ScriptException se)
    {       
        if (se.getColumnNumber() >= 0)
            return se.getColumnNumber();
        return callMethod(se.getCause(), "columnNumber", Integer.class);
    }
    private Integer lineNumber(ScriptException se)
    {       
        if (se.getLineNumber() >= 0)
            return se.getLineNumber();
        return callMethod(se.getCause(), "lineNumber", Integer.class);
    }
    static private Method getMethod(Object object, String methodName)
    {
        try {
            return object.getClass().getMethod(methodName);
        }
        catch (NoSuchMethodException e) {
            return null;
            /* gulp */ 
        }
    }
    static private <T> T callMethod(Object object, String methodName, Class<T> cl) {
        try {
            Method m = getMethod(object, methodName);
            if (m != null)
            {
                Object result = m.invoke(object); 
                return cl.cast(result);
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}
何以畏孤独 2024-11-06 17:57:26

创建一个从键盘(Scanner 类)读取数据并从多行输入创建完整字符串的方法。在空行上输入表示用户输入结束。将字符串传递给 eval 方法。

Create a method that reads from the keyboard (Scanner class) and creates a complete string from multiple lines of input. Enter on a blank line signals the end of user input. Pass the string into the eval method.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文