Simplifying scala scripts
I've already written several dozens of scala scripts (series computations, ssh automation, various jmx operations, remote administration, garbage collector log analysis, ...) and found this language quite interesting for use as a script language. There's many reasons for that :
In a next POST, I'll describe more in detail a new bootstrap implementation that will bring #include feature to scala script.
- 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
Then generate a standalone executable jar with this class and all needed dependencies, thanks to such SBT build specification :
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)
}
}
you'll be able to directly run any scala script like that :
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"
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.
#!/bin/sh
exec java -jar bootstrap.jar "$0" "$@"
!#
cd("/etc/")
"ls" #| "grep net" !
In a next POST, I'll describe more in detail a new bootstrap implementation that will bring #include feature to scala script.