How to invoke external command from within Kotlin code?

Solution 1:

Example of running a git diff by shelling out:

"git diff".runCommand(gitRepoDir)

Here are two implementations of the runCommand extension function:

1. Redirect to stdout/stderr

This wires any output from the subprocess to regular stdout and stderr:

fun String.runCommand(workingDir: File) {
    ProcessBuilder(*split(" ").toTypedArray())
                .directory(workingDir)
                .redirectOutput(Redirect.INHERIT)
                .redirectError(Redirect.INHERIT)
                .start()
                .waitFor(60, TimeUnit.MINUTES)
}

2. Capturing output as a String

An alternative implementation redirecting to Redirect.PIPE instead allows you to capture output in a String:

fun String.runCommand(workingDir: File): String? {
    try {
        val parts = this.split("\\s".toRegex())
        val proc = ProcessBuilder(*parts.toTypedArray())
                .directory(workingDir)
                .redirectOutput(ProcessBuilder.Redirect.PIPE)
                .redirectError(ProcessBuilder.Redirect.PIPE)
                .start()

        proc.waitFor(60, TimeUnit.MINUTES)
        return proc.inputStream.bufferedReader().readText()
    } catch(e: IOException) {
        e.printStackTrace()
        return null
    }
}

Solution 2:

If you're running on the JVM you can just use Java Runtime exec method. e.g.

Runtime.getRuntime().exec("mycommand.sh")

You will need to have security permission to execute commands.

Solution 3:

Based on @jkschneider answer but a little more Kotlin-ified:

fun String.runCommand(
    workingDir: File = File("."),
    timeoutAmount: Long = 60,
    timeoutUnit: TimeUnit = TimeUnit.SECONDS
): String? = runCatching {
    ProcessBuilder("\\s".toRegex().split(this))
        .directory(workingDir)
        .redirectOutput(ProcessBuilder.Redirect.PIPE)
        .redirectError(ProcessBuilder.Redirect.PIPE)
        .start().also { it.waitFor(timeoutAmount, timeoutUnit) }
        .inputStream.bufferedReader().readText()
}.onFailure { it.printStackTrace() }.getOrNull()

Update: If you are in Gradle Groovy to Kotlin DSL migration all you need is this, less code you should care about and would have less unexpected (e.g. on quoted parameters) results than above code may have,

import org.codehaus.groovy.runtime.ProcessGroovyMethods

ProcessGroovyMethods.getText(ProcessGroovyMethods.execute("ls"))

// Or,
fun String.execute() = ProcessGroovyMethods.execute(this)
val Process.text: String? get() = ProcessGroovyMethods.getText(this)
// then just like groovy, "ls".execute().text