post-thumb

Scripting with Kotlin

Kotlin is a modern and versatile programming language that is used for many targets and types of applications. Although it is a compiled language at its core and often used with Gradle and Maven build systems, Kotlin supports scripting, which enables small programs or scripts to be written and executed directly, providing a developer experience (DX) similar to other scripting languages (such as Linux shell scripts and Python). This means, that the developer can write and run Kotlin code without the need for a build system or complex setup. In this article, three ways of scripting with Kotlin are presented: using the *.main.kts file, using kscript, and using JBang. Kotlin notebooks are also mentioned as a bonus at the end.

Scripting with *.main.kts

The official way to write Kotlin scripts is with a *.main.kts file, which is a Kotlin file with some exclusive syntax that can be executed directly. Such a file contains a shebang (the first line that starts with #! and tells the system which interpreter to use), some optional attributes and Kotlin code. It can be run using the kotlin command. For example, a script.main.kts file may be created with the following content:

#!/usr/bin/env kotlin

println("Hello, Kotlin scripting!")

We note that there is no main function in this script, which is a key difference from regular Kotlin files. The script can be executed using the following command:

kotlin script.main.kts

“Hello, Kotlin scripting!” is printed to the console. Thanks to the shebang line (the first line of the file), the script may also be made executable by running:

chmod +x script.main.kts

and then it can be executed like a regular shell script:

./script.main.kts

If the error env: kotlin\r: No such file or directory is encountered, then changing the line endings of the file to Unix format may fix the issue. This can be done by setting end_of_line = lf in the .editorconfig file or by running the following command:

sed -i 's/\r$//' script.main.kts

Kotlin scripts with the *.main.kts extension support features that enhance their functionality. For example, the @file:DependsOn attribute allows dependencies to be included in the script, and the implicit args variable provides access to command line arguments passed to the script. These two features are highlighted in the following example that prints the last 10 posts from a given RSS feed. In this example, the @file:DependsOn attribute is used to include dependencies using their Maven coordinates with the following syntax @file:DependsOn("groupId:artifactId:version"). The URL of the RSS feed is passed as a command line argument and is retrieved in the script using the implicit args array which is automatically created and populated by the Kotlin scripting runtime starting from index 0 (the first argument passed to the command line is args[0]).

#!/usr/bin/env kotlin

@file:DependsOn("com.apptasticsoftware:rssreader:3.9.3")

import com.apptasticsoftware.rssreader.RssReader
import kotlin.jvm.optionals.getOrDefault

# The first argument of the command line is at index 0
val feedUrl = args[0]
println("Showing the last 10 posts from $feedUrl")

var rssReader = RssReader()
var items = rssReader.read(feedUrl).filter { it.title.isPresent }
  .map { it.title.getOrDefault("") }.toList().take(10)
println(items.joinToString(separator = "\n- ", prefix = "- "))

The script may be executed with the following command:

kotlin rssreader.main.kts [feed URL]
# For example:
kotlin rssreader.main.kts "https://blog.worldline.tech/index.xml"

The output of the script is similar to the following:

Showing the last 10 posts from https://blog.worldline.tech/index.xml
- The Yoga of Image Generation – Part 3
- Insights from Onboarding young developers and Mentoring Experiences
- The Yoga of Image Generation – Part 2
- The Superpowers of JavaScript Proxies
- Devops on Google Cloud Platform: Automating your build delivery on GCP in nutshell
- Introduction to QEMU 386
- The Yoga of Image Generation – Part 1
- Proper key management in the cloud with a Cloud Secure Module
- The OAuth proxification guide for frontend developers with Microsoft EntraID
- Gemini, but the other one

There are other features that enhance the scripting experience with *.main.kts files. It is possible to debug scripts using the IntelliJ IDEA debugger, which allows breakpoints to be set and variables to be inspected. In order to debug a script, a run configuration must be created in IntelliJ IDEA. This can be done by clicking on the “Add Configuration” button in the top right corner of the IDE, selecting “Kotlin Script (Beta)”, and then specifying the script file to debug as the value of the command line argument as illustrated in the following screenshot.

Add Kotlin script run config Kotlin script run config

After setting up the run configuration, the script can be debugged by adding breakpoints and by clicking on the “Debug” button of the IDE.

Kotlin script run config

Additionally, the @file:Repository attribute can be used to specify a custom Maven repository from which dependencies should be downloaded. This is useful when the dependencies are not available in the default Maven repositories. Finally, caching support for fast execution is available by default.

Kotlin scripting with *.main.kts files is a straightforward way to write and execute Kotlin scripts. This approach is available out of the box with the Kotlin compiler. However, there are two major limitations to be aware of:

  • Only IntelliJ IDEA currently provides good IDE support for *.main.kts files (syntax highlighting, code completion, debugging). VSCode has limited support with the unofficial Kotlin plugin, which does not support debugging or code completion and the official Kotlin Language Server Protocol (LSP) is still in its early days.
  • The status of scripting with *.main.kts files is still experimental, so changes may occur in the future.

For further information, JetBrains has published a blog post about the current state of Kotlin scripting by the end of 2024 , which initially led to some doubts and misunderstandings about the future of official Kotlin scripting support. Ultimately, the feedback was related to other less successful and relevant scripting features being dropped. Additional perspectives on the current state of official Kotlin scripting can be found in InfoWorld and Martin Bonin posts.

In addition to the official *.main.kts scripting, there are two other popular ways to write Kotlin scripts: using kscript and JBang.

Scripting with kscript

kscript is an open-source tool designed to provide a user-friendly experience for writing Kotlin scripts. It offers a command-line interface and features that simplify the process of writing and running Kotlin scripts.

After installing kscript , a script file with the .kts extension can be created, and the shebang line added at the top of the file. For example, the following file, named script.kts, is a kscript file that prints “Hello, Kotlin scripting!” and takes a command line argument:

#!/usr/bin/env kscript

println("Hello, Kotlin scripting! ${args[0]}")

As with *.main.kts scripting, the script can be run using the kscript command:

kscript script.kts "from kscript"

The script may also be made executable by running:

chmod +x script.kts

and then run with the following command:

./script.kts "from kscript"

Features similar to the official *.main.kts scripting, such as dependency management and command line arguments, are provided by kscript. Additional developer-friendly features are also available, such as passing code from the command line. The following commands demonstrate two examples of passing code from the command line to kscript:

kscript 'println("hello world")'
echo 'println("Hello Kotlin.")' |  kscript -

kscript was more advanced compared to the official Kotlin scripting support when it was in its early stages. However, many exclusive features of kscript have since been integrated into the official *.main.kts scripting, such as dependency management, command line arguments, and more. Additionally, kscript has not been updated for a long time, with the last release in July 2023.

A more feature-rich third-party Kotlin scripting experience can be achieved with JBang, as described in the next section.

Scripting with JBang

JBang is a multipurpose tool designed for working with self-contained files for JVM languages (Java, Kotlin, etc.). In addition to scripting, jars may be generated, executed, and scripts may be shared in a streamlined way. For more details on JBang features aside from scripting, refer to my blog post about JBang , my talk at Devoxx UK , and the official website . This section focuses on the scripting features of JBang.

JBang supports templates which create new scripts based on predefined ones. Of the many available templates, the hello.kt one create a simple Kotlin script. We can create a new file based on this template by passing the argument -t hello.kt to the jbang init command.

The following commands demonstrate how to create and run a Kotlin script named hello-jbang.kt based on the hello.kt template:

jbang init -t hello.kt hello-jbang.kt
# Execute the script
jbang hello-jbang.kt
# Alternative way to execute by making it executable
chmod +x hello-jbang.kt
./hello-jbang.kt

The hello-jbang.kt file is a regular Kotlin file with a main method. Its content is as follows:

///usr/bin/env jbang "$0" "$@" ; exit $?

public fun main() {
    println("Hello World")
}

It is considered a JBang script thanks to shebang-like syntax on the first line (// instead of #!), which instructs JBang to execute the file as a script. To be more precise, this is not a shebang but a command that instructs the shell to run the file (the "$0" argument) with the jbang command while passing it any command line argument the user provided ("$@"). The ; exit $? tells the interpreter to exit with the same status code as the jbang command and not execute the rest of the file. To illustrate, when running the file with ./hello-jbang.kt, this is what happens:

  1. The default interpreter (bash, sh, zsh, etc.) opens the file ./hello-jbang.kt in order to run it as a shell script (not a Kotlin script but a regular shell script).
  2. The first line of the file is thus execute as shell script ///usr/bin/env jbang "$0" "$@" ; exit $?
  3. The first part ///usr/bin/env jbang "$0" "$@" runs the file a new time using jbang, and the shell waits for jbang to finish.
  4. As soon as jbang finishes execution, the interpreter resumes its execution and runs the second part of the first line of the file exit $?, which exits the shell with the same status code as the jbang command.

JBang supports all scripting features of *.main.kts with these additions:

  • File extension is .kt: the same extension as regular Kotlin files. Thus, the file must have a main method.
  • Templates: the ability to create scripts from predefined models.
  • Include other sources: useful for sharing code between scripts.
  • Include resources: if the code has resources, such as an app icon or a translation file.
  • Compiler and runtime options: for customizing the behavior of the compiler and the runtime.

The following script demonstrates a Quarkus REST server written in Kotlin using JBang. It uses the //DEPS feature to include dependencies.

///usr/bin/env jbang "$0" "$@" ; exit $?

//DEPS io.ktor:ktor-bom:3.0.1@pom
//DEPS io.ktor:ktor-server-netty-jvm
//DEPS io.ktor:ktor-serialization-jackson-jvm
//DEPS io.ktor:ktor-server-content-negotiation-jvm

import com.fasterxml.jackson.databind.SerializationFeature
import io.ktor.serialization.jackson.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

data class Todo(val title: String, val isCompleted: Boolean)

fun main(args: Array<String>) {
    val todos = listOf(Todo("Sleep", false), Todo("Eat", true))
    print(todos)
    embeddedServer(Netty, 8080) {
        install(ContentNegotiation) {
            jackson {
                enable(SerializationFeature.INDENT_OUTPUT)
            }
        }
        routing { get("/") { call.respond(todos) } }
    }.start(wait = true)
}

It can be run with the following command, which downloads the script and runs it locally:

jbang ktor-rest@yostane

Once the server is running, it can be accessed at http://localhost:8080, which returns a JSON response with the list of todos:

[
  {
    "title": "Sleep",
    "isCompleted": false
  },
  {
    "title": "Eat",
    "isCompleted": true
  }
]

JBang provides a versatile and feature-rich approach to writing Kotlin scripts. It supports dependency management, templates, the ability to include other sources and resources, and many other features. It also supports other JVM languages, such as Java and Groovy, making it a good choice for multi-language projects. JBang is actively maintained and has a vibrant community, making it a reliable and robust tool for Kotlin scripting. In my opinion, JBang is the best option for writing Kotlin scripts thanks to its comprehensive set of features, great developer experience and IDE support.

The next section covers an alternative way of running Kotlin code blocks, which is not exactly scripting but is noteworthy.

Bonus: Kotlin notebooks

Jupyter Notebook is a standard format for writing markdown, code, and the result of the code, all in a single file. The extension of these files is .ipynb (interactive Python notebooks). Notebooks are widely used for the following reasons:

  • Code blocks may be written in any language, as long as a Jupyter Kernel for that language is installed. Although the extension of notebook files is “.ipynb” (for Python), it does not mean that only Python is supported.
  • Code blocks may be executed in any order and independently, and a global session context retains the values of global variables across code block executions.
  • Code and markdown blocks may be written in any order.
  • Jupyter Notebooks are rendered properly by GitHub, GitLab, and many other tools and apps, including syntax highlighting and graphics and charts generated by code.

These features make Jupyter Notebooks suitable for writing documentation, experimentation, data science, and scripting.

Official support for Kotlin is available through the Kotlin Kernel, which allows Kotlin code blocks to be run inside Jupyter Notebooks. The IntelliJ extension for Kotlin notebooks enables editing features found in regular Kotlin files (such as syntax highlighting and code suggestions). VSCode also has a Kotlin extension for notebooks, but the language support is not as comprehensive (this may change with the official Kotlin LSP ).

The following screenshot shows a Kotlin notebook with a markdown block, a code block, and the result of the code block.

Kotlin notebook sample

The notebook may be opened and edited with IntelliJ IDEA, VSCode, or any other Jupyter Notebook-compatible editor. It can also be rendered on GitHub or GitLab. The above notebook can be viewed on GitHub at this link .

Other screenshots of Kotlin notebooks are shown below:

Kandy graph

source: https://github.com/gaplo917/awesome-kotlin-notebook/blob/main/postgres/postgres-vanilla.ipynb (source gaplo917/awesome-kotlin-notebook )

Kotlin notebooks provide an interactive and visual way to write Kotlin code. They are not exactly scripting, but may be used for scripting-like tasks, such as experimenting with code, writing documentation, and sharing code snippets.

Conclusion

In this post, we have seen that Kotlin supports scripting through three techniques: using the official *.main.kts scripting, using kscript, and using JBang. We have explored the features and limitations of each approach. I do not recommend kscript files for new scripts as there are better options available. Instead, I recommend using JBang or alternatively the official *.main.kts scripting. However, this recommendation may change in the future as the official Kotlin scripting ecosystem evolves and matures from the experimental stage. Indeed, Kotlin keeps evolving and proposing new possibilities. For example, we have seen that Kotlin notebooks provide an interactive and visual way to write Kotlin code, making it easier to experiment and document code snippets. Maybe we’ll see more creative ways to use Kotlin in the future. In the meantime, we can enjoy writing Kotlin with the different possibilities available today.

Happy coding!

References