← На главную

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

Прямо скажем, использование 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