Koodies 1.5: Execute Commands, Scripts & Containers the Kotlin-way

Koodies 1.5 is out now and allows you to run command lines and shell scripts, either on your host or in a Docker container of your choice, synchronously or asynchronously, optionally logging and even while interacting with the created process—all of this with zero boilerplate.

Background

The Java’s Development Kit allows developers to execute native OS processes using Runtime.exec since its first release. As of Java 1.5/5.0 the newly introduced ProcessBuilder rendered process handling easier; as did subsequent feature additions like ProcessHandle.

Apache Commons Exec was somewhat a breakthrough as it took away one of the biggest challenges programmers face with processes—reading their standard output and error. Furthermore Apache Commons Exec supports parameter substitution and helps coping with concurrency (e.g. DefaultExecutor, ExecuteWatchdog). Yet, a couple of everyday tasks are still not easily achievable.

ZT Process Executor is a process executor that makes a lot of things better. It has meta logging, which greatly helps at debugging and provides “one-liners” (that are rather “one-line-ishs” because of line length and exception handling boilerplate), as one can see in the following example:

String output;
boolean success = false;
try {
    output = new ProcessExecutor().command("java", "-version")
                  .readOutput(true).exitValues(3)
                  .execute().outputUTF8();
    success = true;
} catch (InvalidExitValueException e) {
    System.out.println("Process terminated with " + e.getExitValue());
    output = e.getResult().outputUTF8();
}

Unfortunately when it comes to script, no simple solutions seem to exist at all. Also libraries taking advantage of Kotlin features are not known.

This is where Koodies Exec jumps in.
The following snippet provides the same functionality as the code from:

val exec = CommandLine("java", "-version").exec()
     .apply { if(state is Failed) println(exitCode) }
val (output, success) = exec.io.out to exec.successful

Features

Execute Command Lines on Host

CommandLine("printenv", "HOME")
  .exec() // .exec.logging() // .exec.processing { io -> … }

Execute Shell Scripts on Host

ShellScript {
  "printenv | grep HOME | perl -pe 's/.*?HOME=//'"
}.exec() // .exec.logging() // .exec.processing { io -> … }

Execute Command Lines in Docker Container

CommandLine("printenv", "HOME")
  .dockerized{ "ubuntu" }
  .exec() // .exec.logging() // .exec.processing { io -> … }

Execute Shell Scripts in Docker Container

ShellScript {
  "printenv | grep HOME | perl -pe 's/.*?HOME=//'"
}.dockerized{ "ubuntu" }
 .exec() // .exec.logging() // .exec.processing { io -> … }

Logging and Interaction

As you can see in the above examples, three execution variants exist:

▶️ executing-only

ShellScript { … }.exec()

📝 logging

ShellScript { … }.exec.logging()

If things go wrong, it’s also logged:

Process {PID} terminated with exit code {…}
➜ A dump has been written to:
  - {WorkDir}/koodies.dump.{}.log
  - {WorkDir}/koodies.dump.{}.ansi-removed.log
➜ The last 10 lines are:
  {…}
  3
  2
  1
  Boom!

🧠 processing

ShellScript { … }.exec.processing { io -> 
  doSomething(io)
}

io is typed; simply use io is IO.Output to filter out errors and meta information.

Asynchronous Execution

Processes are executed synchronously by default. Simply add async to the exec call to run the process asynchronously:

  • ShellScript { … }.exec.async()
  • ShellScript { … }.exec.async.logging()
  • ShellScript { … }.exec.async.processing { io -> doSomething(io) }

Automatically Captured I/O

Whatever variant you choose, life-cycle events, sent input, the process’s output and errors are stored for you:

  • CommandLine(…).exec().io
  • CommandLine(…).exec().io.output
  • CommandLine(…).exec().io.error.ansiRemoved

Typed (Exit) State

  • Access the state with state, which is either an instance of RunningExited (with the sub states Succeeded and Failed) or Excepted.
  • All states print nicely and provide a copy of all logged I/O, and state-dependent information such as the exit code.
  • By default, processes are killed on VM shutdown, which can be configured.
  • Life-cycle callbacks can be registered.

Ready to run Docker commands

