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!
jmfayard / kotlin-cli-starter
Life is too short for Bash programming
A starter project to build command-line tools in Kotlin Multiplatform
Contains a re-implementation of a real world CLI tool: git-standup
Installation
You can install using one of the options listed below
Source | Command |
---|---|
Node | npm install -g kotlin-cli-starter |
Installer | curl -L https://raw.githubusercontent.com/jmfayard/kotlin-cli-starter/main/installer.sh | sudo sh |
Tests | ./gradlew allTests |
Kotlin All Platforms | Run ./gradlew allRun
|
Kotlin JVM | Run ./gradlew run
|
Kotlin Native | Run ./gradlew install then $ git standup
|
Kotlin Node.JS | Run ./gradlew jsNodeRun
|
Why?
Being able to write your own command-line tools is a great skill to have. Automate all the things!
You can write the CLI tools in Kotlin and reap the benefits of using
- a modern programming language
- modern IDE support
- modern practices such as unit testing and continuous integration
- leverage Kotlin multiplatform libraries
- run your code on the JVM and benefit from a wealth of Java libraries
- or build a native executable, which starts very fast andβ¦
git-standup, Kotlin Multiplatform edition
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:
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.
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 :
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)
- 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
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
- There are also a plethora of libraries on Node.js, and packaging it is easy with 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.
For example, I needed to check if a file exists:
// commonMain
expect fun fileIsReadable(filePath: String): Boolean
Implementing this on the JVM is straightforward
// 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".
The first answer is about the access system call.
We read the friendly manual with man access:
Which leads us directly to this implementation:
// 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:
// 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)
}
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
- Kotlin/kotlinx.serialization: Kotlin multiplatform / multi-format serialization
- cashapp/sqldelight: SQLDelight - Generates typesafe Kotlin APIs from SQL
- Kodein-Framework/Kodein-DB: Multiplatform NoSQL database
- square/okio: A modern I/O library for Android, Kotlin, and Java.
- Kodein-Framework/Kodein-DI: Painless Kotlin Dependency Injection
- InsertKoinIO/koin: Koin - a pragmatic lightweight dependency injection framework for Kotlin
- Kotlin/kotlinx.coroutines: Library support for Kotlin coroutines
- Kotlin/kotlinx-datetime: KotlinX multiplatform date/time library
- Ktor client - multiplatform asynchronous HTTP client
- kotest/kotest: Powerful, elegant and flexible test framework for Kotlin with additional assertions, property testing and data driven testing
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()
- Stumbling on issues like runBlocking does not exist in Kotlin Multiplatform
- ... 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.
I have put comments starting with CUSTOMIZE_ME in all places you need to customize
Find them with Edit > File > Find in Files
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.
jmfayard / kotlin-cli-starter
Life is too short for Bash programming
A starter project to build command-line tools in Kotlin Multiplatform
Contains a re-implementation of a real world CLI tool: git-standup
Installation
You can install using one of the options listed below
Source
Command
Node
npm install -g kotlin-cli-starter
Installer
curl -L https://raw.githubusercontent.com/jmfayard/kotlin-cli-starter/main/installer.sh | sudo sh
Tests
./gradlew allTests
Kotlin All Platforms
Run
./gradlew allRun
Kotlin JVM
Run
./gradlew run
Kotlin Native
Run
./gradlew install
then $ git standup
Kotlin Node.JS
Run
./gradlew jsNodeRun
Why?
Being able to write your own command-line tools is a great skill to have. Automate all the things!
You can write the CLI tools in Kotlin and reap the benefits of using
- a modern programming language
- modern IDE support
- modern practices such as unit testing and continuous integration
- leverage Kotlin multiplatform libraries
- run your code on the JVM and benefit from a wealth of Java libraries
- or build a native executable, which starts very fast andβ¦
If you build something with the kotlin-cli-starter, I would be glad if you let me know!
Top comments (10)
None of these things are strictly true.
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
The biggest issue with writing a CLI in a JVM language is no one wants to run a CLI via a JVM, download Gradle (even with the wrapper), etc which wasn't really addressed here. The selling point of bash is it runs natively.
I'm confused "biggest issue with writing a CLI in a JVM language" comment. If a dev is already doing Kotlin wouldn't writing CLI tools in a language they are familiar with be useful? When the target is native, you don't need a JVM to run the resultant code.
Aren't you fairly limited in the libraries you can use when building native though? Is being restrict to C library wrappers really better than canonic GNU programs?
I would say this is a matter of how comfortable one is using bash versus how comfortable one is using Kotlin. As someone who uses Kotlin on the regular, I can visualize the business logic better in that language and the developer tools are more familiar to me. Additionally, if your CLI tool is doing less interaction with GNU tools and more processing and you are very comfortable with Kotlin then Kotlin CLI might be more for you... especially if you don't need to write a lot of CLI tools. Also if you are writing a tool that is targeted to multi-platform then this is exactly what you want to do if Kotlin is your primary language as you would only need to write the business logic once.
But I agree with your statement in general.
The thing is, you can compile it to a native exe
a native windows exe
a native MacOS exe
a native linux exe
if you ever watched a bash script running for 24 minutes and then fail
because again you forgot some single/double quotes or parenthesises or 3times-escapes, then you start value strongly typed languages ... and HashMaps ... and lambdas ... and and and ...
or use github.com/holgerbrandl/kscript
without all the gradle, Intellij, etc. stuff
There are shell scripts who do all of this, and there are flying fishes, but that's not the majority of the species!
Gradle and the JVM are only needed for building the tool.
After, if you use Kotlin/JS, only node is required, and if you use Kotlin/native, nothing is required
this is quite interesting, kotlin seems quite underrated (outside android ecosystem)
Thanks a lot Karan!
Found your Article ... was SOOOOO curious how YOU solved the multiplatform command execute ... Thought NOW I learn how to really do it ...
THEN found out, you used my Stackoverflow thingy.
Good work man!
proud to be referenced!
(now I keep trying to stop giggle about myself :)
Thanks for sharing Jean!