How to Write a Command-Line Tool with Kotlin Multiplatform
Tags: kotlin, productivity, showdev
Published: 2021-07-13 19:36:19

Being able to write your own command-line tools is a great skill to have: automate all the things!

Unfortunately, writing CLI tools is still most often done with Bash. My strong opinion - weakly held - is that there are only two good kinds of Bash scripts:

- the ones that are five lines long or less
- the ones that are written and maintained by others

Fortunately, retiring Bash is pretty simple: you just have to use your favorite programming language instead!

I've played around with Kotlin Multiplatform and not only built a cool CLI tool with it, but I've packaged it as a template project that you can leverage to get started faster!

{% github https://github.com/jmfayard/kotlin-cli-starter %}

## git-standup, Kotlin Multiplatform edition

[git-standup](https://github.com/kamranahmedse/git-standup) is a smart little tool for people like me who often panic before the stand-up. What the hell did I do yesterday? That command refreshes my mind by browsing all git repositories and printing the commits that I made yesterday. Try it out, it may well become part of your workflow.

Clone the repo:

```shell
git clone https://github.com/jmfayard/kotlin-cli-starter
cd kotlin-cli-starter
./gradlew install
git standup
```

`./gradlew install` will install the tool as a native executable.

There are two more options available:

- `./gradlew run` will run it on the JVM
- `./gradlew jsNodeRun` will run it on Node.js

Same common code, multiple platforms.

![git standup](http://i.imgur.com/4xmkA49.gif)

## Modern Programming Practices

A traditional Bash script contains

- a syntax where you can't remember how to do an if/else of a for loop
- unreadable code due to [Code golf](https://en.wikipedia.org/wiki/Code_golf) : `ztf -P -c -D -a`
- no dependency management (apart from an error message: `ztf not found`)
- one file that does everything
- no build system
- no IDE support (`vim` should be enough for everybody)
- no unit tests
- no CI/CD

By choosing a modern programming language like Kotlin instead, we get to have

- a modern syntax
- readable code
- separation of concerns in multiple functions, files, class and packages
- a modern build system like Gradle
- dependency management (here with [gradle refreshVersions](https://jmfayard.github.io/refreshVersions/))
- a modern IDE like JetBrains IntelliJ
- unit tests. Here, they are run on all platforms with `./gradlew allRun`
- CI-CD. Here, the unit tests are run on GitHub Actions. [See workflow](https://github.com/jmfayard/kotlin-cli-starter/blob/5201ee91122b4572d40167e7fbfae2f341ce5dfb/.github/workflows/runOnGitHub.yml#L31-L36)

That may sound obvious, yet people still write their CLI tools in Bash, so it's worth repeating.

## Why support multiple platforms?

The three platforms have different characteristics, and by making your code available on all platforms, you can decide which one works better for you:

- The JVM has a plethora of available libraries, superb tooling support ; it lacks a package manager and is slow to start
- A native stand-alone executable starts very fast, it can be distributed by Homebrew for example - I make [a Homebrew recipe](https://github.com/jmfayard/kotlin-cli-starter/blob/5201ee91122b4572d40167e7fbfae2f341ce5dfb/kotlin-cli-starter.rb#L5-L33)
- There are also a plethora of libraries on Node.js, and packaging it is easy with [npm-publish](https://github.com/mpetuska/npm-publish). My version of git-standup is available at https://www.npmjs.com/package/kotlin-cli-starter and I find it fascinating that I could do so without writing a line of JavaScript.

But mostly **it's a good opportunity to learn Kotlin/Multiplatform**, a fascinating technology, but one that can also be complex. By writing a CLI tool with Kotlin/Multiplatform, you start with something simple.

## What is it like to write Kotlin Multiplatform code?

It feels like regular Kotlin once you have the abstractions you need in place, and all you need is to modify **commonMain**.

It gets interesting when you have to [connect to platform-specific APIs with the expect/actual mechanism](https://kotlinlang.org/docs/mpp-connect-to-apis.html).

For example, I needed to check if a file exists:

```Kotlin
// commonMain
expect fun fileIsReadable(filePath: String): Boolean
```

Implementing this on the JVM is straightforward

```Kotlin
// jvmMain
actual fun fileIsReadable(filePath: String): Boolean =
    File(filePath).canRead()
```

But how to implement this with Kotlin Native?

Since Kotlin Native gives us access to the underlying C APIs, the question is: how would they do that in C?

Let's google ["how to check a file is readable C"](https://letmegooglethat.com/?q=how%20to%20check%20if%20a%20file%20is%20readable%20in%20c&l=1). 

The first answer is about the **access** system call. 

We read the friendly manual with **man access**:

<img width="740" alt="man_access__Users_jmfayard_and_Untitled" src="https://user-images.githubusercontent.com/459464/125505737-b76d2105-70d9-44f0-a66f-f92084c499c6.png">

Which leads us directly to this implementation:

```kotlin
// nativeMain
actual fun fileIsReadable(filePath: String): Boolean =
    access(filePath, R_OK ) == 0
```

Interesting, strange feeling that we are writing C in Kotlin.

It works, but wait, there is a better solution!

## Use an existing Kotlin Multiplatform library

I discovered later that **okio** has already implemented the multiplatform primitives I needed to work with files.

I was glad I could delete my platform-specific code:

```Kotlin
// commonMain
expect val fileSystem: FileSystem

fun fileIsReadable(filePath: String): Boolean =
    fileSystem.exists(filePath.toPath())

fun readAllText(filePath: String): String =
    fileSystem.read(filePath.toPath()) {
        readUtf8()
    }

fun writeAllText(filePath: String, text: String): Unit =
    fileSystem.write(filePath.toPath()) {
        writeUtf8(text)
    }
```

[See commit](https://github.com/jmfayard/kotlin-cli-starter/commit/a035c26acec04d2e99af3304577c60af3051e4d5)

## Which multiplatform libraries should I be aware of?

The most common use cases are covered with Kotlin Multiplatform libraries: parsing arguments, JSON, databases, IO, DI, coroutines and testing. 

Bookmark those libraries:

* [ajalt/clikt: Multiplatform command line interface parsing for Kotlin](https://github.com/ajalt/clikt)
* [Kotlin/kotlinx.serialization: Kotlin multiplatform / multi-format serialization](https://github.com/Kotlin/kotlinx.serialization)
* [cashapp/sqldelight: SQLDelight - Generates typesafe Kotlin APIs from SQL](https://github.com/cashapp/sqldelight)
* [Kodein-Framework/Kodein-DB: Multiplatform NoSQL database](https://github.com/Kodein-Framework/Kodein-DB)
* [square/okio: A modern I/O library for Android, Kotlin, and Java.](https://github.com/square/okio)
* [Kodein-Framework/Kodein-DI: Painless Kotlin Dependency Injection](https://github.com/Kodein-Framework/Kodein-DI)
* [InsertKoinIO/koin: Koin - a pragmatic lightweight dependency injection framework for Kotlin](https://github.com/InsertKoinIO/koin)
* [Kotlin/kotlinx.coroutines: Library support for Kotlin coroutines](https://github.com/Kotlin/kotlinx.coroutines)
* [Kotlin/kotlinx-datetime: KotlinX multiplatform date/time library](https://github.com/Kotlin/kotlinx-datetime)
* [Ktor client - multiplatform asynchronous HTTP client](https://ktor.io/docs/getting-started-ktor-client.html)
* [kotest/kotest: Powerful, elegant and flexible test framework for Kotlin with additional assertions, property testing and data driven testing](https://github.com/kotest/kotest)

I especially liked **CliKt** for arguments parsing. It has this killer feature that it can generate automatically not only the help message, but also shell completions for Bash, Zsh, and Fish.

## The drawback: it takes time to set everything up

Building this project was a lot of fun, and I learned a lot, but it also took lots of time.

- Setting up Gradle and a hierarchy of srcsets - not trivial at all
- Setting up testing
- Setting up GitHub Actions
- Defining primitives like [fun executeCommandAndCaptureOutput()](https://github.com/jmfayard/kotlin-cli-starter/commit/87d2c8ca8fb90ab6cbb16202d88ecd5fc91b47f5)     
- Stumbling on issues like [runBlocking does not exist in Kotlin Multiplatform](https://github.com/jmfayard/kotlin-cli-starter/issues/9)
- ... and more.

What should I do next time if I don't have so much time available?

Go back to Bash?

## kotlin-cli-starter : starter project to build CLI tools

I _really_ didn't want to go back to Bash. 

This is when I decided to go another route, and transform my experiment in a starter project that I, and everyone, could re-use.

Click _Use this template_ and you get a head start for writing your own CLI tool.

<img width="1214" alt="jmfayard_kotlin-cli-starter__Life_is_too_short_for_Bash_programming_and_Telegram_and_GitHub_Desktop" src="https://user-images.githubusercontent.com/459464/117193054-b40e9200-ade2-11eb-86a8-ad65a45a82da.png">

I have put comments starting with [**CUSTOMIZE_ME**](https://github.com/jmfayard/kotlin-cli-starter/search?q=CUSTOMIZE_ME) in all places you need to customize

Find them with `Edit > File > Find in Files`

<img width="998" src="https://user-images.githubusercontent.com/459464/117405450-f253a080-af0b-11eb-882c-7bae969bc7e2.png">

## Conclusion

Automate your workflow by using command-line tools. But think twice before using Bash like in the previous century. Kotlin and other modern programming language could well be a far superior solution. 

Using Kotlin Multiplatform gives you the strength of each platform, like a wealth of libraries for the JVM, a fast startup for native executables and a package manager (npm) for Node.js.

It is not strictly necessary, but if you want to learn Kotlin Multiplatform, this is a great point to start.

It could take a while to set up from scratch, though.

This is why I transformed my project to a starter project / GitHub template that you can reuse.

{% github https://github.com/jmfayard/kotlin-cli-starter %}

If you build something with the kotlin-cli-starter, I would be glad if you let me know!