如何在Scala中为命令行创建DSL,并使用最少的额外样板文件

如何在Scala中为命令行创建DSL,并使用最少的额外样板文件,scala,dsl,Scala,Dsl,我需要为不熟悉scala(既不是Java)但熟悉Shell的用户开发一个API。他们将,基本上在scala类中编写shell脚本(我知道我可以调用外部shell脚本,但是,来吧!而且,稍后我们将有一些用于常见shell任务的函数) 我希望能完成以下任务: 1 object MyCoolScript extends MyMagicTrait { 2 $ "mkdir /tmp/test" 3 $ "cd /tmp/test" 4 $ "wget some-url" 5 } 更直

我需要为不熟悉scala(既不是Java)但熟悉Shell的用户开发一个API。他们将,基本上在scala类中编写shell脚本(我知道我可以调用外部shell脚本,但是,来吧!而且,稍后我们将有一些用于常见shell任务的函数)

我希望能完成以下任务:

1 object MyCoolScript extends MyMagicTrait {
2   $ "mkdir /tmp/test"
3   $ "cd /tmp/test"
4   $ "wget some-url"   
5 }
更直接地说,我如何将第2-4行(或可能不太简洁的版本)转换成可以在MyMagicTrait中处理的Seq[String]

我知道sys.process.stringToProcess,但如果我有:

object MyCoolScript extends MyMagicTrait {
  "mkdir /tmp/test" !!
  "cd /tmp/test" !!
  "wget some-url" !!  
}
如何以简洁的方式获得每个命令的结果?另外,我希望有一个$“xxx”符号

发布答案更新:

多亏了@debilski、@tenshi和@daniel-c-sobral,我才能够找到一个非常接近理想实现的方法:

虚拟方法 编辑:

我会这样做:

res = List("mkdir /tmp/test", "cd /tmp/test", "wget some-url").map(_!!)
// res is a List[String] of the output of your commands
您可以执行如下系统命令:

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

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

您应该能够将其封装到DSL中,但它已经相当简洁了

这只是重复@Debilski和@Tomasz的想法,表明您可以将它们很好地结合起来:

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

如果您想将shell与Scala命令相结合,我真的不知道如何减少样板文件,因为您需要一些东西来显示shell的开始和结束位置。

Scala 2.10附带的字符串插值似乎可以在这方面帮助您。首先,您可以实现简单的
$
方法,该方法只需立即执行命令。为了实现此目的,您需要在
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")
            }

    }
} 
现在您可以这样使用它:

import ShellSupport._

val testDir = "/tmp/test"

$"mkdir $testDir"
$"cd $testDir" 
$"""
    wget some-url
    wget another-url
 """ 
您可以利用它的语法(唯一的缺点是不能在
$
之间添加空格)和命令中的字符串插值


现在,让我们尝试实现您的神奇特性。这通常是相同的想法,但我也使用了
DelayedInit
,以便正确定义命令,然后在创建类时自动执行它们

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
    }
}
它的用法是:

class MyCoolScript extends MyMagicTrait {
  val downloader = "wget"

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

new MyCoolScript
这两种解决方案产生相同的输出:

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

这些结果到处都是。唉,我认为sys.process不足以满足您的需要。让我们首先写出这个示例,这样它就能满足您的要求:

val result = (
    "mkdir /tmp/test ###
    "cd /tmp/test" ###
    "wget someurl
).lines_!
现在,为什么它不起作用:

首先,
cd
将抛出一个异常——没有
cd
可执行文件。这是一个内部shell命令,因此只有shell可以执行它


接下来,假设
cd
可以工作,那么
wget
不会在
/tmp/test
发生。上面的每个命令都会在Java的当前工作目录中重新执行。您可以指定执行每个命令的目录,但不能有CWD(它不是“可变的”).

谢谢,但我知道,我的问题是如何将字符串行分组并将它们转换为列表,以便我可以处理它们。我需要得到每个处理的结果。此解决方案的问题是脚本很快会变得不可读。我说的是10-50行脚本。我想我可以使用一行+逗号作为元素ts是列表的一部分,但这不是很清楚。这种方法的问题是,我不能在命令行之间或作为命令行的一部分使用scala语句。也许scala的2.10字符串插值可以帮助我实现这一点。但我还是在寻找更好的解决方案。谢谢。@JhonnyEverson:我也希望有更好的解决方案:-)。现在我们在讨论!如果我调用$而不是shell,则我有$>”“现在我只需要一种自动应用的方法。谢谢。我喜欢代码中的行号。数到5真是太难了!它让我的sed体验始终保持新鲜。我这样做是因为我参考了第2-4行。现在看不到sed的用法:)。顺便说一句,不要使用
$
。您可以使用它编写代码,但它不是法律代码,它可能会在未经通知的情况下中断。我不确定,但我已经想到了这一点。我正在考虑对命令进行必要的调整,使其正常工作。
val result = (
    "mkdir /tmp/test ###
    "cd /tmp/test" ###
    "wget someurl
).lines_!