Kotlin-native execute command and get the output
Asked Answered
A

4

11

I'd like to know if there's a way in kotlin native to call a command via posix and receive it's terminal output. For example, I'd like to get the "git diff" command working without having to create a temporary file, write output to it and then read from that file.

On SO I've only found solutions requiring ProcessBuilder, which isn't available on kotlin-native, as it's a Java library.

Augmentation answered 20/7, 2019 at 10:27 Comment(0)
S
5

It's an improved version of exec command for Kotlin Native posted by mg-lolenstine, it throws an exception with command stderr instead of just returning exit(1) (which not always desirable behavior itself), also trim is now optional

import kotlinx.cinterop.*
import platform.posix.*

fun executeCommand(
    command: String,
    trim: Boolean = true,
    redirectStderr: Boolean = true
): String {
    val commandToExecute = if (redirectStderr) "$command 2>&1" else command
    val fp = popen(commandToExecute, "r") ?: error("Failed to run command: $command")

    val stdout = buildString {
        val buffer = ByteArray(4096)
        while (true) {
            val input = fgets(buffer.refTo(0), buffer.size, fp) ?: break
            append(input.toKString())
        }
    }

    val status = pclose(fp)
    if (status != 0) {
        error("Command `$command` failed with status $status${if (redirectStderr) ": $stdout" else ""}")
    }

    return if (trim) stdout.trim() else stdout
}
Simonsen answered 12/10, 2020 at 3:3 Comment(1)
this doesn't work on windows though :( github.com/jmfayard/kotlin-cli-starter/issues/… Any hint?Selftaught
A
13

I found a working piece of code I wanted to use, so I'm posting it here for future viewers!

fun executeCommand(command: String): String{
    val fp: CPointer<FILE>? = popen(command, "r")
    val buffer = ByteArray(4096)
    val returnString = StringBuilder()

    /* Open the command for reading. */
    if (fp == NULL) {
        printf("Failed to run command\n" )
        exit(1)
    }

    /* Read the output a line at a time - output it. */
    var scan = fgets(buffer.refTo(0), buffer.size, fp)
    if(scan != null) {
        while (scan != NULL) {
            returnString.append(scan!!.toKString())
            scan = fgets(buffer.refTo(0), buffer.size, fp)
        }
    }
    /* close */
    pclose(fp)
    return returnString.trim().toString()
}
Augmentation answered 20/7, 2019 at 12:57 Comment(1)
this doesn't work on windows though :( github.com/jmfayard/kotlin-cli-starter/issues/… Any hint?Selftaught
E
6

Usually you can use the POSIX api and use fork and wait and some I/O related functions for your purpose

fun main() {
    val childPid: pid_t = fork()
    if (childPid == 0) {
        val commands = listOf("git", "diff", "HEAD^1", "$projectDir/path/to/file", null)
        val cwd = "$projectDir"
        chdir(cwd)
        memScoped {
            execvp(commands[0], allocArrayOf(commands.map { it?.cstr?.ptr }))
        }
    } else {
        wait(null)
    }
}

Of course, this needs to deal with a lot of c-style code, so I also wrote a more practical library for this

repositories {
    mavenCentral()
}
// add dependencies into your native target sourceSet
dependencies {
    implementation("com.kgit2:kommand:1.0.1")
}

It is also very simple to use

fun main(args: Array<String>) {
    val diffResult = Command("git")
    .args("diff", "HEAD^1", "$projectDir/path/to/file")
    .cwd("$projectDir")
    .spawn()
    .output()
}
Eluviation answered 31/10, 2022 at 3:15 Comment(1)
If this answer still doesn't meet the forum specs, I'll revise itEluviation
S
5

It's an improved version of exec command for Kotlin Native posted by mg-lolenstine, it throws an exception with command stderr instead of just returning exit(1) (which not always desirable behavior itself), also trim is now optional

import kotlinx.cinterop.*
import platform.posix.*

fun executeCommand(
    command: String,
    trim: Boolean = true,
    redirectStderr: Boolean = true
): String {
    val commandToExecute = if (redirectStderr) "$command 2>&1" else command
    val fp = popen(commandToExecute, "r") ?: error("Failed to run command: $command")

    val stdout = buildString {
        val buffer = ByteArray(4096)
        while (true) {
            val input = fgets(buffer.refTo(0), buffer.size, fp) ?: break
            append(input.toKString())
        }
    }

    val status = pclose(fp)
    if (status != 0) {
        error("Command `$command` failed with status $status${if (redirectStderr) ": $stdout" else ""}")
    }

    return if (trim) stdout.trim() else stdout
}
Simonsen answered 12/10, 2020 at 3:3 Comment(1)
this doesn't work on windows though :( github.com/jmfayard/kotlin-cli-starter/issues/… Any hint?Selftaught
S
1

playing around I used above's answers to create a working gradle kotlin native/jvm multiplatform multiproject that runs arbitrary local Processes/Commands:

here's my result:

https://github.com/hoffipublic/minimal_kotlin_multiplatform

import kotlinx.cinterop.refTo
import kotlinx.cinterop.toKString
import platform.posix.fgets
import platform.posix.pclose
import platform.posix.popen

actual object MppProcess : IMppProcess {
    actual override fun executeCommand(
        command: String,
        redirectStderr: Boolean
    ): String? {
        val commandToExecute = if (redirectStderr) "$command 2>&1" else command
        val fp = popen(commandToExecute, "r") ?: error("Failed to run command: $command")

        val stdout = buildString {
            val buffer = ByteArray(4096)
            while (true) {
                val input = fgets(buffer.refTo(0), buffer.size, fp) ?: break
                append(input.toKString())
            }
        }

        val status = pclose(fp)
        if (status != 0) {
            error("Command `$command` failed with status $status${if (redirectStderr) ": $stdout" else ""}")
        }

        return stdout
    }
}

on jvm

import java.util.concurrent.TimeUnit

actual object MppProcess : IMppProcess {
    actual override fun executeCommand(
        command: String,
        redirectStderr: Boolean
    ): String? {
        return runCatching {
            ProcessBuilder(command.split(Regex("(?<!(\"|').{0,255}) | (?!.*\\1.*)")))
                //.directory(workingDir)
                .redirectOutput(ProcessBuilder.Redirect.PIPE)
                .apply { if (redirectStderr) this.redirectError(ProcessBuilder.Redirect.PIPE) }
                .start().apply { waitFor(60L, TimeUnit.SECONDS) }
                .inputStream.bufferedReader().readText()
        }.onFailure { it.printStackTrace() }.getOrNull()
    }
}
Socorrosocotra answered 26/4, 2021 at 12:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.