2011-04-07 15 views
5

OK. ScriptEngine.eval(String string) evalúa una cadena en su totalidad, y ScriptEngine.eval(Reader reader) evalúa la entrada desde un Reader en su totalidad.jsr223 + escribiendo un intérprete de guiones

Así que si tengo un archivo, puedo abrir FileInputStream, envolver un Reader y llamar al scriptEngine.eval(reader).

Si tengo una instrucción completa como una cadena, puedo llamar al scriptEngine.eval(string).

¿Qué debo hacer si necesito implementar un intérprete interactivo? Tengo un usuario que está escribiendo de forma interactiva en una declaración de líneas múltiples, p.

function f() { 
    return 3; 
} 

Si leo la línea de entrada por línea, y el uso de la forma de cadena de eval(), voy a terminar de pasarla declaraciones incompletas, por ejemplo function f() { y obtener un error.

Si paso en un lector, el ScriptEngine esperará hasta que se complete la entrada, y no es interactivo.

¡Ayuda!


Solo para aclarar: el problema aquí es que sólo puedo pasar ScriptEngine.eval() declaraciones completas, y como el cliente de ScriptEngine, no sé cuando una línea de entrada está completa sin la ayuda de la propia ScriptEngine.


shell interactivo de Rhino utiliza Rhino de Context.stringIsCompilableUnit() (ver LXR para usage y implementation).

Respuesta

4

Implementé algo que funciona bien con Java SE 6 Rhino (Javascript) y Jython 1.5.2 (Python), usando un enfoque bastante simple similar al shell interactivo de Rhino (vea mi comentario al final de la pregunta):

  • Mantenga una lista pendiente de líneas de entrada aún no evaluadas.
  • Intente compilar (pero no evaluar) las líneas de entrada pendientes.
    • Si la compilación es correcta, puede poder ejecutar líneas de entrada pendientes.
    • Si la compilación arroja una excepción, y hay una indicación de la posición (línea + número de columna) del error, y esto coincide con el final de la entrada pendiente, entonces esa es una pista de que estamos esperando más información, así que trague la excepción y espere la próxima línea.
    • De lo contrario, o bien no sabemos dónde está el error, o sucedió antes del final de la entrada pendiente, entonces vuelva a lanzar la excepción.
  • Si no esperamos más líneas de entrada, y solo tenemos una línea de entrada pendiente, entonces evalúe y reinicie.
  • Si no esperamos más líneas de entrada, y la última está en blanco (por la respuesta de @karakuricoder) y tenemos más de una línea de entrada pendiente, entonces evalúelo y reinícialo. Shell interactivo de Python parece hacer esto.
  • De lo contrario, siga leyendo las líneas de entrada.

Lo que no quiero que suceda es o bien:

  • usuarios se molesta tener que introducir líneas en blanco después de las entradas de una sola línea
  • usuarios entran en un comunicado de varias líneas de largo y solo descubrir después del hecho de que hubo un error de sintaxis en la 2da línea.

Aquí está una clase de ayuda que escribí que implementa mi enfoque:

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; 
    } 

} 
2

Cree un método que se lea desde el teclado (clase de escáner) y cree una cadena completa a partir de varias líneas de entrada. Ingrese en una línea en blanco indica el final de la entrada del usuario. Pase la cadena al método eval.

+0

... y cómo ayuda eso? ¿Cómo se supone que debo distinguir entre un enunciado incompleto y un enunciado completo sin la ayuda de ScriptingEngine mismo? –

+0

"Entrar en una línea en blanco indica el final de la entrada del usuario" - ICK - eso funcionaría, pero es molesto. –

+0

No hay nada que impida que una declaración incompleta provenga de una transmisión, ya sea un archivo, una conexión de red o un teclado. Tendrá que hacer alguna pre-verificación o la eval impl tendrá que tomar las medidas necesarias para manejar/verificar que la declaración no esté completa. En segundo lugar, cada flujo debe tener un marcador final. Leer desde un flujo que no sea del teclado devuelve un valor de indicador como EOF, -1, nulo, etc. Puede elegir el medio que desee para el final de la entrada, pero tiene que haber una forma programática para determinar que el usuario ha terminado. – karakuricoder

Cuestiones relacionadas