with(tempDir()) {
    SvgFile.copyTo(resolve("koodies.svg"))

    // convert SVG to PNG using command line-style docker command
    docker("minidocks/librsvg", "-z", 5, "--output", "koodies.png", "koodies.svg")

    // convert PNG to ASCII art using shell script-style docker command
    docker("rafib/awesome-cli-binaries", logger = null) {
        """
           /opt/bin/chafa -c full -w 9 koodies.png
        """
    }.io.output.ansiKept.let { println(it.resetLines()) }
}

Output

&kyTTTTTTTTTTTTTTTTTTTTuvvvvvvvvvvvvvvvvvvvvvvvv\.
RR&kyTTTTTTTTTTTTTTTTTvvvvvvvvvvvvvvvvvvvvvvvv\.
BBRR&kyTTTTTTTTTTTTTvvvvvvvvvvvvvvvvvvvvvvvv\.
BBBBRR&kyTTTTTTTTTvvvvvvvvvvvvvvvvvvvvvvvv\.
BBBBBBRR&kyTTTTTvvvvvvvvvvvvvvvvvvvvvvvv\.
BBBBBBBBRR&kyTx}vvvvvvvvvvvvvvvvvvvvvv\.
BBBBBBBBBBRZT}vvvvvvvvvvvvvvvvvvvvvv\.
BBBBBBBBBBQxvvvvvvvvvvvvvvvvvvvvvv\.
BBBBBBBB&xvvvvvvvvvvvvvvvvvvvvvv\.
BBBBBBZzvvvvvvvvvvvvvvvvvvvvvv\.
BBBBZuvvvvvvvvvvvvvvvvvvvvvv▗▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
BBZTvvvvvvvvvvvvvvvvvvvvvv\.▝▜MMMMMMMMMMMMMMMMMMMM
R3vvvvvvvvvvvvvvvvvvvvvv\. .▝▜MMMMMMMMMMMMMMMMMM
vvvvvvvvvvvvvvvvvvvvvv\. .▝▜MMMMMMMMMMMMMMMM
vvvvvvvvvvvvvvvvvvvv\. .▝▜MMMMMMMMMMMMMM
uxvvvvvvvvvvvvvvvvz3x_ ▝▜MMMMMMMMMMMM
▁3uxvvvvvvvvvvvv▁▅&▆▂gx` ▝▜MMMMMMMMMM
Z▅▁3uxvvvvvvvvz▆WWRZ&▆▂gv. `▀WMMMMMMMM
WR&▄▁3uxvvvvvuk▀BWWWRZ&▆▂gv. .\vvz▀WMMMMMM
WWWRZ▅▁3ux▁▂Zg33k▀BWWWRZ&▆▂g}. .\vvvvvvz▀WMM0W
000WWRZ▅▃▆MM▆▂Zg33k▀BWWWRZ&▆▂g}. .\vvvvvvvvvvx▀BBR
00000WMMMMMMMM▆▂Zg33k▀BWWWRZ&▆▂yxxvvvvvvvvvvvvvx▝▀
0000MMMMMMMMMMMM▆▂Zg33k▀BWWWRZ▆▆▂gTxvvvvvvvvvvvvvx
00MMMMMMMMMMMMMMMM▆▂Zg33k▀BWWWRZ&▆▂gTxvvvvvvvvvvvv
MMMMMMMMMMMMMMMMMMMM▆▂Zg33g▀BWWWRZ&▆▂gTxvvvvvvvvvv
  • All docker commands (dockerubuntubusyboxcurldownload, …) use the path in the receiver to
    • set the working directory of both the host command and the docker container
    • map the host working directory to the container’s working directory,
    • that is, all files of that directory are equally available in your container instance.
  • Low-level docker commands: startrunstopkillremovesearchimageps
  • Object-oriented design
    • DockerengineRunninginfoimagescontainerssearchexec
    • DockerImagelistisPulledpulltagsOnDockerHub
    • DockerContainerstartstopstatekillremove
  • See ExecutionIntegrationTest.kt and Docker.kt for more examples.

Installation

Koodies is a Kotlin Multiplatform Library, using the hierarchical project structure, a minimal set of dependencies, and is hosted on GitHub with releases provided on Maven Central.

Gradle

implementation("com.bkahlert:koodies:1.4.2")

Maven

<dependency>
  <groupId>com.bkahlert</groupId>
  <artifactId>koodies</artifactId>
  <version>1.5.0</version>
</dependency>