Cómo crear DSL en Scala para líneas de comando con un mínimo de repetitivo extra

Necesito desarrollar una API para usuarios que no estén familiarizados con Scala (ni con Java) pero que estén familiarizados con Shell. Básicamente, escribirán scripts de shell dentro de una clase de Scala (sé que podría simplemente llamar a scripts de shell externos, ¡pero vamos! Además, más adelante tendremos algunas funciones para tareas de shell comunes).

Esperaba lograr algo como:

1 object MyCoolScript extends MyMagicTrait {
2   $ "mkdir /tmp/test"
3   $ "cd /tmp/test"
4   $ "wget some-url"   
5 }

Siendo más directo, ¿cómo puedo convertir las líneas 2-4 (o una versión posiblemente menos concisa) en Seq[String] que podría procesar en MyMagicTrait?

Se acerca de sys.process.stringToProcess pero si tengo:

object MyCoolScript extends MyMagicTrait {
  "mkdir /tmp/test" !!
  "cd /tmp/test" !!
  "wget some-url" !!  
}

¿Cómo puedo obtener el resultado de cada comando de manera concisa? Además, esperaba una notación de $ "xxx".

Publicar actualización de respuestas:

Gracias a @debilski, @tenshi y @daniel-c-sobral pude acercarme mucho a la implementación deseada: https://gist.github.com/2777994

preguntado el 22 de mayo de 12 a las 19:05

Me gusta cuando obtenemos números de línea en código. ¡Es tan difícil contar hasta 5! Y mantiene mi experiencia sed siempre fresca. -

Lo hice porque me refería a las líneas 2-4. No veo el uso de sed ahora :). -

Por cierto, NO UTILIZAR $. Puede escribir código usándolo, pero no es un código legal y puede romperse sin previo aviso. -

6 Respuestas

class Shell {
  var elems = Vector[String]()
  def >(str: String) = elems :+= str

  def run() = elems.map( whatever )
}

val shell = new Shell

shell> "mkdir /tmp/test.dir"
shell> "cd /tmp/test.dir"

contestado el 22 de mayo de 12 a las 20:05

¡Ahora estamos hablando! Si llamo $ en lugar de shell, tengo $> "". Ahora solo necesito una forma de aplicar eso automáticamente. Gracias. - johnny everson

Parece que la interpolación de cadenas que viene con Scala 2.10 puede ayudarlo aquí. Al principio puedes implementar simples $ método, que simplemente ejecuta el comando de inmediato. Para hacerlo, debe agregar este método personalizado en StringContext:

object ShellSupport {
    implicit class ShellStrings(sc: StringContext) {
        def $(args: Any*) = 
            sc.s(args: _*) split "\n" map (_.trim) filterNot (_.isEmpty) foreach { cmd =>
                // your excution logic goes here
                println(s"Executing: $cmd")
            }

    }
} 

Ahora puedes usarlo así:

import ShellSupport._

val testDir = "/tmp/test"

$"mkdir $testDir"
$"cd $testDir" 
$"""
    wget some-url
    wget another-url
 """ 

