All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.KommandWrappers.kt Maven / Gradle / Ivy

There is a newer version: 0.0.87
Show newest version
@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) } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy