Skip to content

Kommons is a set of Kotlin Multiplatform Libraries (MPP) to allow the execution of command lines / scripts, to support print debugging and to ease testing.

License

Notifications You must be signed in to change notification settings

bkahlert/kommons

Repository files navigation

Koodies — Random Kotlin Goodies

Koodies Download from Maven Central Download from GitHub Packages Repository Size Repository Size

Koodies is a Kotlin Multiplatform Library, with a minimal set of dependencies, allowing you to run Command Lines and Shell Scripts, locally or in a Docker Container—and a dozen of other features like various builders, an improved Java NIO 2 integration, decimal and binary units, and Unicode-related features.

Installation / Setup

Koodies is hosted on GitHub with releases provided on Maven Central.

  • Gradle implementation("com.bkahlert.koodies:koodies:1.9.0")

  • Maven

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

Features

OpenTelemetry Integration (since 1.6.0)

The observability library OpenTelemetry is natively supported. Simply start a process and watch for yourself:

DockerPi-based Test

For manual instrumentation, the spanning function is provided:

spanning("span name") {
    event("test event", Key.stringKey("test attribute") to "test value")
    log("description") // = event("log", RenderingAttributes.DESCRIPTION to description)
    42 // = return value
}

Simple Span with two events

By default, the span and all events with a description are also printed to the console:

╭──╴span name
│
│   description                                                                 
│
╰──╴✔︎

Kotlin 1.5 Duration Extensions (since 1.5.1)

Those of you missing the duration extension removed in Kotlin 1.5 may sigh of relief, since Koodies 1.5.1 brings them back:

42.days + 42.hours + 42.minutes + 42.seconds // 43.8d
42.milli.seconds + 450.micro.seconds + 50_000.nano.seconds // 42.5ms 

Exec: Feature-Rich Process Execution (since 1.5.0)

⌨️ 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" }
    .dockerized { "ubuntu" }
    .exec() // .exec.logging() // .exec.processing { io -> … }

or even simpler

with(tempDir()) { // working directory provided via receiver
    ubuntu("printenv", "HOME") // busybox
        .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 -> … }

or even simpler

with(tempDir()) { // working directory provided via receiver
    ubuntu { "printenv | grep HOME | perl -pe 's/.*?HOME=//'" } // busybox
        .exec() // .exec.logging() // .exec.processing { io -> … }
}

How can you run?

Degree of Interaction
▶️ executing-only
CommandLine("") // ShellScript { … }
    .exec()
📝 logging
CommandLine("") // ShellScript { … }
    .exec.logging()
  • If things go wrong, it's also logged:
    Process {PID} terminated with exit code {…}
    ➜ A dump has been written to:
    - {TempDir}/koodies/exec/dump.{}.log
    - {TempDir}/koodies/exec/dump.{}.ansi-removed.log
      ➜ The last 10 lines are:
      {…}
      3
      2
      1
      Boom!
    