Puede aprovechar su sintaxis (el único inconveniente es que no puede agregar espacio entre $ y ") e interpolación de cadenas dentro del comando.


Ahora intentemos implementar tu rasgo mágico. Generalmente es la misma idea, pero también estoy usando DelayedInit para definir correctamente los comandos y luego ejecutarlos automáticamente durante la creación de la clase.

trait MyMagicTrait extends DelayedInit {
    private var cmds: List[String] = Nil

    def commands = cmds

    implicit class ShellStrings(sc: StringContext) {
        def $(args: Any*) = {
            val newCmds = sc.s(args: _*) split "\n" map (_.trim) filterNot (_.isEmpty)
            cmds = cmds ++ newCmds
        }
    }

    def delayedInit(x: => Unit) {
        // your excution logic goes here
        x
        cmds map ("Excutintg: " + _) foreach println
    }
}

Y su uso:

class MyCoolScript extends MyMagicTrait {
  val downloader = "wget"

  $"mkdir /tmp/test"
  $"cd /tmp/test" 
  $"""
    $downloader some-url
    $downloader another-url
   """ 
}

new MyCoolScript

Ambas soluciones producen el mismo resultado:

Executing: mkdir /tmp/test
Executing: cd /tmp/test
Executing: wget some-url
Executing: wget another-url

Respondido 27 Jul 15, 03:07

Esto es solo una referencia a las ideas de @Debilski y @Tomasz para demostrar que puedes combinarlas muy bien:

trait Magic {
  object shell {
    var lines = IndexedSeq[String]()
    def >(xs: String) { lines ++= xs.split("\n").map(_.trim) }
  }
  def doSomethingWithCommands { shell.lines foreach println }
}

object MyCoolScript extends App with Magic {
  println("this is Scala")

  shell> """
    mkdir /tmp/test
    cd /tmp/test
  """

  doSomethingWithCommands

  shell> """
    wget some-url
  """
}

Realmente no veo cómo podría obtener menos repetitivo si desea combinar comandos de shell con Scala, ya que necesita algo para mostrar dónde comienza y termina Shell.

contestado el 22 de mayo de 12 a las 21:05

enfoque ficticio

class Shell(commands: String) {
    commands.lines foreach {
        command =>
            //run your command
    }
}

class MyCoolScript extends Shell("""

    mkdir /tmp/test
    cd /tmp/test
    wget some-url

""")

contestado el 22 de mayo de 12 a las 20:05

El problema con este enfoque es que no puedo tener declaraciones Scala entre líneas de comando o como parte de ellas. Tal vez la interpolación de cadenas 2.10 de Scala pueda ayudarme con eso. Todavía esperaba una solución mejor. Gracias. - johnny everson

@JhonnyEverson: También espero mejores soluciones :-). - Tomasz Nurkiewicz

Estos resultados están por todas partes. Por desgracia, no creo que sys.process sea suficiente para ti. Primero escribamos ese ejemplo, de tal manera que haga lo que pediste:

val result = (
    "mkdir /tmp/test ###
    "cd /tmp/test" ###
    "wget someurl
).lines_!

Ahora, por qué no funciona:

En primer lugar, cd lanzará una excepción, no hay cd ejecutable. Ese es un comando de shell interno, por lo que solo el shell puede ejecutarlo.

A continuación, suponiendo cd funcionaría, el wget se no suceder en /tmp/test. Cada uno de los comandos anteriores se ejecuta de nuevo en el directorio de trabajo actual de Java. Puede especificar el directorio en el que se ejecutará cada comando, pero no puede tener un CWD (no es "mutable").

contestado el 22 de mayo de 12 a las 23:05

No estaba seguro, pero lo tenía en mente. Estuve pensando en hacer los ajustes necesarios para que funcionen los comandos. - johnny everson

EDIT:

Yo haría esto:

res = List("mkdir /tmp/test", "cd /tmp/test", "wget some-url").map(_!!)
// res is a List[String] of the output of your commands

Puede ejecutar comandos del sistema así:

scala> import scala.sys.process._
import scala.sys.process._

scala> "pwd"!
/Users/kgann
res0: Int = 0

Debería poder envolver esto en un DSL, pero ya es bastante conciso.

contestado el 22 de mayo de 12 a las 20:05

Gracias, pero sé que mi pregunta es cómo puedo agrupar líneas de cadenas y convertirlas en una lista para poder procesarlas. Necesito obtener el resultado de cada proceso: johnny everson

El problema con esta solución es que los scripts se volverán rápidamente ilegibles. Estoy hablando de 10-50 líneas de guión. Supongo que podría usar una línea + coma para los elementos de la Lista, pero eso no es muy claro. - johnny everson

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.