Scripting in Scala¶
| scala |
Scala language compiles sources code to Java bytecode. But, it has some nice scripting facilities. Let’s review them.
So, the scala
command is also a shell-script wrapper around the java command.
According to official [scala utility] documentation (here highlighted only post-related notes):
scala [ <option> ]... [ <torun> <argument>... ]
The
scala
utility runs Scala code using a Java runtime environment.If a script file is specified to run, then the file is read and all Scala statements and declarations in the file are processed in order. Any arguments specified will be available via the argsvariable.
Script files may have an optional header that is ignored if present. There are two ways to format the header: either beginning with #! and ending with !#, or beginning with ::#! and ending with ::!#.
Such a header must have each header boundary start at the beginning of a line. Headers can be used to make stand-alone script files, as shown in the examples below.
Here is a complete Scala script (check.sh) for Unix:
Here is a complete Scala script (check.bat) for MS Windows:#!/bin/sh exec scala "$0" "$@" !# Console.println("Hello, world!") argv.toList foreach Console.println
If you want to use the compilation cache to speed up multiple executions of the script (check.sh), then add -savecompiled to the scala command:::#! @echo off call scala %0 %* goto :eof ::!# Console.println("Hello, world!") argv.toList foreach Console.println
#!/bin/sh exec scala -savecompiled "$0" "$@" !# Console.println("Hello, world!") argv.toList foreach Console.println
These tricks give us an ability to run Scala script as plain shell script. Also, based on the setting above this script can have input parameters and almost cross-platform (see script header differences for Linux .vs. Windows).
Now, we should save the mentioned above code snippet in some file (e.g. check.sh) and make it executable. This script can be run as any Linux shell script ./check.sh
(check.bat
- Windows).
Scala utility internals¶
Linux¶
Linux script header uses the next items:
#!
it’s [shebang] interpreter directiveexec
is used to runscala
without creation new process. Commands which go right afterexec
will not be executed!#
is simple marker forscala
utility (see notes below)
E.g. This script
#! /bin/sh
echo Header
exec echo
!#
echo Body
will have the next output
$ ./test.sh
Header
We will get error in case exec
is removed:
#! /bin/sh
echo Header
!#
echo Body
Output
$ ./test.sh
Header
./test.sh: line 4: !#: command not found
Body
Windows¶
Windows batch script header uses the next items:
::
is a remark without displaying or executing that line when the batch file is run (see [Information on batch files]).::#!
is simple marker forscala
utility (see notes below)@echo off
disable echocall
calls one batch program from another.goto :eof
go to end of file::!#
is simple marker forscala
utility (see notes below)
The OS-specific script settings were identified, now let’s dive deeper to understand how scala
utility works.
scala
internals¶
This utility performs the next flow to run script:
- Run
scala.tools.nsc.MainGenericRunner#process
and identify run target “as Script” (there are other targets)ScriptRunner.runScriptAndCatch(settings, thingToRun, command.arguments)
ScriptRunner
creates temp fileFile.makeTemp("scalacmd", ".scala")
- Run compiler and clean script header
class ScriptRunner extends HasCompileSocket {
...
private def withCompiledScript(
settings: GenericRunnerSettings,
scriptFile: String)
(handler: String => Boolean): Boolean =
{
def mainClass = scriptMain(settings)
val compiler = newGlobal(settings, reporter)
new compiler.Run compile List(scriptFile)
class Global
/** If this compilation is scripted, convert the source to a script source. */
private def scripted(s: SourceFile) = s match {
case b: BatchSourceFile if settings.script.isSetByUser => ScriptSourceFile(b)
case _ => s
}
/** Compile abstract file until `globalPhase`, but at least
* to phase "namer".
*/
def compileLate(file: AbstractFile) {
if (!compiledFiles(file.path))
compileLate(new CompilationUnit(scripted(getSourceFile(file))))
}
- Cleanup shell script (remove header) via
SourceFile
. Now, it’s clear why script’s header have such strange closing markers (see line 21,content drop headerLen
- actual header remove)
object ScriptSourceFile {
/** Length of the script header from the given content, if there is one.
* The header begins with "#!" or "::#!" and ends with a line starting
* with "!#" or "::!#".
*/
def headerLength(cs: Array[Char]): Int = {
val headerPattern = Pattern.compile("""((?m)^(::)?!#.*|^.*/env .*)(\r|\n|\r\n)""")
val headerStarts = List("#!", "::#!")
if (headerStarts exists (cs startsWith _)) {
val matcher = headerPattern matcher cs.mkString
if (matcher.find) matcher.end
else throw new IOException("script file does not close its header with !# or ::!#")
}
else 0
}
def apply(file: AbstractFile, content: Array[Char]) = {
val underlying = new BatchSourceFile(file, content)
val headerLen = headerLength(content)
val stripped = new ScriptSourceFile(underlying, content drop headerLen, headerLen)
stripped
}
Add libraries to Scala script¶
Scala script libraries (jars) can be added in script’s header section:
#!/bin/sh
exec scala -classpath "lib/lib.1.jar:lib/lib.2.jar" "$0" "$@"
!#
Summary¶
There is nothing magical in Scala interpretation. Every single peace of code must be compiled. Such interesting scripting approach can be applied to Java as well.
Unfortunately, Scala is not looks like nice scripting language (because it’s not designed for this use case). It will be uncomfortable “scripting” in Scala without IDE.
References¶
- System Scripting with Scala
- [scala utility]
- [shebang]
- [Information on batch files]
- Scala Sources: