kyper License codecov

Functional Kotlin friendly way to create command line applications.

It’s coming from the basic need of having a function like:

fun main(
    content: String,
    target: File,
    mode: Mode = Mode.APPEND,
) {
    TODO()
}

enum class Mode { APPEND, OVERWRITE }

with all the expected behaviors we’re expecting when using it in our code:

  • support more than String like File/Path/Enum/etc,
  • mode being an optional parameter,

and turns it into a command line application powered by Kotlin script.

This library is solving this need ?

Kyper? Is it a name?

This library is hugely inspired by the wonderful typer from the Python ecosystem.

Also naming is hard ?

But we already have clikt? (or any alternative)

Correct, but I try to keep my Kotlin scripts as small as possible, and have to deal with classes is not what I would describe as simple.

Also chained property delegates are great, but I always need to read the documentation to figure out all the options.

So let’s migrate everything to this wonderful library?

For simple use cases like Kotlin scripts, feel free.

For more complex applications, where readability is important, I would stick with clikt or any alternative not working on magic reflection and/or implicit behaviors as this library is doing.

Installation central

repositories {
    mavenCentral()
}

dependencies {
    // Check the ? maven central badge ? for the latest $kyperVersion
    implementation("com.github.pgreze:kyper:$kyperVersion")
}

Or in your kotlin script:

@file:DependsOn("com.github.pgreze:kyper:$kyperVersion")

Usage with function(s)

Start with a single function

We can start by defining a simple function handling our logic:

#!/usr/bin/env kotlinc -script

import com.github.pgreze.kyper.Help
import com.github.pgreze.kyper.kyper

@Help("function help message")
fun main(
    @Help("the name to greet")
    name: String
) {
    println("hello $name")
}

kyper().invoke(args)

And run it with the name parameter:

$ ./script.main.kts there
hello there

Notice we also defined help messages for both the command and its parameter:

$ # Use `--` to indicates following arguments to the script, not kotlinc itself
$ ./script.main.kts -- --help
Usage: main NAME

  function help message

Options:
  -h, --help      Show this message and exit

Arguments:
  NAME  the name to greet

Only public methods are exported as commands

Our script can declare more internal/private methods, without exposing them as command:

#!/usr/bin/env kotlinc -script

import com.github.pgreze.kyper.kyper

fun main(name: String) {
    greet(name)
}

private fun greet(name: String) {
    println("Hello $name")
}

kyper().invoke(args)

Usage is the same:

$ ./script.main.kts there
hello there

But having several public functions will turn our application into a multi-command mode:

#!/usr/bin/env kotlinc -script

import com.github.pgreze.kyper.kyper

@Help("Say hello in English")
fun hello(name: String) {
    println("hello $name")
}

@Help("Say hello in French")
fun bonjour(name: String) {
    println("bonjour $name")
}

kyper(help = "Say hello to your user").invoke(args)

We can now notice several commands are available by calling –help:

$ ./kyper/hellos.main.kts -- --help
Usage: [OPTIONS] COMMAND [ARGS]...

  Say hello to your user

Options:
  -h, --help      Show this message and exit

Commands:
  BONJOUR  Say hello in French
  HELLO    Say hello in English

Each command can be requested for --help:

$ ./kyper/hellos.main.kts -- --help bonjour
Usage: bonjour NAME

  Say hello in French

Options:
  -h, --help      Show this message and exit

Arguments:
  NAME

Handle more than strings as arguments

Following types are supported:

fun main(
    string: String = "arg",
    int: Int = 1,
    float: Float = 1.2f,
    double: Double = 3.14,
    long: Long = Long.MAX_VALUE,
    boolean: Boolean = true,
    bigInteger: BigInteger = BigInteger.valueOf(12),
    bigDecimal: BigDecimal = BigDecimal.valueOf(12.3),
    file: File = File("file"),
    path: Path = Path.of("path"),
    choice: Choice = Choice.NO,
    vararg strings: String, // For vararg, only String/File are supported.
) {
    TODO()
}

enum class Choice { YES, NO }

Default values (unstable)

As shown in the last code block, default values are supported as long as they’re at the end of the method.

?️ WIP: current implementation is quite simple, just based on parameter positioning, and not allowing any --flag logic.

Usage with lambda(s) (experimental)

Maybe the Kotlin DSL syntax is something you’re looking for in your Kotlin script, and so this library is also providing a similar syntax based on lambdas:

#!/usr/bin/env kotlinc -script

@file:DependsOn("/Users/pgreze/git/pgreze/kyper/kyper/build/libs/kyper.jar")
@file:Suppress("OPT_IN_USAGE")

import com.github.pgreze.kyper.kyper

kyper(help = "Run multiple commands from Kotlin script with ease") {
    register(name = "time", help = "Display current timestamp") { ->
        println(System.currentTimeMillis())
    }

    register(name = "greet") { name ->
        println("Hello $name")
    }
}.invoke(args)

But this is coming with restrictions:

  • no default argument(s),
  • up to 4 arguments,
  • only String is supported.

This may be dropped in the future if we cannot reach the same support as with the functions based usage.

GitHub

View Github