· 5 min read

November 2024: the state of Kotlin scripting

*.main.kts files can replace most of your shell scripts

*.main.kts files can replace most of your shell scripts

JetBrains recently published a blog post about the current state of Kotlin scripting.

This sparked a series of reactions that Kotlin scripting was “not recommended”, “dead” or just general questions about what the post really meant for the future of Kotlin scripting.

I believe the future of Kotlin scripting is bright. Despite some of the initial impressions, I believe the post is really about shifting focus to where Kotlin scripting really matters:

I myself wrote about Kotlin scripting back in May 2020. To this day, this is one of my most read blog post so this felt like the perfect opportunity to make a small refresher.

Let’s see how you can use *.main.kts today, in November 2024!

And what’s in stock for the future!

What are *.main.kts scripts?

*.main.kts files are a first-party solution to run Kotlin code without having to use a full build system such as Amper/Gradle/Maven.

Just create a hello.main.kts file:

#!/usr/bin/env kotlin

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

Make it executable and run it:

$ chmod +x hello.main.kts
$ ./hello.main.kts world
Hello world!

Pretty cool right? If your file ends with .main.kts, kotlin knows that it’s a script and does a bunch of cool stuff:

  • Caching: for faster invocations
  • IDE support
  • Dependencies: @file:DependsOn and @file:Repository

If you don’t want to use a shebang line, you can also call your script explicitly:

$ kotlin hello.main.kts world
Hello world!

Want to replace text using a regex in some of your files? Use your favorite Kotlin constructs:

#!/usr/bin/env kotlin

File(args[0]).walk().filter {
  it.isFile && it.name.startsWith("MyPrefix")
}.forEach {
  it.writeText(it.readText().replace(myRegex, myReplacement))
}

Sure you can do the same with a collection of find, xargs, sed and what not but do you really have that much space in your brain?

What’s more, the IDE can help your brain find and remember Kotlin patterns much better. Autocomplete is available everywhere:

autocomplete.png

Want to parse a CSV file? No problem either, the JVM ecosystem has everything for you:

#!/usr/bin/env kotlin

@file:DependsOn("org.jetbrains.kotlinx:dataframe:0.14.1")

import org.jetbrains.kotlinx.dataframe.DataFrame
// ...

val df = DataFrame.read("toto.csv")

val price by column<Int>()
val name by column<String>()

val cheapestProduct = df.minBy(price)[name]

Good luck doing all of that in shell…

Oh! And did I mention you can debug your scripts?

debug.png

Modern syntax, autocomplete, debugging, huge ecosystem with JavaDoc accessible from your IDE and more, using *.main.kts is a simple, efficient to run Kotlin with just a simple file today.

*.main.kts can replace your shell scripts

For the last 4 years or so, I’ve been using *.main.kts scripts quite happily. One of my favorite scripts updates my SIEVE email spam filters from a GitHub action. Goodbye spammers 👋

I use Kotlin scripts at Apollo to run our benchmarks in Firebase test labs or keep the Kotlin shadow branch in sync.

GitHub actions supports Kotlin scripting out of the box making them perfect candidates for one-off tasks in CI. Kotlin scripts can even help you write typesafe GitHub actions workflows!

What’s more, JetBrains is planning to improve *.main.kts files even more. From the aforementioned blog post:

We will continue to develop the `.main.kts` script type, which is already
helpful for simple automation tasks.
We have plans to extend its functionality and streamline IDE support.

Every time I want to write a bash script (or zsh? beware the incompatibilities!), I’d say it can be written in Kotlin and leverage a modern language with sensible syntax and escaping rules, a huge ecosystem of libraries and state of the art IDE support.

Not to forget, one less language to learn and remember!

What’s missing in *.main.kts?

For the sake of completeness, and despite being a huge fan, I’ll mention the current limitations:

  1. Initial compile time may take several seconds (subsequent runs are faster).
  2. Including scripts from other scripts has issues.
  3. The resolver does not understand Gradle metadata.
  4. You have to manually reload the dependencies when adding/removing them.
  5. Process redirection and signal handling is a bit awkward.

I’m quite happy with the kotlin vs shell tradeoff at the moment and the lost time in compile time and writing ProcessBuilders is more than made up with Kotlin syntax and IDE support.

Can this be improved? Sure. But the value proposition of *.main.kts scripts is already quite good in my opinion.

So what is the post about?

Alright so if *.main.kts is great and there are plans to make it even better, if build.gradle.kts and Notebooks are here to stay, then what is it all about?

If I’m reading right and trying to summarize, the post is mostly about:

  1. REPL is going away, replaced by scratch files and Notebooks.
  2. JSR-223 support, KotlinScriptMojo, kotlin-scripting-ide-services are also going away.
  3. Custom scripting APIs are staying experimental. Those are the APIs powering the build.gradle.kts files.

While the interactive Kotlin REPL console could be handy, I have always preferred scratch files for full IDE support so I don’t mind too much there.

I have never used JSR-223 support, KotlinScriptMojo, kotlin-scripting-ide-services and won’t be missing them.

Custom scripting APIs are unchanged. For all matters, stable features such as build.gradle.kts can be built on top of experimental APIs. KSP is another great example where experimental APIs allow faster innovation while another layer provides stability.

Most importantly, *.main.kts, build.gradle.kts and Notebooks are going to get more focus, which is in my opinion great news.

I’m looking forward to the next 25 years of Kotlin scripting!

    Share:
    Back to Blog