Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create DSL in Scala for command lines with minimum extra boilerplate

Tags:

scala

dsl

I need to develop an API for users not familiar with scala (neither Java) but familiar with Shell. They will, basically write shell scripts inside a scala class (I know I could just call external shell scripts, but come on! Also, later we will have some functions for common shell tasks).

I was hoping to accomplish something like:

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

Being more direct, how can I turn lines 2-4 (or a possibly less concise version) into Seq[String] that I could process in MyMagicTrait?

I know about sys.process.stringToProcess but if I have:

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

How can I get the result of each command in a concise way? also, I was hoping for a $ "xxx" notation.

Post Answers Update:

Thanks to @debilski, @tenshi and @daniel-c-sobral I was able to come up a very close to the desired implementation: https://gist.github.com/2777994

like image 635
Johnny Everson Avatar asked May 22 '12 18:05

Johnny Everson


2 Answers

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"
like image 182
Debilski Avatar answered Oct 16 '22 19:10

Debilski


Seems that string interpolation that comes with Scala 2.10 can help you here. At first you can implement simple $ method, that simply executes command immediately. In order to make it you need to add this custom method on 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")
            }

    }
} 

Now you can use it like this:

import ShellSupport._

val testDir = "/tmp/test"

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

You can take advantage of it's syntax (it's only downside, is that you can't add space between $ and ") and string interpolation within command.


Now let's try to implement your magic trait. It's generally the same idea, but I'm also using DelayedInit in order to properly define commands and then automatically execute them during class creation.

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

And it's usage:

class MyCoolScript extends MyMagicTrait {
  val downloader = "wget"

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

new MyCoolScript

Both of these solutions produce the same output:

Executing: mkdir /tmp/test
Executing: cd /tmp/test
Executing: wget some-url
Executing: wget another-url
like image 37
tenshi Avatar answered Oct 16 '22 19:10

tenshi