commonMain.KommandWrappers.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kommandline Show documentation
Show all versions of kommandline Show documentation
Kotlin DSL for popular CLI commands.
@file:Suppress("unused")
package pl.mareklangiewicz.kommand
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import okio.Path
import pl.mareklangiewicz.annotations.DelicateApi
import pl.mareklangiewicz.kground.io.localUWorkDirOrNull
/**
* Separate from Kommand, because I want Kommand to be simple and serializable!
* TODO_someday_maybe: Use kotlinx-serialization for all Kommands (but not TypedKommands)
* Note1: TypedKommand assumes no redirections at CLI level.
* Instead, just compose stdin/out/err data types (usually flows) (it will result in better code in most cases).
* If CLI level redirections are really needed, then just use lower level api.
* But first, consider if you can just locally save/load flows to/from files using Okio.
* (Overall goal is to gradually move AWAY from CLI craziness and more towards safe/composable kotlin programming.)
*/
data class TypedKommand(
val kommand: K,
val stdinRetype: StdinCollector.() -> In,
val stderrRetype: Flow.() -> Err,
val stderrToOut: Boolean,
val stdoutRetype: Flow.() -> Out,
)
fun K.typed(
stdinRetype: StdinCollector.() -> In,
stderrRetype: Flow.() -> Err,
stderrToOut: Boolean = false,
stdoutRetype: Flow.() -> Out,
) = TypedKommand(this, stdinRetype, stderrRetype, stderrToOut, stdoutRetype)
// these default retype algorithms/vals are defined here mostly for me to be able to compare when debugging/testing
internal val defaultInRetypeToItSelf: StdinCollector.() -> StdinCollector = { this }
internal val defaultOutRetypeToItSelf: Flow.() -> Flow = { this }
fun K.typed(
stderrToOut: Boolean = false,
stdoutRetype: Flow.() -> Out,
): TypedKommand> =
typed(
stdinRetype = defaultInRetypeToItSelf,
stderrRetype = defaultOutRetypeToItSelf,
stderrToOut = stderrToOut,
stdoutRetype = stdoutRetype,
)
class TypedExecProcess(
private val eprocess: ExecProcess,
stdinRetype: StdinCollector.() -> In,
stderrRetype: Flow.() -> Err,
stdoutRetype: Flow.() -> Out,
) {
fun kill(forcibly: Boolean = false) = eprocess.kill(forcibly)
suspend fun awaitExit(finallyClose: Boolean = true) = eprocess.awaitExit(finallyClose)
val stdin: In = eprocess.stdin.stdinRetype()
val stdout: Out = eprocess.stdout.stdoutRetype()
val stderr: Err = eprocess.stderr.stderrRetype()
}
suspend fun TypedExecProcess<*, *, Flow>.awaitAndChkExit(
firstCollectErr: Boolean,
finallyClose: Boolean = true,
testExit: Int.() -> Boolean = { this == 0 },
): Int {
val collectedErr: List? = if (firstCollectErr) stderr.toList() else null
return awaitExit(finallyClose).chkExit(testExit, collectedErr)
}
/**
* If unexpected exit, then it will normally throw [BadExitStateErr], but with stderr set to null (meaning: unknown).
* Usually it's better to capture stderr in some way, so think twice before choosing this extension function.
*/
@DelicateApi
suspend fun TypedExecProcess<*, *, *>.awaitAndChkExitIgnoringStdErr(
finallyClose: Boolean = true,
testExit: Int.() -> Boolean = { this == 0 },
) = awaitExit(finallyClose).chkExit(testExit)
/**
* @param workDir working directory for started subprocess - null means inherit from the current process
*/
fun CLI.lx(
kommand: TypedKommand,
workDir: Path? = null,
) = TypedExecProcess(
eprocess = lx(kommand = kommand.kommand, workDir = workDir, errToOut = kommand.stderrToOut),
stdinRetype = kommand.stdinRetype,
stderrRetype = kommand.stderrRetype,
stdoutRetype = kommand.stdoutRetype,
)
// TODO_someday: @CheckResult https://youtrack.jetbrains.com/issue/KT-12719
// TODO_someday: (When we have context receivers in MPP and it's time for bigger refactor):
// I introduced ReducedScript as a coy experiment already... will see
// this ReducedKommand interface is in fact more general contract - sth like "ReducedScript",
// that should also represent executing more kommands on some platform, not just one.
// Rethink if I need both fun interface ReducedScript, and just empty interface ReducedKommand : ReducedScript,
// or maybe ReducedScript is even enough and ReducedKommand could be deleted.
// Then ReducedKommandMap, etc. would also be just a specific form of ReducedScript.
fun interface ReducedScript {
suspend fun ax(): ReducedOut
// TODO_someday: @CheckResult https://youtrack.jetbrains.com/issue/KT-12719
}
interface ReducedKommand : ReducedScript
internal class ReducedKommandImpl(
val typedKommand: TypedKommand,
val reduceTEProcess: suspend TypedExecProcess.() -> ReducedOut,
) : ReducedKommand {
override suspend fun ax(): ReducedOut {
val cli = localCLI()
val dir = localUWorkDirOrNull()
return reduceTEProcess(cli.lx(typedKommand, workDir = dir?.dir))
}
}
internal class ReducedKommandMap(
val reducedKommand: ReducedKommand,
val reduceMap: suspend InnerOut.() -> MappedOut,
) : ReducedKommand {
override suspend fun ax(): MappedOut = reducedKommand.ax().reduceMap()
}
fun ReducedKommand.reducedMap(
reduceMap: suspend InnerOut.() -> MappedOut,
): ReducedKommand = ReducedKommandMap(this, reduceMap)
/** Mostly for tests to try to compare wrapped kommand line to expected line. */
@DelicateApi
fun ReducedKommand<*>.lineRawOrNull(): String? = when (this) {
is ReducedKommandImpl<*, *, *, *, *> -> typedKommand.kommand.lineRaw()
is ReducedKommandMap<*, *> -> reducedKommand.lineRawOrNull()
else -> null
}
/** Note: Manually means: user is responsible for collecting all necessary streams and awaiting and checking exit. */
fun TypedKommand.reducedManually(
reduceManually: suspend TypedExecProcess.() -> ReducedOut,
): ReducedKommand = ReducedKommandImpl(this, reduceManually)
/** Note: Manually means: user is responsible for collecting all necessary streams and awaiting and checking exit. */
fun K.reducedManually(
reduceManually: suspend TypedExecProcess, Flow>.() -> ReducedOut,
): ReducedKommand = typed(stdoutRetype = defaultOutRetypeToItSelf)
.reducedManually(reduceManually)
/**
* Note: reduceOut means: user is responsible only for reducing stdout;
* stderr and exit will be handled in the default way; stdin will not be used at all.
*/
fun TypedKommand>.reducedOut(
reduceOut: suspend Out.() -> ReducedOut,
): ReducedKommand = ReducedKommandImpl(this) {
coroutineScope {
val deferredErr = async { stderr.toList() }
val reducedOut = stdout.reduceOut()
val collectedErr = deferredErr.await()
awaitExit().chkExit(stderr = collectedErr)
reducedOut
}
}
fun K.reducedOut(
reduceOut: suspend Flow.() -> ReducedOut,
): ReducedKommand = this
.typed(stdoutRetype = defaultOutRetypeToItSelf)
.reducedOut(reduceOut)
fun K.reducedExit(
reduceExit: suspend (Int) -> ReducedExit,
): ReducedKommand = this
.typed(stdoutRetype = defaultOutRetypeToItSelf)
.reducedManually { reduceExit(awaitExit()) }
// These four below look unnecessary, but I like how they explicitly suggest common correct thing to do in the IDE.
fun K.reducedOutToUnit(): ReducedKommand = reducedOut {}
fun K.reducedOutToList(): ReducedKommand> = reducedOut { toList() }
fun K.reducedOutToFlow(): ReducedKommand> =
reducedManually { stdout.onCompletion { awaitAndChkExit(firstCollectErr = false) } }
fun TypedKommand<*, In, Flow, Flow>.reducedOutToList(): ReducedKommand> =
reducedOut { toList() }
fun TypedKommand<*, In, Out, Flow>.reducedOutToUnit(): ReducedKommand =
reducedOut {}
fun TypedKommand<*, In, Flow, Flow>.reducedOutToFlow(): ReducedKommand> =
reducedManually { stdout.onCompletion { awaitAndChkExit(firstCollectErr = false) } }