TL;DR : Read the complete Github issue here
What is picoCLI?
From the website :
Picocli is a modern framework for building powerful, user-friendly, GraalVM-enabled command line apps with ease. It supports colors, autocompletion, subcommands, and more. In 1 source file so apps can include as source & avoid adding a dependency. Written in Java, usable from Groovy, Kotlin, Scala, etc.
In short, picoCLI is a cool library that allows you to create command line applications on the JVm with very little effort.
As many developers, I really like command line applications. They are simple to use, you can chain them and you can get a lot done with them without clicky clicky.
What I LOVED about picocli is how packed with features it is (I mean, have a look at the doc!). You can use annotations, or a programmatic API. It supports Kotlin, coloration, auto completion and even GraalVM. Really cool. But even more, the main contributor is a gem, very open and always supportive. I really appreciated interacting with him when preparing my talk.
And the hard work clearly pays off, as seen by the sheer amount of projects that make use of it :
A minimal example could be the following :
package nl.lengrand.swacli
import picocli.CommandLine
import picocli.CommandLine.*
import picocli.CommandLine.Model.*
import java.util.concurrent.Callable
import kotlin.system.exitProcess
@Command(
name = "sw",
version = ["0.1"],
mixinStandardHelpOptions = true,
description = [asciiArt, "@|bold,yellow \uD83E\uDE90 A Star Wars CLI built on top of https://swapi.dev/ \uD83E\uDE90 |@"],
subcommands = [PlanetsCommand::class, HelpCommand::class]
)
class SwaCLISubCommands : Callable<Int> {
@Spec
lateinit var spec: CommandSpec
override fun call(): Int {
spec.commandLine().usage(System.out)
return 0
}
companion object{
@JvmStatic
fun main(args: Array<String>){
exitProcess(CommandLine(SwaCLISubCommands()).execute(*args))
}
}
}
@Command(name = "planets", description = ["Search for planets"])
class PlanetsCommand : Callable<Int> {
@Spec
lateinit var spec: CommandSpec
override fun call(): Int {
PrettyPrinter(spec).print(SwApi.getPlanets())
return 0
}
}
This class creates a command line application called sw
that has a planets
subcommand. This means that once installed, you could do the following in command line and get results :
$sw planets
I have chopped up this class for the purpose of this blog post, but you can find a complete implementation here. This code was used in my talk at JFall (the recording should be on youtube at some point).
One of the cool things to note here is that picocli will take care of generating useful documentation and help for my application automagically using the mixinStandardHelpOptions
option.
We will not dive into the details of the PrettyPrinter
in this blog post. You can assume it does nothing more than prettifying the output of my call. The SwApi.getPlanets()
call will fetch the Star Wars API and return a JSON formatted list of planets present in the movies.
Now, one of the issues with that is that there are a lot of planets in Star Wars. So every call, I would receive a lot of output and have to scroll back up to list the results.
For a much better user experience, I had to look at how to paginate those results, to have git log
like results.
Paginating results : a first crude version
The cool thing about CLI is that you can pipe things into each other. That's the first thing I tried : pipe the results of the command into less
.
That's how it would look like :
$ sw planets | less -R
Now, that works as intended, but it also has a bad side effect in my opinion : You basically have to create an alias outside of the actual application to get the desired behaviour. Not great. Ideally, I'd like people to be able to download my little tool and go on with their life. 0 setup required.
Paginating results from within the application
For this, I actually asked for some help from the author of picocli. You can read the complete GitHub issue here.
The main idea is to create your own execution strategy and override the default one in your main command.
What will happen is as such:
- We create a process that spawns
less
and inherits input and output from the main process. - We want to run the command as usual but send the results of the output to the input of the process mentioned above.
This is how the updated code looks like. Essentially, we created a private function executionStrategy
.
Some notes to help understand the code:
- we check if no subcommand has been used, in which case we want to print the help without pagination.
- I have tried for a while using
StreamWriter
but never got it to work, while Remko suggested a working solution with a temporaryFileWriter
<!--kg-card-begin: code-->
package nl.lengrand.swacli
import picocli.CommandLine
import picocli.CommandLine.*
import picocli.CommandLine.Model.*
import java.io.FileWriter
import java.io.PrintWriter
import java.nio.file.Files
import java.util.concurrent.Callable
import kotlin.system.exitProcess
@Command(
name = "sw",
version = ["0.2"],
mixinStandardHelpOptions = true,
description = [asciiArt, "@|bold,yellow \uD83E\uDE90 A Star Wars CLI built on top of https://swapi.dev/ \uD83E\uDE90 |@"],
subcommands = [PlanetsCommand::class, HelpCommand::class]
)
class SwaCLIPaginate : Callable<Int> {
@Spec
lateinit var spec: CommandSpec
private fun executionStrategy(parseResult: ParseResult): Int {
if (!parseResult.hasSubcommand())
return RunLast().execute(parseResult)
val file = Files.createTempFile("pico", ".tmp").toFile()
this.spec.commandLine().out = PrintWriter(FileWriter(file), true)
val result = RunLast().execute(parseResult)
val processBuilder = ProcessBuilder("less", file.absolutePath).inheritIO()
val process = processBuilder.start()
process.waitFor()
return result
}
override fun call(): Int {
spec.commandLine().usage(System.out)
return 0
}
companion object{
@JvmStatic
fun main(args: Array<String>){
val app = SwaCLIPaginate()
exitProcess(CommandLine(app)
.setExecutionStrategy(app::executionStrategy)
.execute(*args))
}
}
}
You can read the complete code here in case you're interested.
Here is how the output of $sw planets
look like now, you can go through the results just like you would with $git log
. It even retains the colored output!
The only little thing I'm not perfectly happy about is the fact that we see the name of the temporary file appear at the end of the screen. I tried to fiddle with the less
options to remove it, without success so far.
Closing words
The current solution is not perfect, but it works like a charm for my use case. I wonder if this is a use case that could be desired for more peopel; in which case it could be interesting to bring it into the library.
During my work, I even found my first Intellij bug when playing around with ZWJ sequence emojis. Quite a learning experience for me :).
Let me know on Twitter if you have comments, or create an issue in the repository if you see something weird with swacli. And give a shot to picocli, it's fun!
Top comments (0)