Hey there,
I am creating an issue based on a question I raised a few weeks back on Twitter.
I currently have a fun little API that returns results from the Star Wars API. Depending on the query, it returns a lot of results.
I would like to be able to present the results paginated, like Git does it. Like @remkop already mentions it on Twitter, it would be best to leverage already existing infrastructure on the system rather than implement a local pagination.
I've been fiddling around with code for the past few days but I'm afraid I didn't manage to come up with anything conclusive yet. The twitter thread seems to mention IExecutionHandler but I couldn't find anything related in the source code of picocli, so I have been looking at creating a custom IParseResultHandler instead. Past that, I also ran into stdin forwarding issues with ProcessBuilder, so it's hard to come with a minimal reproducible example.
Any help moving in the right direction would be most appreciated!
Just to be clear, I do have some code lying around that attempts to do what is described in the Twitter thread, I simply didn't get it to function as expected just yet.
Most of the noteworthy code is quite small :
private fun executionStrategy(parseResult: ParseResult): Int {
println("in there")
val processBuilder = ProcessBuilder("less")
val process = processBuilder.start()
this.spec.commandLine().out = PrintWriter(process.outputStream)
return RunLast().execute(parseResult)
}
companion object{
@JvmStatic
fun main(args: Array<String>){
val app = SwaCLIPaginateDummy()
exitProcess(CommandLine(app)
.setExecutionStrategy(app::executionStrategy)
.execute(*args))
}
}
I tried to strip down as many things as possible in there. Example to be ran with the "planets" argument to trigger the printing of the dummy string
Thank you for raising this!
There are other commitments that I have to take care of first, but I will take a look.
EDIT: sorry, on Twitter I said IExecutionHandler, I meant IExecutionStrategy. But it looks like you already figured that out. :+1:
No hurry really, this is not a priority. I keep tinkering with it and I think someone used to the library might be going faster. Thanks!
No success yet. One small thing:
The planets command should not write to STDOUT, but to the output stream of the commandline:
@Command(name = "planets", description = ["Search for planets"])
class PlanetsCommandPaginateDummy : Callable<Int> {
@Spec
lateinit var spec: CommandSpec
override fun call(): Int {
spec.commandLine().out.println(dummyLongString)
return 0
}
}
I tried writing to a temp file, but no joy yet...
I got it to work with a temp file and by calling inheritIO() on the ProcessBuilder, and calling waitFor() on the Process. You may want to delete the temp file when done.
package nl.lengrand.swacli
import picocli.CommandLine
import picocli.CommandLine.*
import picocli.CommandLine.Model.CommandSpec
import java.io.*
import java.nio.file.Files
import java.util.concurrent.Callable
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
const val dummyLongString =
"""
Found 60 total results for that query
Showing 10 results
Tatooine
Climate : arid
Diameter (km) : 10465
Gravity : 1 standard
Orbital period : 304
Alderaan
Climate : temperate
Diameter (km) : 12500
Gravity : 1 standard
Orbital period : 364
Yavin IV
Climate : temperate, tropical
Diameter (km) : 10200
Gravity : 1 standard
Orbital period : 4818
Hoth
Climate : frozen
Diameter (km) : 7200
Gravity : 1.1 standard
Orbital period : 549
Dagobah
Climate : murky
Diameter (km) : 8900
Gravity : N/A
Orbital period : 341
Bespin
Climate : temperate
Diameter (km) : 118000
Gravity : 1.5 (surface), 1 standard (Cloud City)
Orbital period : 5110
Endor
Climate : temperate
Diameter (km) : 4900
Gravity : 0.85 standard
Orbital period : 402
Naboo
Climate : temperate
Diameter (km) : 12120
Gravity : 1 standard
Orbital period : 312
Coruscant
Climate : temperate
Diameter (km) : 12240
Gravity : 1 standard
Orbital period : 368
Kamino
Climate : temperate
Diameter (km) : 19720
Gravity : 1 standard
Orbital period : 463
"""
@Command(
mixinStandardHelpOptions = true,
subcommands = [PlanetsCommandPaginateDummy::class, HelpCommand::class]
)
class SwaCLIPaginateDummy : Callable<Int> {
@Spec
lateinit var spec: CommandSpec
private fun executionStrategy(parseResult: ParseResult): Int {
println("in there")
val file = Files.createTempFile("pico", ".tmp").toFile()
this.spec.commandLine().out = PrintWriter(FileWriter(file), true)
println("writing to ${file}")
val result = RunLast().execute(parseResult)
this.spec.commandLine().out.flush() // may not be needed
println("starting `less ${file.absolutePath}`")
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 = SwaCLIPaginateDummy()
exitProcess(CommandLine(app)
.setExecutionStrategy(app::executionStrategy)
.execute(*args))
}
}
}
@Command(name = "planets", description = ["Search for planets"])
class PlanetsCommandPaginateDummy : Callable<Int> {
@Spec
lateinit var spec: CommandSpec
override fun call(): Int {
spec.commandLine().out.println(dummyLongString)
return 0
}
}
Thanks for the help! I didn't think about using a temp file, good idea
I can confirm indeed that this does the trick. A few limitations I can see when testing :

Thanks again, I don't expect you to test all the comments here :), I'm listing them in case someone else ends up here at some point. I'll be looking if I can improve those myself.
Ok, so getting around the usage and help part is quite simple. You gotta avoid the mambo jambo in case no subcommand is detected.
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)
this.spec.commandLine().out.flush() // may not be needed
val processBuilder = ProcessBuilder("less", file.absolutePath).inheritIO()
val process = processBuilder.start()
process.waitFor()
return result
}
would work in that case. It could become more complex in a real life application.
Thanks for all the help @remkop. I used your changes and the working example can be found here https://github.com/jlengrand/swacli/blob/main/src/main/kotlin/nl/lengrand/swacli/SwaCLIPaginate.kt.
It is possible to play around with the less options to get more or less info in the output for those that would want to dive deeper into it :).