Skip to content
/ kyper Public

Functional Kotlin friendly way to create command line applications.

License

Notifications You must be signed in to change notification settings

pgreze/kyper

Repository files navigation

kyper License codecov

Functional Kotlin friendly way to create command line applications.

It comes from the basic need to have a function like:

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

enum class Mode { APPEND, OVERWRITE }

with all the behaviors we expect when using it in our code:

  • supports more data types than just String, i.e. 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 having 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 usecases like Kotlin scripts, feel free.

For more complex applications, where readability is important, I would stick with clikt or any alternative not relying on magic reflection and/or implicit behaviors like 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.Command
import com.github.pgreze.kyper.Parameter
import com.github.pgreze.kyper.kyper

@Commmand("function help message")
fun main(
    @Parameter("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 indicate that the following arguments are for 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 @Command annotated methods are exported

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

#!/usr/bin/env kotlinc -script

import com.github.pgreze.kyper.Command
import com.github.pgreze.kyper.kyper

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

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

kyper().invoke(args)

Usage is the same:

$ ./script.main.kts there
hello there

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

#!/usr/bin/env kotlinc -script

import com.github.pgreze.kyper.Command
import com.github.pgreze.kyper.kyper

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

@Command("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

The 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: the current implementation is quite simple; it is just based on parameter positioning, and does not allow any --flag logic.

Usage with lambda(s) (experimental)

If the Kotlin DSL syntax is something you're looking for in your Kotlin script, this library also provides a similar syntax based on lambdas:

#!/usr/bin/env kotlinc -script

@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 comes with restrictions:

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

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