build.gradle.kts
val revealKtVersion = "0.2.4"
group = "dev.limebeck"
version = "1.0.0"
plugins {
kotlin("jvm" ) version "1.9.10"
}
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation("dev.limebeck:revealkt-script-definition:$revealKtVersion " )
}
JetBrains Space CI/CD DSL
@file:DependsOn ("com.squareup.okhttp:okhttp:2.7.4" )
import com.squareup.okhttp.*
job("Get example.com" ) {
container(image = "amazoncorretto:17-alpine" ) {
kotlinScript {
val client = OkHttpClient()
val request = Request.Builder().url("http://example.com" ).build()
val response = client.newCall(request).execute()
println(response)
}
}
}
Github Workflows Kt
#!/usr/bin/env kotlin
@file:DependsOn ("io.github.typesafegithub:github-workflows-kt:1.13.0" )
import ...
workflow(name = "Build" , on = listOf(PullRequest()), sourceFile = __FILE__.toPath()) {
job(id = "build" , runsOn = UbuntuLatest) {
uses(action = CheckoutV4())
uses(action = SetupJavaV3())
uses(
name = "Build" ,
action = GradleBuildActionV2(
arguments = "build" ,
)
)
}
}.writeToFile()
Простое CLI приложение
#!/usr/bin/env kotlin
@file:DependsOn ("org.jetbrains.kotlinx:kotlinx-cli-jvm:0.3.6" )
import kotlinx.cli.*;
import java.io.File;
val parser = ArgParser("copyIndexed" )
val input by parser.option(ArgType.String, shortName = "i" ,
description = "Input file" ).required()
parser.parse(args)
val file = File(input)
file.useLines {
it.mapIndexed { index, line -> "$index - $line " }
.forEach { File("${file.name} -copy" ).appendText(it) }
}
Live-Plugin
(простые плагины для IDEA)
import com.intellij.openapi.actionSystem.AnActionEvent
import liveplugin.*
registerAction(id = "Insert New Line Above" , keyStroke = "ctrl alt shift ENTER" ) { event ->
val project = event.project ?: return @registerAction
val editor = event.editor ?: return @registerAction
executeCommand(editor.document, project) { document ->
val caretModel = editor.caretModel
val lineStartOffset = document.getLineStartOffset(caretModel.logicalPosition.line)
document.insertString(lineStartOffset, "\n" )
caretModel.moveToOffset(caretModel.offset + 1 )
}
}
show("Loaded 'Insert New Line Above' action<br/>Use 'ctrl+alt+shift+Enter' to run it" )
Презентации с Reveal-Kt
title = "Kotlin Script: для кого, зачем и как"
configuration {
controls = false
progress = false
}
slides {
slide {
autoanimate = true
+title { "Kotlin Script: для кого, зачем и как" }
}
}
Read-Eval-Print Loop (REPL)
Read-Eval-Print Loop (REPL)
Читает (парсит)
Исполняет
Выводит ответ
И снова
Read-Eval-Print Loop (REPL)
Зачем он нужен?
Зачем он нужен?
Обучение основам
Прототипирование
Быстрая обратная связь
Read-Eval-Print Loop (REPL)
Альтернативы
Read-Eval-Print Loop (REPL)
JShell
> jshell
| Welcome to JShell -- Version 17.0 .4 .1
| For an introduction type: /help intro
jshell> var hello = "Hello, world!"
hello ==> "Hello, world!"
jshell> System.out.println(hello)
Hello, world!
+Встроен в jdk
-нет зависимостей
Read-Eval-Print Loop (REPL)
Groovy Shell
> groovysh
Groovy Shell (4.0 .20 , JVM: 17.0 .4 .1 )
Type ':help' or ':h' for help.
-------------------------------------------------
groovy: 000 > hello = 'Hello, World!'
===> Hello, World!
groovy: 000 > println hello
Hello, World!
===> null
+есть зависимости
+проще писать благодаря Groovy
-нужно тянуть Groovy
-магия Groovy
Read-Eval-Print Loop (REPL)
KScript (REPL mode)
> kscript --interactive kscript.kts
[kscript] Resolving com.fasterxml.jackson.module:jackson-module-kotlin:2.17 .0 ...
[kscript] Creating REPL
Welcome to Kotlin version 1.9 .22 (JRE 22 +37 )
Type :help for help, :quit for quit
>>> println("Hello, World!" )
Hello, World!
>>>
kscript.kts
@file:DependsOn ("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0" )
Kotlin SHELL
> kotlin
Welcome to Kotlin version 1.9 .22 (JRE 17.0 .4 .1 +1 -LTS)
Type :help for help, :quit for quit
>>> val hello = "Hello, World!"
>>> println(hello)
Hello, World!
IJ IDEA Kotlin REPL
IJ IDEA > Tools > Kotlin > Kotlin REPL (Experimental)
Зачем нужен REPL: максимально быстро получить обратную связь по коду
Замена BASH-скриптов в автоматизации задач
Зачем заменять BASH?
Рассказать, какая боль - писать скрипты на баше.
Особенно, если в скриптах нужно например сходить за данными в БД
или по сети и после как-то преобразовать их.
Ну и typesafe, когда скрипт не запустится, пока не напишешь нормально
Зачем заменять BASH?
Bash
sed = a.txt | sed 'N; s/^/ /; s/ *\(.\{4,\}\)\n/\1 /'
Kotlin
import java.io.File
File("a.txt" ).useLines {
it.forEachIndexed { i, l -> println("$i : $l " ) }
}
Зачем заменять BASH?
Сложные скрипты на Bash - боль
Bash - write-only код
Зависимости на Bash?
Рассказать, какая боль - писать скрипты на баше.
Особенно, если в скриптах нужно например сходить за данными в БД
или по сети и после как-то преобразовать их.
Ну и typesafe, когда скрипт не запустится, пока не напишешь нормально
Почему именно Kotlin Script
Богатая экосистема JVM
Зависимости не в системе
Удобство Kotlin DSL
Типобезопасность на уровне компиляции
Java 11 (JEP 330)
class Prog {
public static void main (String[] args) { Helper.run(); }
}
class Helper {
static void run () { System.out.println("Hello!" ); }
}
> ./Prog.java
Hello!
Shebang обычный
#!/usr/bin/env java
Shebang для .java
///usr/bin/env java "$0 " "$@ " ; exit $?
Java 22 (JEP 458)
class Prog {
public static void main (String[] args) { Helper.run(); }
}
class Helper {
static void run () { System.out.println("Hello!" ); }
}
> ./Prog.java
Hello!
Java 22 (+ JEP 463)
public static void main () { Helper.run(); }
class Helper {
static void run () { System.out.println("Hello!" ); }
}
> ./ProgMain.java
Hello!
Groovy
#!/usr/bin/env groovy
@Grab ('com.fasterxml.jackson.core:jackson-databind:2.17.0' )
import com.fasterxml.jackson.databind.ObjectMapper
def value = new ObjectMapper().with {
readValue('{"key":"Hello, World!"}' , Map.class )["key" ]
}
println value
> ./ScriptWithDeps.groovy
Hello, World!
JBang (java)
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
class Main {
public static void main (String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper ();
var value = mapper.readValue("{\"key\":\"Hello, World!\"}" , Map.class);
System.out.println(value.get("key" ));
}
}
> ./JBangEx.java
[jbang] Resolving dependencies...
[jbang] com.fasterxml.jackson.core:jackson-databind:2.17.0
[jbang] Dependencies resolved
[jbang] Building jar for JBangEx.java...
Hello, World!
JBang (Groovy)
import com.fasterxml.jackson.databind.ObjectMapper
def value = new ObjectMapper().with {
readValue('{"key":"Hello, World!"}' , Map.class )["key" ]
}
println value
> ./JBangEx.groovy
[jbang] Downloading Groovy 4.0.14. Be patient, this can take several minutes...
[jbang] Installing Groovy 4.0.14...
[jbang] Resolving dependencies...
[jbang] com.fasterxml.jackson.core:jackson-databind:2.17.0
[jbang] org.apache.groovy:groovy:4.0.14
[jbang] Dependencies resolved
[jbang] Building jar for JBangEx.groovy...
Hello, World!
JBang (Kotlin)
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
val value = jacksonObjectMapper().readValue<Map<String, String>>(
"""{"key":"Hello, World!"}"""
)
fun main () {
println(value.get ("key" ))
}
> ./JBangEx.kt
[jbang] Downloading Kotlin 1.8.22. Be patient, this can take several minutes...
[jbang] Installing Kotlin 1.8.22...
[jbang] Resolving dependencies...
[jbang] com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0
[jbang] Dependencies resolved
[jbang] Building jar for JBangEx.kt...
Hello, World!
KScript
#!/usr/bin/env kscript
@file:DependsOn ("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0" )
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
val value = jacksonObjectMapper()
.readValue<Map<String, String>>("""{"key":"Hello, World!"}""" )
println(value["key" ])
> ./kscript.kts
[kscript] Resolving com.fasterxml.jackson.module:jackson-module-kotlin:2.17.0...
Hello, World!
Замена BASH-скриптов в автоматизации задач
Kotlin Script
Скрипты .kts
#!/usr/bin/env kotlin
import java.io.File
fun complexProcess (text: String ) : String = TODO()
val file = File("path/to/my.file" )
val text = file.readText()
val newText = complexProcess(text)
file.writeText(newText)
Но встроенных в Kotlin библиотек недостаточно (по сравнению с тем же Python).
Тогда на помощь приходят скрипты .main.kts, куда можно добавить любые JVM библиотеки
Скрипты .main.kts
Подключение репозиториев и библиотек
Конфигурация комплятора в самом скрипте
Кэширование между запусками
Поддержка в IDE "из коробки"
Скрипты .main.kts
#!/usr/bin/env kotlin
@file:DependsOn ("org.jetbrains.kotlinx:kotlinx-cli-jvm:0.3.6" )
import kotlinx.cli.*;
import java.io.File;
val parser = ArgParser("copyIndexed" )
val input by parser.option(ArgType.String, shortName = "i" ,
description = "Input file" ).required()
parser.parse(args)
val file = File(input)
file.useLines {
it.mapIndexed { index, line -> "$index - $line " }
.forEach { File("${file.name} -copy" ).appendText(it) }
}
#!/usr/bin/env kotlin
@file:DependsOn ("org.jetbrains.kotlinx:kotlinx-cli-jvm:0.3.6" )
import kotlinx.cli.*;
import java.io.File;
val parser = ArgParser("copyIndexed" )
val input by parser.option(ArgType.String, shortName = "i" ,
description = "Input file" ).required()
parser.parse(args)
val file = File(input)
file.useLines {
it.mapIndexed { index, line -> "$index - $line " }
.forEach { File("${file.name} -copy" ).appendText(it) }
}
#!/usr/bin/env kotlin
@file:DependsOn ("org.jetbrains.kotlinx:kotlinx-cli-jvm:0.3.6" )
import kotlinx.cli.*;
import java.io.File;
val parser = ArgParser("copyIndexed" )
val input by parser.option(ArgType.String, shortName = "i" ,
description = "Input file" ).required()
parser.parse(args)
val file = File(input)
file.useLines {
it.mapIndexed { index, line -> "$index - $line " }
.forEach { File("${file.name} -copy" ).appendText(it) }
}
#!/usr/bin/env kotlin
@file:DependsOn ("org.jetbrains.kotlinx:kotlinx-cli-jvm:0.3.6" )
import kotlinx.cli.*;
import java.io.File;
val parser = ArgParser("copyIndexed" )
val input by parser.option(ArgType.String, shortName = "i" ,
description = "Input file" ).required()
parser.parse(args)
val file = File(input)
file.useLines {
it.mapIndexed { index, line -> "$index - $line " }
.forEach { File("${file.name} -copy" ).appendText(it) }
}
Скрипты .main.kts
> ./test.main.kts
Value for option --input should be always provided in command line.
Usage: example options_list
Options:
--input, -i -> Input file (always required) { String }
--debug, -d [false ] -> Turn on debug mode
--help , -h -> Usage info
Script Definition
Базовый пример
@KotlinScript(
fileExtension = "gitlab-ci.kts" ,
displayName = "Gitlab CI Kotlin configuration" ,
)
abstract class GitlabCiKtScript
@KotlinScript(
fileExtension = "gitlab-ci.kts" ,
displayName = "Gitlab CI Kotlin configuration" ,
)
abstract class GitlabCiKtScript
@KotlinScript(
fileExtension = "gitlab-ci.kts" ,
displayName = "Gitlab CI Kotlin configuration" ,
)
abstract class GitlabCiKtScript
@KotlinScript(
fileExtension = "gitlab-ci.kts" ,
displayName = "Gitlab CI Kotlin configuration" ,
)
abstract class GitlabCiKtScript
@KotlinScript(
fileExtension = "gitlab-ci.kts" ,
displayName = "Gitlab CI Kotlin configuration" ,
)
abstract class GitlabCiKtScript
Базовый пример
@KotlinScript(
fileExtension = "gitlab-ci.kts" ,
displayName = "Gitlab CI Kotlin configuration" ,
)
abstract class GitlabCiKtScript {
val myProperty = 123
}
Базовый пример
println(myProperty)
Конфигурация компиляции
Зависимости
Импорты по умолчанию
Конфигурация IDE
Параметры компилятора Kotlin
Доступные в скрипте свойства
Определение неявных (implicit) ресиверов
Конфигурация компиляции
Пример
object GitlabCiKtScriptCompilationConfiguration : ScriptCompilationConfiguration({
jvm {
dependenciesFromClassContext(PipelineBuilder::class , wholeClasspath = true )
}
defaultImports(
"dev.otbe.gitlab.ci.core.model.*" ,
"dev.otbe.gitlab.ci.dsl.*" ,
"dev.otbe.gitlab.ci.core.goesTo"
)
ide { acceptedLocations(ScriptAcceptedLocation.Everywhere) }
compilerOptions.append("-Xcontext-receivers" )
providedProperties("propName" to String::class )
implicitReceivers(PipelineBuilder::class )
})
object GitlabCiKtScriptCompilationConfiguration : ScriptCompilationConfiguration({
jvm {
dependenciesFromClassContext(PipelineBuilder::class , wholeClasspath = true )
}
defaultImports(
"dev.otbe.gitlab.ci.core.model.*" ,
"dev.otbe.gitlab.ci.dsl.*" ,
"dev.otbe.gitlab.ci.core.goesTo"
)
ide { acceptedLocations(ScriptAcceptedLocation.Everywhere) }
compilerOptions.append("-Xcontext-receivers" )
providedProperties("propName" to String::class )
implicitReceivers(PipelineBuilder::class )
})
object GitlabCiKtScriptCompilationConfiguration : ScriptCompilationConfiguration({
jvm {
dependenciesFromClassContext(PipelineBuilder::class , wholeClasspath = true )
}
defaultImports(
"dev.otbe.gitlab.ci.core.model.*" ,
"dev.otbe.gitlab.ci.dsl.*" ,
"dev.otbe.gitlab.ci.core.goesTo"
)
ide { acceptedLocations(ScriptAcceptedLocation.Everywhere) }
compilerOptions.append("-Xcontext-receivers" )
providedProperties("propName" to String::class )
implicitReceivers(PipelineBuilder::class )
})
object GitlabCiKtScriptCompilationConfiguration : ScriptCompilationConfiguration({
jvm {
dependenciesFromClassContext(PipelineBuilder::class , wholeClasspath = true )
}
defaultImports(
"dev.otbe.gitlab.ci.core.model.*" ,
"dev.otbe.gitlab.ci.dsl.*" ,
"dev.otbe.gitlab.ci.core.goesTo"
)
ide { acceptedLocations(ScriptAcceptedLocation.Everywhere) }
compilerOptions.append("-Xcontext-receivers" )
providedProperties("propName" to String::class )
implicitReceivers(PipelineBuilder::class )
})
object GitlabCiKtScriptCompilationConfiguration : ScriptCompilationConfiguration({
jvm {
dependenciesFromClassContext(PipelineBuilder::class , wholeClasspath = true )
}
defaultImports(
"dev.otbe.gitlab.ci.core.model.*" ,
"dev.otbe.gitlab.ci.dsl.*" ,
"dev.otbe.gitlab.ci.core.goesTo"
)
ide { acceptedLocations(ScriptAcceptedLocation.Everywhere) }
compilerOptions.append("-Xcontext-receivers" )
providedProperties("propName" to String::class )
implicitReceivers(PipelineBuilder::class )
})
object GitlabCiKtScriptCompilationConfiguration : ScriptCompilationConfiguration({
jvm {
dependenciesFromClassContext(PipelineBuilder::class , wholeClasspath = true )
}
defaultImports(
"dev.otbe.gitlab.ci.core.model.*" ,
"dev.otbe.gitlab.ci.dsl.*" ,
"dev.otbe.gitlab.ci.core.goesTo"
)
ide { acceptedLocations(ScriptAcceptedLocation.Everywhere) }
compilerOptions.append("-Xcontext-receivers" )
providedProperties("propName" to String::class )
implicitReceivers(PipelineBuilder::class )
})
object GitlabCiKtScriptCompilationConfiguration : ScriptCompilationConfiguration({
jvm {
dependenciesFromClassContext(PipelineBuilder::class , wholeClasspath = true )
}
defaultImports(
"dev.otbe.gitlab.ci.core.model.*" ,
"dev.otbe.gitlab.ci.dsl.*" ,
"dev.otbe.gitlab.ci.core.goesTo"
)
ide { acceptedLocations(ScriptAcceptedLocation.Everywhere) }
compilerOptions.append("-Xcontext-receivers" )
providedProperties("propName" to String::class )
implicitReceivers(PipelineBuilder::class )
})
Конфигурация компиляции
Пример
PipelineBuilder.kt
class PipelineBuilder {
var stages: List<Stage> = mutableListOf()
fun stages (vararg stage: String ) {
stages += stage.asList().map { Stage(it) }
}
}
example.gitlab-ci.kts
stages("build" , "deploy" )
Конфигурация компиляции
Пример
@KotlinScript(
fileExtension = "gitlab-ci.kts" ,
displayName = "Gitlab CI Kotlin configuration" ,
compilationConfiguration = GitlabCiKtScriptCompilationConfiguration::class,
)
abstract class GitlabCiKtScript
Конфигурация компиляции
Внешние зависимости
object GitlabCiKtScriptCompilationConfiguration : ScriptCompilationConfiguration({
defaultImports(DependsOn::class , Repository::class )
refineConfiguration {
onAnnotations(
DependsOn::class ,
Repository::class ,
handler = ::configureMavenDepsOnAnnotations
)
}
jvm {
dependenciesFromClassContext(PipelineBuilder::class , wholeClasspath = true )
}
defaultImports(
"dev.otbe.gitlab.ci.core.model.*" ,
"dev.otbe.gitlab.ci.dsl.*" ,
"dev.otbe.gitlab.ci.core.goesTo"
)
ide { acceptedLocations(ScriptAcceptedLocation.Everywhere) }
compilerOptions.append("-Xcontext-receivers" )
providedProperties("propName" to String::class )
implicitReceivers(PipelineBuilder::class )
}
Конфигурация компиляции
Внешние зависимости
example.gitlab-ci.kts
@file:Repository ("https://private-nexus.company.org/ci-templates/" )
@file:DependsOn ("org.company.ci.templates:jvm-jobs:1.0.0" )
import org.company.ci.templates.jvm.jobs.*
val appName = "kotlin-app"
gradleJob {
task = "build"
artifact = "./build/libs/${appName} .jar"
}