- Script automatic compilation reduce runtime error. I am often amazed at the first attempt to get a script that works and without any runtime error !
- Take benefits of scala powerfull collections that make possible to write "sql" like operations
- It becomes straightforward to parallelize tasks using Actors; one of my favorite use case is a short script that trigger an explicit garbage collection on several dozens of remote jvm in a very short time
- A #include like feature within script
- A way to modify default imports, to avoid adding always the same imports in all scripts
- the #! !# shell scala bootstrap can become long (and not DRY) once you want to add many external java dependencies
package fr.janalyse.script
import scala.tools.nsc.ScriptRunner
import scala.tools.nsc.GenericRunnerCommand
import scala.io.Source
object Bootstrap {
val header =
"""// WARNING
// Automatically generated file - do not edit !
import sys.process.Process
import sys.process.ProcessBuilder._
case class CurDir(cwd:java.io.File)
implicit def stringToCurDir(d:String) = CurDir(new java.io.File(d))
implicit def stringToProcess(cmd: String)(implicit curDir:CurDir) = Process(cmd, curDir.cwd)
implicit def stringSeqToProcess(cmd:Seq[String])(implicit curDir:CurDir) = Process(cmd, curDir.cwd)
implicit var cwd:CurDir=scala.util.Properties.userDir
def cd(dir:String=util.Properties.userDir) = cwd=dir
"""
val footer =
"""
"""
def main(cmdargs:Array[String]) {
def f(name:String) = new java.io.File(name)
val na = List("-nocompdaemon","-usejavacp","-savecompiled", "-deprecation") ++ cmdargs.toList
val command = new GenericRunnerCommand(na)
import command.settings
val scriptname = command.thingToRun
val script = f(scriptname)
val richerScript = f(scriptname.replaceFirst(".scala", ".scala-plus"))
if (script.exists()) {
if (!richerScript.exists || (script.lastModified > richerScript.lastModified)) {
val content=Source.fromFile(script).getLines().toList
val cleanedContent = content.dropWhile(x => !x.startsWith("!#")).tail.mkString("\n")
val newcontent = List(header, cleanedContent, footer).mkString("\n")
new java.io.FileOutputStream(richerScript) {
write(newcontent.getBytes())
}.close()
}
}
val args = command.arguments
ScriptRunner.runScript(settings, richerScript.getName, args)
}
}
Then generate a standalone executable jar with this class and all needed dependencies, thanks to such SBT build specification :
import AssemblyKeys._
seq(assemblySettings: _*)
name := "bootstrap"
version := "0.1"
scalaVersion := "2.9.1"
libraryDependencies <++= scalaVersion { sv =>
("org.scala-lang" % "scala-swing" % sv) ::
("org.scala-lang" % "jline" % sv % "compile") ::
("org.scala-lang" % "scala-compiler" % sv % "compile") ::
("org.scala-lang" % "scala-dbc" % sv % "compile") ::
("org.scala-lang" % "scalap" % sv % "compile") ::
("org.scala-lang" % "scala-swing" % sv % "compile") ::Nil
}
mainClass in assembly := Some("fr.janalyse.script.Bootstrap")
jarName in assembly := "bootstrap.jar"
you'll be able to directly run any scala script like that :
#!/bin/sh
exec java -jar bootstrap.jar "$0" "$@"
!#
cd("/etc/")
"ls" #| "grep net" !
Thanks to the assembly SBT plugin, you've generated a standalone executable jar, which contains the scala compiler, and our custom scala script startup mechanism.
In a next POST, I'll describe more in detail a new bootstrap implementation that will bring #include feature to scala script.
This is a great blog. How could I miss it? You should try to get on planetscala.
ReplyDeleteI am also using scala for scripting (scientific analysis), and I am partly missing this feature.
It forces me to refactor my one-off stuff and put it into my big library.
But #include would be really nice. Please publish part 2 ASAP ;-).
And maybe one could make a SIP to include a facility like this into scala. I think its too late for 2.10