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.
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>
The observability library OpenTelemetry is natively supported. Simply start a process and watch for yourself:
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
}
By default, the span and all events with a description are also printed to the console:
╭──╴span name
│
│ description
│
╰──╴✔︎
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
CommandLine("printenv", "HOME")
.exec() // .exec.logging() // .exec.processing { io -> … }
ShellScript { "printenv | grep HOME | perl -pe 's/.*?HOME=//'" }
.exec() // .exec.logging() // .exec.processing { io -> … }
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 -> … }
}
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 -> … }
}
CommandLine("…") // ShellScript { … }
.exec()
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!
CommandLine("…") // ShellScript { … }
.exec.processing { io -> doSomething(io) }`
io
is typed; simply useio is IO.Output
to filter out errors and meta information
CommandLine("…") // ShellScript { … }
.exec() // .exec.logging() // .exec.processing { io -> … }
CommandLine("…") // ShellScript { … }
.exec.async() // .exec.async.logging() // .exec.async.processing { io -> … }
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
- Access the state with
state
, which is either an instance ofRunning
,Exited
(with the sub statesSucceeded
andFailed
) orExcepted
. - 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.
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) }
}
⧹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
- Docker:
- See ExecutionIntegrationTest.kt and Docker.kt for more examples.
val array = buildArray {
add("test")
add("𓌈🥸𓂈")
}
val array = buildList {
add("test")
add("𓌈🥸𓂈")
}
val array = buildSet {
add("test")
add("𓌈🥸𓂈")
}
val array = buildMap {
"ten" to 3
"𓌈🥸𓂈".let { it to it.length }
}
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) // ✔︎
classPath("file.svg").copyTo(somewhere)
directory.copyRecursivelyTo(somewhere)
directory.deleteRecursively()
if (path.notExists()) path.createParentDirectories().createFile()
42.days
42.hours
42.minutes
42.seconds
42.milli.seconds
42.micro.seconds
42.nano.seconds
10.Yobi.bytes > 10.Yotta.bytes
3.Tera.bytes + 200.Giga.bytes == 3.2.Tera.bytes
2 * 3.Kibi.bytes == 6.Kibi.bytes
Path.of("/tmp").getSize() // 1.9 TB
listOf(largeFile, smallFile, mediumFile).sortedBy { it.getSize() }
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"
42.hecto.bytes
42.mebi.days
// 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
)
"""
line 1
line 2
""".lines() // line 1, line 2
"""
line 1
line 2
""".lineSequence(keepDelimiters = true) // line 1⏎, line 2⏎␍␊
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