02 February 2015

Tags: scripting | scala | bash | shell |

blog_post_image

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:

#!/bin/sh
exec scala "$0" "$@"
!#
Console.println("Hello, world!")
argv.toList foreach Console.println

Here is a complete Scala script (**check.bat**) for MS Windows:

::#!
@echo off
call scala %0 %*
goto :eof
::!#
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:

#!/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:

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:

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:

  1. Run scala.tools.nsc.MainGenericRunner#process and identify run target “as Script” (there are other targets) ScriptRunner.runScriptAndCatch(settings, thingToRun, command.arguments)
  2. ScriptRunner creates temp file File.makeTemp("scalacmd", ".scala")
  3. 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))))
    }

  1. 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 (**jar**s) 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


comments powered by Disqus