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
.ktfiles. kotlincparses 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
.classper top-level file (as a synthetic class likeFooKt) plus one per user-defined class or object. With-d out.jar, these are packed into a jar whose manifest pointsMain-Classat whichever class containsfun main(). - If
-include-runtimewas passed,kotlincalso copies the classes fromkotlin-stdlib.jarinto the output jar, making it self-contained. kotlinlaunches a JVM (it’s a wrapper aroundjava), ensures kotlin-stdlib is on the classpath if it isn’t bundled, and invokes the discoveredmainmethod.- For
.ktsscripts,kotlinc/kotlinskip 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.jarwhenever you intend to hand someone a jar that must run outside your own dev machine. - Prefer
kotlin app.jar(or plainjava -jar app.jaronce it’s self-contained) over invoking loose.classfiles with a hand-built classpath — there’s less to get wrong. - Use
.ktsscripts 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-targetexplicitly 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
kotlincREPL 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
kotlincfor 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.ktwithfun main(args: Array<String>)that reads two command-line arguments as a rectangle’s width and height, safely converts them withtoDoubleOrNull(), 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 askotlin area.jar 3.5 4. - Write a Kotlin script
squares.ktsthat computes the sum of the squares of 1 through 10 usingsumOfwith a lambda, and run it withkotlinc -script squares.kts. Expected output:385. - Compile a file without
-include-runtime, then try running the resulting jar with plainjava -jar. Observe what happens, then explain in your own words why it happens and how-include-runtimeor thekotlincommand fixes it.
Summary
kotlinccompiles.ktsource to JVM bytecode;kotlinruns compiled bytecode or executes.ktsscripts, both acting as thin, stdlib-aware wrappers over the JVM.- A plain compiled jar contains only your classes — add
-include-runtimeto bundle kotlin-stdlib so it’s runnable with plainjava -jar. - Every top-level
fun main()lives inside an auto-generated class named after its file (FooKtforFoo.kt) — that’s the class name the JVM actually looks for. .ktsscripts skipfun main()entirely; their top-level statements execute directly, in order, when compiled and run.- Running bare
kotlincwith no file argument launches the interactive REPL for quick experiments. args: Array<String>is never null but can be empty — usegetOrNull()with the elvis operator instead of indexing directly to avoid runtime crashes.
