Об использовании Scala в качестве скриптового языка

27 ноября 2015

Прямо скажем, использование Scala в качестве языка для написания скриптов — довольно сомнительная идея. Язык действительно можно использовать таким образом. Но проблема заключается в том, что скрипты довольно долго стартуют. На моей машине время запуска одного скрипта на Scala составляет около 4-5 секунд. На всяких же там ультрабуках это время еще больше.

В результате убивается, пожалуй, одно из самых главных преимуществ скриптовых языков — скорость разработки. Не говоря уже о том, что не всякий пользователь станет устанавливать JVM и тем более Scala ради запуска скриптов, а затем ждать 5 и более секунд при каждом запуске. Уж проще скомпилировать небольшую программку и распространять ее в виде fat jar. Тем не менее, в некоторых случаях такой сценарий использования Scala может быть оправдан. Например, если вся команда пишет только на Scala, и следовательно на этом языке написан какой-то код, который хочется использовать в скриптах.

Простейший скрипт на Scala выглядит так:

#!/usr/bin/sbt -Dsbt.version=0.13.7 -Dsbt.main.class=sbt.ScriptMain
!#

/***
scalaVersion := "2.11.7"
*/


if(args.length < 1) {
  println("Usage: ./hello.scala <name>")
  System.exit(1)
}

val name = args(0)

println(s"Hello, $name!")

Делаем ему chmod u+x, запускаем, радуемся. Вы, конечно же, заметили, что в скрипте можно указывать требуемую версию Scala. Помимо нее также можно указывать необходимые зависимости:

#!/usr/bin/sbt -Dsbt.version=0.13.7 -Dsbt.main.class=sbt.ScriptMain
!#

/***
scalaVersion := "2.11.7"

libraryDependencies += "org.json4s" %% "json4s-jackson" % "3.2.11"
*/


import org.json4s._
import org.json4s.jackson.JsonMethods._

val t = parse("""{"postId":123, "text":"ololo"}""")
println(t)

При написании скриптов часто требуется взаимодействовать со сторонними программами. Тут на помощь приходит пакет sys.process. Например, так можно получить код возврата:

#!/usr/bin/sbt -Dsbt.version=0.13.7 -Dsbt.main.class=sbt.ScriptMain
!#

/***
scalaVersion := "2.11.7"
*/


import sys.process._

val code = getExitCode("pwd")
println(s"code = $code")

def getExitCode(cmd: ProcessBuilder): Int = {
  cmd.!(ProcessLogger(line => () ))
}

Для получения текста, выведенного процессом, я написал такую функцию:

def run(cmd: ProcessBuilder): String = {
  val buffer = new StringBuffer()
  val exitCode = cmd.!(ProcessLogger(line => buffer append s"$line\n"))
  if(exitCode != 0) {
    println(s"Command `$cmd` terminated with status $exitCode")
    System.exit(1)
  }
  buffer.toString.trim
}

В некоторых случаях больше подходит функция runVerbose:

def runVerbose(cmd: ProcessBuilder): String = {
  val buffer = new StringBuffer()
  println("--------------------------------------------------------")
  val exitCode = cmd.!(ProcessLogger({ line =>
                         println(line)
                         buffer append s"$line\n"
                       } : String => Unit) )
  if(exitCode != 0) {
    println(s"Command `$cmd` terminated with status $exitCode")
    System.exit(1)
  }
  println("--------------------------------------------------------")
  buffer.toString
}

Вывод команд можно перенаправлять при помощи оператора #|:

def getInstanceStatus(instanceId: String): String = {
  run(
    awsCmd(s"ec2 describe-instances --instance-ids $instanceId") #|
      Seq("jq", "-r", ".Reservations[0].Instances[0].State.Name")
  ).trim
}

def awsCmd(cmd: String): String = {
  s"aws --profile $profile --region $region $cmd"
}

К этому моменту вы, конечно же, успели обратить внимание, как String и Seq[String] неявно преобразуются в класс ProcessBuilder, принимаемый функцией run. Мне лично не очень нравится использование неявных преобразований для таких задач, но интерфейс придумывал не я.

Довольно часто может потребоваться перенаправить вывод команды в файл:

#!/usr/bin/sbt -Dsbt.version=0.13.7 -Dsbt.main.class=sbt.ScriptMain
!#

/***
scalaVersion := "2.11.7"
*/


import sys.process._
import java.io._

run("ls -la" #| "wc -l" #> new File("/tmp/out.txt"))

def run(cmd: ProcessBuilder): String = {
  // ... see above ...
}

В пакете sys.process._ есть и другие интересные операторы. По моему опыту они редко нужны на практике, тем не менее знать о них полезно:

scala> import sys.process._
import sys.process._

scala> import java.io._
import java.io._

scala> import scala.language.postfixOps
import scala.language.postfixOps

scala> "ls -la" #| "grep pdf" !
... some output here ...
res0: Int = 0

scala> "id" !!
res1: String =
"uid=1000(eax) gid=1000(eax) groups=...
"

scala> "id".!!.trim
res2: String = uid=1000(eax) gid=1000(eax) groups=...

scala> "wc -l" #< new File("/tmp/out.txt") !
1
res3: Int = 0

scala> "id" #>> new File("/tmp/out.txt") !
res4: Int = 0

scala> "id" #&& "pwd" !
uid=1000(eax) gid=1000(eax) groups=...
/home/eax/temp
res5: Int = 0

scala> "ls -la /no/such/file" #|| "true" !
ls: cannot access /no/such/file: No such file or directory
res6: Int = 0

Собственно, это все. А что вы думаете о написании скриптов на языке Scala?

Дополнение: Как я выбирал скриптовый язык и остановился на Python

Метки: , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.