🧠 processing
CommandLine("") // ShellScript { … }
    .exec.processing { io -> doSomething(io) }`
  • io is typed; simply use io is IO.Output to filter out errors and meta information
Synchronicity
👯‍♀️ 👯‍♂️ synchronous
CommandLine("") // ShellScript { … }
    .exec() // .exec.logging() // .exec.processing { io -> … }
💃 🕺 asynchronous
CommandLine("") // ShellScript { … }
    .exec.async() // .exec.async.logging() // .exec.async.processing { io -> … }

Features

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 Running, Exited (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.resetLines().let { println(it) }
}
Output
⧹kyTTTTTTTTTTTTTTTTTTTTuvvvvvvvvvvvvvvvvvvvvvvvv.  
RR⧹kyTTTTTTTTTTTTTTTTTvvvvvvvvvvvvvvvvvvvvvvvv.
BBRR⧹kyTTTTTTTTTTTTTvvvvvvvvvvvvvvvvvvvvvvvv.
BBBBRR⧹kyTTTTTTTTTvvvvvvvvvvvvvvvvvvvvvvvv.
BBBBBBRR⧹kyTTTTTvvvvvvvvvvvvvvvvvvvvvvvv.
BBBBBBBBRR⧹kyTx/vvvvvvvvvvvvvvvvvvvvvv.
BBBBBBBBBBRZ⧹/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 (docker, ubuntu, busybox, curl, download, nginx, …) 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: start, run, stop, kill, remove, search, image, ps
  • Object-oriented design
    • Docker: engineRunning, info, images, containers, search, exec
    • DockerImage: list, isPulled, pull, tagsOnDockerHub
    • DockerContainer: start, stop, state, kill, remove
  • See ExecutionIntegrationTest.kt and Docker.kt for more examples.

Multi-Platform Builders

Array Builder

val array = buildArray {
    add("test")
    add("𓌈🥸𓂈")
}

List Builder

val array = buildList {
    add("test")
    add("𓌈🥸𓂈")
}

Set Builder

val array = buildSet {
    add("test")
    add("𓌈🥸𓂈")
}

Map Builder

val array = buildMap {
    "ten" to 3
    "𓌈🥸𓂈".let { it to it.length }
}

IP Address Tooling (4 & 6)

val ip4 = ipOf<IPv4Address>("192.168.16.25")
val ip6 = ip4.toIPv6Address()
val range = ip6.."::ffff:c0a8:1028".toIp() // ::ffff:c0a8:1019..::ffff:c0a8:1028
val subnet = ip6 / 122 // ::ffff:c0a8:1000/122
check(range.smallestCommonSubnet == subnet) // ✔︎
check(subnet.broadcastAddress.toInetAddress().isSiteLocalAddress) // ✔︎

Improved Java NIO 2 Integration

Access the Class Path

classPath("file.svg").copyTo(somewhere)

Copy and Delete Recursively

directory.copyRecursivelyTo(somewhere)
directory.deleteRecursively()

Fluent API

if (path.notExists()) path.createParentDirectories().createFile()

Units

Kotlin 1.5 Duration Extensions

42.days
42.hours
42.minutes
42.seconds
42.milli.seconds
42.micro.seconds
42.nano.seconds

Decimal and Binary Bytes

10.Yobi.bytes > 10.Yotta.bytes

Arithmetics

3.Tera.bytes + 200.Giga.bytes == 3.2.Tera.bytes
2 * 3.Kibi.bytes == 6.Kibi.bytes

File and Directory Size

Path.of("/tmp").getSize() // 1.9 TB
listOf(largeFile, smallFile, mediumFile).sortedBy { it.getSize() }

From and to String

1.25.Mega.bytes.toString() == "1.25 MB"
"1.25 MB".toSize() == 1.25.Mega.bytes
4_200_000.Yobi.bytes.toString(BinaryPrefix.Mebi, 4) == "4.84e+24 MiB"

Useless Nerd Stuff

42.hecto.bytes
42.mebi.days

Unicode

Code Points and Grapheme Clusters

// Process each actual character (and not each `char`)
"aⒷ☷\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67".asCodePointSequence {
    println(it) // "a", "Ⓑ", "☷", ":woman:" ZWJ, ":woman:", ZWJ, ":girl:", ZWJ, ":girl:"
}
LineSeparators.toList() == listOf(
    LineSeparators.CRLF, // carriage return + line feed (\\r\\n)
    LineSeparators.LF,   // line feed (\\n)
    LineSeparators.CR,   // carriage return (\\r)
    LineSeparators.NL,   // next line 
    LineSeparators.PS,   // paragraph separator 
    LineSeparators.LS,   // line separator
)
Split string into its lines…
"""
line 1
line 2
  
""".lines() // line 1, line 2 
Split string into its lines lazily and keep the line separator…
"""
line 1
line 2

""".lineSequence(keepDelimiters = true) // line 1⏎␤, line 2⏎␍␊ 

Debugging

Check if your program currently runs in debugging mode.

if (isDebugging) {
  …
}

Use debug to check what's actually inside a String:

"a  b\n".debug // a ❲THREE-PER-EM SPACE❳ b ⏎␊
"" // D800▌﹍ (low surrogate with a missing high surrogate)

Use trace to print stuff without interrupting the call chain:

chain().of.endless().calls()

// print return value of endless()
chain().of.endless().trace.calls()

// prints return value of endless() formatted with debug
chain().of.endless().trace { debug }.calls() 
  • Never look for orphaned print statements again. trace is declared as deprecated and inflicts a build warning.
    w: Koodies.kt: (42, 15): 'trace: T' is deprecated. Don't forget to remove after you finished debugging.
  • trace has replaceWith set so that in IntelliJ the cleanup action removes all trace statements in one stroke.
  • Each trace statement prints the file and line it was called at.
    .ͥ (Koodies.kt:42) ⟨ … ⟩

Upgrading

# upgrade gradle
./gradlew wrapper --gradle-version=7.0.2 --distribution-type=bin

Releasing
RELEASING.md

About

Kommons is a set of Kotlin Multiplatform Libraries (MPP) to allow the execution of command lines / scripts, to support print debugging and to ease testing.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Languages