Kotlin - Scripting
Overview
Kotlin scripting allows you to use Kotlin for quick automation tasks, build scripts, and command-line utilities. This tutorial covers .kts files, scripting APIs, integration with build tools, and practical automation examples.
๐ฏ Learning Objectives:
- Understand Kotlin scripting fundamentals and .kts files
- Learn command-line scripting with kotlinc
- Master build script creation with Gradle Kotlin DSL
- Apply scripting for automation and DevOps tasks
- Create custom scripting solutions and integrations
Getting Started with Kotlin Scripts
What is Kotlin Scripting?
Kotlin scripting allows you to execute Kotlin code directly without the traditional compile-then-run cycle. Scripts use the .kts extension and can be run immediately.
Basic Script Structure
#!/usr/bin/env kotlin
// hello.kts
println("Hello, Kotlin Scripting!")
// Variables and functions work normally
val name = "World"
fun greet(name: String) = "Hello, $name!"
println(greet(name))
// Access command line arguments
if (args.isNotEmpty()) {
println("Arguments: ${args.joinToString()}")
}
Running Kotlin Scripts
# Using kotlinc
kotlinc -script hello.kts
# With arguments
kotlinc -script hello.kts arg1 arg2 arg3
# Using kotlin command (if available)
kotlin hello.kts
# Make script executable (Unix/Linux/Mac)
chmod +x hello.kts
./hello.kts
Command-Line Scripting
File Processing Script
#!/usr/bin/env kotlin
// file_processor.kts
import java.io.File
fun processFile(filename: String) {
val file = File(filename)
if (!file.exists()) {
println("File not found: $filename")
return
}
val lines = file.readLines()
val wordCount = lines.sumOf { it.split("\\s+".toRegex()).size }
val charCount = lines.sumOf { it.length }
println("File: $filename")
println("Lines: ${lines.size}")
println("Words: $wordCount")
println("Characters: $charCount")
println("Average words per line: ${wordCount.toDouble() / lines.size}")
}
// Process command line arguments
if (args.isEmpty()) {
println("Usage: kotlin file_processor.kts ")
} else {
args.forEach { processFile(it) }
}
System Information Script
#!/usr/bin/env kotlin
// sysinfo.kts
import java.lang.management.ManagementFactory
import java.text.SimpleDateFormat
import java.util.*
fun getSystemInfo() {
val runtime = Runtime.getRuntime()
val mb = 1024 * 1024
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
println("=== System Information ===")
println("Current time: ${dateFormat.format(Date())}")
println("OS: ${System.getProperty("os.name")} ${System.getProperty("os.version")}")
println("Architecture: ${System.getProperty("os.arch")}")
println("Java version: ${System.getProperty("java.version")}")
println("User: ${System.getProperty("user.name")}")
println("Working directory: ${System.getProperty("user.dir")}")
println()
println("=== Memory Information ===")
println("Total memory: ${runtime.totalMemory() / mb} MB")
println("Free memory: ${runtime.freeMemory() / mb} MB")
println("Used memory: ${(runtime.totalMemory() - runtime.freeMemory()) / mb} MB")
println("Max memory: ${runtime.maxMemory() / mb} MB")
println()
println("=== CPU Information ===")
println("Available processors: ${runtime.availableProcessors()}")
val threadBean = ManagementFactory.getThreadMXBean()
println("Thread count: ${threadBean.threadCount}")
println("Peak thread count: ${threadBean.peakThreadCount}")
}
getSystemInfo()
Dependency Management in Scripts
#!/usr/bin/env kotlin
// Using @file:DependsOn for external dependencies
@file:DependsOn("com.squareup.okhttp3:okhttp:4.11.0")
@file:DependsOn("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
import okhttp3.*
import com.fasterxml.jackson.module.kotlin.*
import java.io.IOException
// weather.kts - Fetch weather data
val client = OkHttpClient()
val mapper = jacksonObjectMapper()
data class WeatherData(
val name: String,
val main: Map,
val weather: List
Build Script Automation
Gradle Kotlin DSL Scripts
// build.gradle.kts
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.9.10"
application
id("com.github.johnrengelman.shadow") version "8.1.1"
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
testImplementation(kotlin("test"))
}
application {
mainClass.set("com.example.MainKt")
}
// Custom task
tasks.register("generateBuildInfo") {
description = "Generates build information"
group = "build"
val outputFile = file("$buildDir/resources/main/build.properties")
outputs.file(outputFile)
doLast {
outputFile.parentFile.mkdirs()
outputFile.writeText("""
version=${project.version}
buildTime=${System.currentTimeMillis()}
buildBy=${System.getProperty("user.name")}
""".trimIndent())
}
}
tasks.processResources {
dependsOn("generateBuildInfo")
}
tasks.withType {
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs = listOf("-Xjsr305=strict")
}
}
tasks.test {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
exceptionFormat = TestExceptionFormat.FULL
}
}
Custom Gradle Plugin Script
// buildSrc/src/main/kotlin/project-conventions.gradle.kts
plugins {
kotlin("jvm")
}
repositories {
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
testImplementation(kotlin("test"))
}
tasks.withType {
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs = listOf("-Xjsr305=strict")
}
}
tasks.test {
useJUnitPlatform()
}
// Custom extension
open class ProjectExtension {
var customProperty: String = "default"
fun customConfiguration(action: Action) {
val config = CustomConfig()
action.execute(config)
println("Custom configuration applied: ${config.value}")
}
}
class CustomConfig {
var value: String = ""
}
DevOps and Automation Scripts
Deployment Script
#!/usr/bin/env kotlin
// deploy.kts
import java.io.File
import java.util.concurrent.TimeUnit
data class DeployConfig(
val environment: String,
val serverHost: String,
val deployPath: String,
val serviceName: String
)
class DeploymentScript {
private val configs = mapOf(
"staging" to DeployConfig("staging", "staging.example.com", "/opt/myapp", "myapp-staging"),
"production" to DeployConfig("production", "prod.example.com", "/opt/myapp", "myapp-prod")
)
fun deploy(environment: String) {
val config = configs[environment] ?: throw IllegalArgumentException("Unknown environment: $environment")
println("Starting deployment to $environment...")
// Build the application
executeCommand("./gradlew clean build")
// Create deployment package
val jarFile = File("build/libs").listFiles()?.find { it.name.endsWith(".jar") }
?: throw RuntimeException("JAR file not found")
// Copy to server
executeCommand("scp ${jarFile.absolutePath} ${config.serverHost}:${config.deployPath}/")
// Restart service
executeCommand("ssh ${config.serverHost} 'sudo systemctl restart ${config.serviceName}'")
// Verify deployment
Thread.sleep(5000) // Wait for service to start
val status = executeCommand("ssh ${config.serverHost} 'curl -f http://localhost:8080/health'")
if (status == 0) {
println("โ
Deployment successful!")
} else {
println("โ Deployment failed - health check failed")
}
}
private fun executeCommand(command: String): Int {
println("Executing: $command")
val process = ProcessBuilder()
.command(command.split(" "))
.inheritIO()
.start()
return if (process.waitFor(60, TimeUnit.SECONDS)) {
process.exitValue()
} else {
process.destroyForcibly()
-1
}
}
}
// Main execution
if (args.isEmpty()) {
println("Usage: kotlin deploy.kts ")
println("Available environments: staging, production")
} else {
try {
DeploymentScript().deploy(args[0])
} catch (e: Exception) {
println("Deployment failed: ${e.message}")
kotlin.system.exitProcess(1)
}
}
Database Migration Script
#!/usr/bin/env kotlin
@file:DependsOn("mysql:mysql-connector-java:8.0.33")
// migrate.kts
import java.sql.DriverManager
import java.io.File
data class Migration(
val version: Int,
val description: String,
val sql: String
)
class DatabaseMigrator(
private val jdbcUrl: String,
private val username: String,
private val password: String
) {
fun migrate() {
val connection = DriverManager.getConnection(jdbcUrl, username, password)
// Create migrations table if it doesn't exist
connection.createStatement().execute("""
CREATE TABLE IF NOT EXISTS schema_migrations (
version INT PRIMARY KEY,
description VARCHAR(255),
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
// Get applied migrations
val appliedVersions = mutableSetOf()
val result = connection.createStatement().executeQuery("SELECT version FROM schema_migrations")
while (result.next()) {
appliedVersions.add(result.getInt("version"))
}
// Load and apply pending migrations
val migrations = loadMigrations()
val pendingMigrations = migrations.filter { it.version !in appliedVersions }
.sortedBy { it.version }
if (pendingMigrations.isEmpty()) {
println("No pending migrations")
return
}
connection.autoCommit = false
try {
pendingMigrations.forEach { migration ->
println("Applying migration ${migration.version}: ${migration.description}")
// Execute migration SQL
connection.createStatement().execute(migration.sql)
// Record migration
val stmt = connection.prepareStatement(
"INSERT INTO schema_migrations (version, description) VALUES (?, ?)"
)
stmt.setInt(1, migration.version)
stmt.setString(2, migration.description)
stmt.executeUpdate()
}
connection.commit()
println("โ
Applied ${pendingMigrations.size} migrations successfully")
} catch (e: Exception) {
connection.rollback()
println("โ Migration failed: ${e.message}")
throw e
} finally {
connection.close()
}
}
private fun loadMigrations(): List {
val migrationsDir = File("migrations")
if (!migrationsDir.exists()) {
println("Migrations directory not found")
return emptyList()
}
return migrationsDir.listFiles { file -> file.name.endsWith(".sql") }
?.mapNotNull { file ->
val parts = file.nameWithoutExtension.split("_", limit = 2)
if (parts.size >= 2) {
val version = parts[0].toIntOrNull()
val description = parts[1].replace("_", " ")
if (version != null) {
Migration(version, description, file.readText())
} else null
} else null
}
?.sortedBy { it.version }
?: emptyList()
}
}
// Usage
if (args.size < 3) {
println("Usage: kotlin migrate.kts ")
println("Example: kotlin migrate.kts 'jdbc:mysql://localhost:3306/mydb' user pass")
} else {
DatabaseMigrator(args[0], args[1], args[2]).migrate()
}
Testing and Monitoring Scripts
Load Testing Script
#!/usr/bin/env kotlin
@file:DependsOn("com.squareup.okhttp3:okhttp:4.11.0")
import okhttp3.*
import kotlinx.coroutines.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import kotlin.system.measureTimeMillis
// loadtest.kts
class LoadTester(
private val baseUrl: String,
private val concurrency: Int = 10,
private val requests: Int = 100
) {
private val client = OkHttpClient()
private val successCount = AtomicInteger(0)
private val errorCount = AtomicInteger(0)
private val totalResponseTime = AtomicLong(0)
suspend fun runTest() = coroutineScope {
println("Starting load test...")
println("URL: $baseUrl")
println("Concurrency: $concurrency")
println("Total requests: $requests")
println("=" * 50)
val startTime = System.currentTimeMillis()
// Create semaphore to limit concurrency
val semaphore = kotlinx.coroutines.sync.Semaphore(concurrency)
// Launch all requests
val jobs = (1..requests).map { requestId ->
async {
semaphore.withPermit {
makeRequest(requestId)
}
}
}
// Wait for all requests to complete
jobs.awaitAll()
val endTime = System.currentTimeMillis()
val totalTime = endTime - startTime
printResults(totalTime)
}
private suspend fun makeRequest(requestId: Int) {
val request = Request.Builder()
.url(baseUrl)
.build()
val responseTime = measureTimeMillis {
try {
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
successCount.incrementAndGet()
} else {
errorCount.incrementAndGet()
println("Request $requestId failed: ${response.code}")
}
}
} catch (e: Exception) {
errorCount.incrementAndGet()
println("Request $requestId error: ${e.message}")
}
}
totalResponseTime.addAndGet(responseTime)
}
private fun printResults(totalTime: Long) {
val successRate = (successCount.get().toDouble() / requests) * 100
val avgResponseTime = totalResponseTime.get().toDouble() / requests
val requestsPerSecond = (requests.toDouble() / totalTime) * 1000
println("\n" + "=" * 50)
println("RESULTS")
println("=" * 50)
println("Total time: ${totalTime}ms")
println("Total requests: $requests")
println("Successful requests: ${successCount.get()}")
println("Failed requests: ${errorCount.get()}")
println("Success rate: ${"%.2f".format(successRate)}%")
println("Average response time: ${"%.2f".format(avgResponseTime)}ms")
println("Requests per second: ${"%.2f".format(requestsPerSecond)}")
}
}
// Main execution
runBlocking {
if (args.isEmpty()) {
println("Usage: kotlin loadtest.kts [concurrency] [requests]")
println("Example: kotlin loadtest.kts http://localhost:8080/health 5 50")
} else {
val url = args[0]
val concurrency = args.getOrNull(1)?.toIntOrNull() ?: 10
val requests = args.getOrNull(2)?.toIntOrNull() ?: 100
LoadTester(url, concurrency, requests).runTest()
}
}
Log Analysis Script
#!/usr/bin/env kotlin
// loganalyzer.kts
import java.io.File
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.regex.Pattern
data class LogEntry(
val timestamp: LocalDateTime,
val level: String,
val message: String,
val source: String?
)
class LogAnalyzer {
private val logPattern = Pattern.compile(
"^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3})\\s+(\\w+)\\s+(.+)$"
)
private val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
fun analyzeLogFile(filename: String) {
val file = File(filename)
if (!file.exists()) {
println("Log file not found: $filename")
return
}
val entries = file.readLines().mapNotNull { parseLogEntry(it) }
if (entries.isEmpty()) {
println("No valid log entries found")
return
}
println("=== Log Analysis for $filename ===")
println("Total entries: ${entries.size}")
println()
// Analyze by level
val levelCounts = entries.groupingBy { it.level }.eachCount()
println("Entries by level:")
levelCounts.toSortedMap().forEach { (level, count) ->
val percentage = (count.toDouble() / entries.size) * 100
println(" $level: $count (${"%.1f".format(percentage)}%)")
}
println()
// Find time range
val startTime = entries.minByOrNull { it.timestamp }?.timestamp
val endTime = entries.maxByOrNull { it.timestamp }?.timestamp
println("Time range: $startTime to $endTime")
println()
// Error analysis
val errors = entries.filter { it.level == "ERROR" }
if (errors.isNotEmpty()) {
println("Error messages:")
errors.take(10).forEach { error ->
println(" [${error.timestamp}] ${error.message.take(100)}")
}
if (errors.size > 10) {
println(" ... and ${errors.size - 10} more errors")
}
println()
}
// Activity by hour
val hourlyActivity = entries.groupingBy { it.timestamp.hour }.eachCount()
println("Activity by hour:")
(0..23).forEach { hour ->
val count = hourlyActivity[hour] ?: 0
val bar = "โ".repeat(count / 10)
println(" %02d:00 %4d %s".format(hour, count, bar))
}
}
private fun parseLogEntry(line: String): LogEntry? {
val matcher = logPattern.matcher(line)
return if (matcher.matches()) {
try {
LogEntry(
timestamp = LocalDateTime.parse(matcher.group(1), dateFormatter),
level = matcher.group(2),
message = matcher.group(3),
source = null
)
} catch (e: Exception) {
null
}
} else null
}
}
// Usage
if (args.isEmpty()) {
println("Usage: kotlin loganalyzer.kts ")
} else {
args.forEach { LogAnalyzer().analyzeLogFile(it) }
}
Advanced Scripting Techniques
Configuration Management
#!/usr/bin/env kotlin
// config.kts
import java.io.File
import java.util.Properties
class ConfigManager(private val configFile: String) {
private val properties = Properties()
init {
loadConfig()
}
private fun loadConfig() {
val file = File(configFile)
if (file.exists()) {
file.inputStream().use { properties.load(it) }
}
}
fun get(key: String, default: String = ""): String {
return properties.getProperty(key, default)
}
fun getInt(key: String, default: Int = 0): Int {
return properties.getProperty(key)?.toIntOrNull() ?: default
}
fun getBoolean(key: String, default: Boolean = false): Boolean {
return properties.getProperty(key)?.toBoolean() ?: default
}
fun set(key: String, value: String) {
properties.setProperty(key, value)
saveConfig()
}
private fun saveConfig() {
File(configFile).outputStream().use {
properties.store(it, "Configuration file")
}
}
fun listAll() {
properties.forEach { key, value ->
println("$key = $value")
}
}
}
// Usage
val config = ConfigManager("app.properties")
when (args.getOrNull(0)) {
"get" -> {
if (args.size > 1) {
println(config.get(args[1], "Not found"))
} else {
println("Usage: kotlin config.kts get ")
}
}
"set" -> {
if (args.size > 2) {
config.set(args[1], args[2])
println("Set ${args[1]} = ${args[2]}")
} else {
println("Usage: kotlin config.kts set ")
}
}
"list" -> config.listAll()
else -> {
println("Usage: kotlin config.kts ")
println("Commands: get , set , list")
}
}
Template Generation
#!/usr/bin/env kotlin
// codegen.kts
import java.io.File
data class ClassTemplate(
val packageName: String,
val className: String,
val properties: List
)
data class Property(
val name: String,
val type: String,
val nullable: Boolean = false
)
class CodeGenerator {
fun generateDataClass(template: ClassTemplate): String {
val props = template.properties.joinToString(",\n ") { prop ->
val type = if (prop.nullable) "${prop.type}?" else prop.type
"val ${prop.name}: $type"
}
return """
package ${template.packageName}
/**
* Generated data class: ${template.className}
*/
data class ${template.className}(
$props
) {
companion object {
fun empty() = ${template.className}(
${template.properties.joinToString(",\n ") { prop ->
when (prop.type) {
"String" -> "${prop.name} = \"\""
"Int" -> "${prop.name} = 0"
"Boolean" -> "${prop.name} = false"
"Double" -> "${prop.name} = 0.0"
else -> "${prop.name} = null"
}
}}
)
}
}
""".trimIndent()
}
fun generateRepository(className: String, entityName: String): String {
return """
package com.example.repository
import com.example.entity.$entityName
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface ${className}Repository : JpaRepository<$entityName, Long> {
fun findByName(name: String): List<$entityName>
fun findByNameContaining(name: String): List<$entityName>
}
""".trimIndent()
}
}
// Usage
if (args.size < 2) {
println("Usage: kotlin codegen.kts [properties...]")
println("Types: dataclass, repository")
println("Example: kotlin codegen.kts dataclass User name:String age:Int email:String?")
} else {
val generator = CodeGenerator()
when (args[0]) {
"dataclass" -> {
val className = args[1]
val properties = args.drop(2).map { propStr ->
val parts = propStr.split(":")
if (parts.size >= 2) {
val name = parts[0]
val type = parts[1].removeSuffix("?")
val nullable = parts[1].endsWith("?")
Property(name, type, nullable)
} else {
Property(propStr, "String")
}
}
val template = ClassTemplate("com.example.model", className, properties)
val code = generator.generateDataClass(template)
val outputFile = File("${className}.kt")
outputFile.writeText(code)
println("Generated data class: ${outputFile.absolutePath}")
}
"repository" -> {
val entityName = args[1]
val code = generator.generateRepository(entityName, entityName)
val outputFile = File("${entityName}Repository.kt")
outputFile.writeText(code)
println("Generated repository: ${outputFile.absolutePath}")
}
else -> println("Unknown type: ${args[0]}")
}
}
Best Practices for Kotlin Scripting
Script Structure and Organization
- Use shebang: Add #!/usr/bin/env kotlin for executable scripts
- Organize dependencies: Use @file:DependsOn for external libraries
- Handle arguments: Always validate command-line arguments
- Error handling: Use try-catch blocks for robust scripts
- Documentation: Add comments explaining script purpose and usage
Performance Considerations
#!/usr/bin/env kotlin
// Efficient scripting practices
// โ
Use lazy initialization
val expensiveResource by lazy {
println("Initializing expensive resource...")
// Expensive computation here
"Resource initialized"
}
// โ
Use sequences for large data processing
fun processLargeDataset(data: List) {
data.asSequence()
.filter { it.isNotBlank() }
.map { it.trim().uppercase() }
.take(100)
.toList()
}
// โ
Cache results when possible
val cache = mutableMapOf()
fun cachedOperation(input: String): String {
return cache.getOrPut(input) {
// Expensive operation
input.reversed()
}
}
// โ
Use appropriate data structures
val frequentLookups = setOf("item1", "item2", "item3")
val orderedData = linkedMapOf()
println("Script optimization examples loaded")
Integration with IDEs and Tools
IntelliJ IDEA Configuration
// .idea/kotlinScripting.xml configuration for custom script templates
// Custom script template
// File > Settings > Editor > File and Code Templates
// Create new template: Kotlin Script (.kts)
#!/usr/bin/env kotlin
/**
* ${NAME}
*
* Description: ${DESCRIPTION}
* Author: ${USER}
* Date: ${DATE}
*/
fun main() {
println("Script: ${NAME}")
// Your code here
}
if (args.isNotEmpty()) {
main()
} else {
println("Usage: kotlin ${NAME}.kts")
}
Key Takeaways
- Kotlin scripting enables quick automation and tooling development
- .kts files can be executed directly without compilation
- External dependencies can be declared with @file:DependsOn
- Scripts are perfect for DevOps, build automation, and system administration
- Gradle Kotlin DSL uses scripting for type-safe build configuration
- Proper error handling and argument validation make scripts robust
Practice Exercises
- Create a script that monitors system resources and sends alerts
- Build a deployment automation script with rollback capabilities
- Write a code generation script for REST API boilerplate
- Develop a log aggregation and analysis tool
Quiz
- What's the difference between .kt and .kts files?
- How do you add external dependencies to Kotlin scripts?
- When should you prefer scripting over regular Kotlin applications?
Show Answers
- .kt files are regular Kotlin source files that need compilation, while .kts files are Kotlin scripts that can be executed directly.
- Use @file:DependsOn("group:artifact:version") annotations at the top of the script file.
- Use scripting for automation tasks, build scripts, one-time utilities, quick prototypes, and when you need immediate execution without compilation.