Kotlin Command Reference

Every Kotlin program you write eventually has to leave the editor and become bytes the JVM can execute, and that journey runs through two command-line tools: kotlinc, the Kotlin compiler, and kotlin, the runner that launches compiled Kotlin classes or executes Kotlin scripts on the JVM. Understanding what these tools produce, which flags matter, and how they differ from Java’s javac/java pair becomes essential the moment you leave an IDE’s “Run” button behind — for CI pipelines, standalone scripts, and distributing a runnable JAR. This lesson is a practical, hands-on reference to the Kotlin command-line workflow: compiling a file, building a self-contained JAR, running Kotlin scripts, and using the interactive REPL.

Overview / How it works

kotlinc compiles one or more .kt source files into JVM bytecode — ordinary .class files, the same format javac produces. Unlike a language such as Go, Kotlin has no standalone runtime baked into the OS binary; every compiled program depends on kotlin-stdlib, a jar of classes that supplies things like Unit, Kotlin’s collection extension functions, and internal null-check helpers. That matters immediately: a jar built by kotlinc without extra flags contains only your classes, not the stdlib, so it won’t run standalone until you either bundle the stdlib in or put it on the classpath yourself.

The kotlin command is a thin, stdlib-aware wrapper around java. Point it at a jar, a compiled class, or a .kts script, and it resolves the classpath (including the stdlib, if it isn’t already bundled) and launches the JVM for you. For .kts script files, there’s no compiled artifact step visible to you at all — the script’s top-level statements are compiled to an in-memory class and executed immediately, which is why Gradle’s Kotlin DSL (build.gradle.kts) and quick automation scripts use the .kts extension instead of a full program.

One JVM quirk shapes how you invoke compiled Kotlin directly: the JVM has no concept of a “top-level function” the way Kotlin source does. When you write fun main() at the top of a file named Foo.kt, the compiler generates a hidden class called FooKt and turns main into a static-style method on it. That’s why running loose class files by name means running FooKt, not Foo — a common point of confusion. Running kotlinc with no source file argument at all drops you into an interactive REPL, which compiles and evaluates each line you type against a live JVM session — useful for testing a standard library function or a language feature without creating a project.

Syntax

The general forms you’ll use most often:

Command What it does
kotlinc file.kt -d out.jar Compiles a file into a jar containing only your classes (no stdlib bundled).
kotlinc file.kt -include-runtime -d out.jar Compiles and bundles kotlin-stdlib into the jar, so it runs standalone.
kotlin out.jar [args...] Runs a jar built by kotlinc, resolving the stdlib classpath automatically.
java -jar out.jar Also works, but only if the jar is self-contained (built with -include-runtime).
kotlinc -script script.kts Compiles and immediately runs a Kotlin script file.
kotlinc No file argument — launches the interactive REPL.
kotlinc file.kt -d bin -jvm-target 17 Compiles to loose class files targeting a specific JVM bytecode version.

Examples

Example 1: compile and run a self-contained program. Save this as CommandHello.kt:

fun main() {
    println("Hello, Kotlin CLI!")
}

Then, from the same directory:

$ kotlinc CommandHello.kt -include-runtime -d hello.jar
$ kotlin hello.jar

Output:

Hello, Kotlin CLI!

The first command compiles CommandHello.kt, and because -include-runtime is present, copies the kotlin-stdlib classes into hello.jar alongside your compiled CommandHelloKt class. The resulting jar’s manifest records that class as the entry point, so kotlin hello.jar (or even java -jar hello.jar, since the jar is now self-contained) finds and runs its main function directly.

Example 2: reading command-line arguments safely. Save this as Greet.kt:

fun main(args: Array<String>) {
    val name = args.getOrNull(0) ?: "stranger"
    println("Hello, $name!")
}
$ kotlinc Greet.kt -include-runtime -d greet.jar
$ kotlin greet.jar Ada
$ kotlin greet.jar

Output:

Hello, Ada!
Hello, stranger!

args is typed Array<String>, which is never null itself — but it can be empty if no arguments are passed, and indexing an empty array throws at runtime. getOrNull(0) returns a nullable String? instead of crashing, and the elvis operator ?: supplies a fallback when it’s null. Running the jar with Ada as an argument prints the first line; running it with no arguments at all prints the second.

Example 3: a Kotlin script. Save this as sum.kts — notice there’s no fun main() at all, just top-level statements executed in order:

val numbers = listOf(4, 8, 15, 16, 23, 42)
val total = numbers.sum()
println("Total: $total")
$ kotlinc -script sum.kts

Output:

Total: 108

There’s no separate compile-then-run step here: kotlinc -script compiles the script to an in-memory class and executes it in one command, which is exactly the workflow Gradle uses for build.gradle.kts files behind the scenes.

How it works step by step

