How to invoke external command from within Kotlin

2020-02-16 11:49发布

问题:

I want to invoke a external command from Kotlin code. In C/Perl, I would use system() function; In Python, I would use subprocess module; In Go, I would use use os/exec; and etc.. But, how do I do this in Kotlin?

回答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
    }
}


回答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.



回答3:

For Kotlin Native:

platform.posix.system("git status")

For JVM

Runtime.getRuntime().exec("git status")


回答4:

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

fun String.runCommand(
    workingDir: File = File("."),
    timeoutAmount: Long = 60,
    timeoutUnit: TimeUnit = TimeUnit.SECONDS
): String? = try {
    ProcessBuilder(split("\\s".toRegex()))
        .directory(workingDir)
        .redirectOutput(ProcessBuilder.Redirect.PIPE)
        .redirectError(ProcessBuilder.Redirect.PIPE)
        .start().apply { waitFor(timeoutAmount, timeoutUnit) }
        .inputStream.bufferedReader().readText()
} catch (e: java.io.IOException) {
    e.printStackTrace()
    null
}


回答5:

I wanted a few changes from the solution of jkschneider, as it didn't catch the error codes I got from the executed commands. Also I did a few refactorings to get this:

directory exec "git status"

or

directory.execute("git", "commit", "-m", "A message")

I also opted to throw exceptions for error codes and shortened the wait, but that can easily be altered according to taste.

/**
 * Shorthand for [File.execute]. Assumes that all spaces are argument separators,
 * so no argument may contain a space.
 * ```kotlin
 *  // Example
 *  directory exec "git status"
 *
 *  // This fails since `'A` and `message'` will be considered as two arguments
 *  directory exec "git commit -m 'A message'"
 * ```
 */
infix fun File.exec(command: String): String {
    val arguments = command.split(' ').toTypedArray()
    return execute(*arguments)
}

/**
 * Executes command. Arguments may contain strings. More appropriate than [File.exec]
 * when using dynamic arguments.
 * ```kotlin
 *  // Example
 *  directory.execute("git", "commit", "-m", "A message")
 * ```
 */
fun File.execute(vararg arguments: String): String {
    val process = ProcessBuilder(*arguments)
        .directory(this)
        .start()
        .also { it.waitFor(10, TimeUnit.SECONDS) }

    if (process.exitValue() != 0) {
        throw Exception(process.errorStream.bufferedReader().readText())
    }
    return process.inputStream.bufferedReader().readText()
}


回答6:

As this is the first google result when searching for how to run commands in kotlin, I found my self struggeling to extend it to get basic standard input working, as the Pipe "|" is not supported for the ProcessBuilder. I've found out how to do it, and thought I'd share. I've converted jkschneider's answer to be run as a simple function, now with optional stdin and output capturing. It's a little less pretty, but more flexible. You can run it like this:

exec("someCommandWithUserInput", stdIn = "yes")
/** Run a system-level command.
 * Note: This is a system independent java exec (e.g. | doesn't work). For shell: prefix with "bash -c"
 * Inputting the string in stdIn (if any), and returning stdout and stderr as a string. */
fun exec(cmd: String, stdIn: String = "", captureOutput:Boolean = false, workingDir: File = File(".")): String? {
    try {
        val process = ProcessBuilder(*cmd.split("\\s".toRegex()).toTypedArray())
            .directory(workingDir)
            .redirectOutput(if (captureOutput) ProcessBuilder.Redirect.PIPE else ProcessBuilder.Redirect.INHERIT)
            .redirectError(if (captureOutput) ProcessBuilder.Redirect.PIPE else ProcessBuilder.Redirect.INHERIT)
            .start().apply {
                if (stdIn != "") {
                    outputStream.bufferedWriter().apply {
                        write(stdIn)
                        flush()
                        close()
                    }
                }
                waitFor(60, TimeUnit.SECONDS)
            }
        if (captureOutput) {
            return process.inputStream.bufferedReader().readText()
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
    return null
}


标签: exec kotlin