Paginate results in a command line application using picoCLI

In this article, I describe a way to paginate results in picocli applications.

Paginate results in a command line application using picoCLI

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 list of projects using picocli, such as spring, aws, micronaut or hadoop

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 temporary FileWriter
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))
        }
    }
}
an example including pagination in kotlin

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!