For a normal .kt file compiled and run as a jar, the pipeline looks like this:

  • You write source in one or more .kt files.
  • kotlinc parses and type-checks the code. This is where null-safety violations, unresolved references, and type mismatches are caught — nothing has executed yet, and a single error here fails the whole compile.
  • The compiler emits JVM bytecode: one .class per top-level file (as a synthetic class like FooKt) plus one per user-defined class or object. With -d out.jar, these are packed into a jar whose manifest points Main-Class at whichever class contains fun main().
  • If -include-runtime was passed, kotlinc also copies the classes from kotlin-stdlib.jar into the output jar, making it self-contained.
  • kotlin launches a JVM (it’s a wrapper around java), ensures kotlin-stdlib is on the classpath if it isn’t bundled, and invokes the discovered main method.
  • For .kts scripts, kotlinc/kotlin skip the separate-artifact step: the script is compiled to an anonymous class in memory and its top-level statements run immediately, in the order they appear.

Common Mistakes

Mistake 1: forgetting -include-runtime, then running the jar with plain java.

$ kotlinc CommandHello.kt -d hello.jar
$ java -jar hello.jar
Exception in thread "main" java.lang.NoClassDefFoundError: kotlin/jvm/internal/Intrinsics

Without -include-runtime, the jar contains only your classes, not kotlin-stdlib — and even a trivial println call relies on stdlib helper classes at runtime. Fix it by bundling the runtime, or by using kotlin instead of raw java (it resolves the stdlib classpath for you even without bundling):

$ kotlinc CommandHello.kt -include-runtime -d hello.jar
$ java -jar hello.jar

Mistake 2: indexing args directly instead of checking its size.

fun main(args: Array<String>) {
    val name = args[0]
    println("Hello, $name!")
}

This compiles fine, but throws ArrayIndexOutOfBoundsException the moment someone runs the jar without an argument. args being non-null doesn’t mean it’s non-empty — those are two different guarantees. Use getOrNull with a fallback instead:

fun main(args: Array<String>) {
    val name = args.getOrNull(0) ?: "stranger"
    println("Hello, $name!")
}

Mistake 3: confusing kotlinc (compiler) with kotlin (runner).

$ kotlinc hello.jar
(no output produced — kotlinc has nothing new to compile)
$ kotlin hello.jar
Hello, Kotlin CLI!

kotlinc only compiles source; it doesn’t know how to execute a finished jar, so pointing it at one typically just does nothing useful. kotlin is the command that actually launches a program. Keeping the two straight — “c” for compile, no “c” for run — avoids a lot of “why isn’t anything happening” confusion.

Best Practices

  • Use kotlinc file.kt -include-runtime -d app.jar whenever you intend to hand someone a jar that must run outside your own dev machine.
  • Prefer kotlin app.jar (or plain java -jar app.jar once it’s self-contained) over invoking loose .class files with a hand-built classpath — there’s less to get wrong.
  • Use .kts scripts for glue and automation tasks, similar to Gradle’s Kotlin DSL, rather than a full compiled program when you don’t need a distributable artifact.
  • Pin -jvm-target explicitly in build scripts and CI so compiled bytecode matches the JVM version you actually deploy to, instead of relying on the compiler’s default.
  • Reach for the bare kotlinc REPL to test a standard library function or language feature in isolation rather than spinning up a whole project.
  • In real projects, let Gradle or Maven’s Kotlin plugin drive kotlinc for you — invoke it directly mainly for scripts, quick experiments, and understanding what your build tool is doing under the hood.

Practice Exercises

  • Write a file Area.kt with fun main(args: Array<String>) that reads two command-line arguments as a rectangle’s width and height, safely converts them with toDoubleOrNull(), and prints the area — with a clear message if an argument is missing or not a valid number. Compile it into a self-contained jar and run it as kotlin area.jar 3.5 4.
  • Write a Kotlin script squares.kts that computes the sum of the squares of 1 through 10 using sumOf with a lambda, and run it with kotlinc -script squares.kts. Expected output: 385.
  • Compile a file without -include-runtime, then try running the resulting jar with plain java -jar. Observe what happens, then explain in your own words why it happens and how -include-runtime or the kotlin command fixes it.

Summary

  • kotlinc compiles .kt source to JVM bytecode; kotlin runs compiled bytecode or executes .kts scripts, both acting as thin, stdlib-aware wrappers over the JVM.
  • A plain compiled jar contains only your classes — add -include-runtime to bundle kotlin-stdlib so it’s runnable with plain java -jar.
  • Every top-level fun main() lives inside an auto-generated class named after its file (FooKt for Foo.kt) — that’s the class name the JVM actually looks for.
  • .kts scripts skip fun main() entirely; their top-level statements execute directly, in order, when compiled and run.
  • Running bare kotlinc with no file argument launches the interactive REPL for quick experiments.
  • args: Array<String> is never null but can be empty — use getOrNull() with the elvis operator instead of indexing directly to avoid runtime crashes.