110

I want to invoke an external command from Kotlin code.

  • In C/Perl, I would use the system() function.
  • In Python, I would use the subprocess module.
  • In Go, I would use os/exec, and etc.

But how do I do this in Kotlin?

Pang
  • 9,564
  • 146
  • 81
  • 122
Big Tummy
  • 1,141
  • 2
  • 8
  • 3
  • This is the same as in Java. You will find many examples for Java that you can use as-is for Kotlin with small syntax changes (maybe). There are even libraries that do this to handle cases like capturing stdout and more. What did you try already, do you have code you tried to share? – Jayson Minard Feb 16 '16 at 12:38
  • http://stackoverflow.com/questions/9657038/how-can-java-execute-batch-file-in-separate-process-tree – Jayson Minard Feb 16 '16 at 12:41
  • 1
    http://stackoverflow.com/questions/931536/how-do-i-launch-a-completely-independent-process-from-a-java-program – Jayson Minard Feb 16 '16 at 12:41
  • http://stackoverflow.com/questions/31201250/execute-or-run-application-with-java-runtime-or-process-builder – Jayson Minard Feb 16 '16 at 12:42
  • basically I want execute a command from with kotlin and capture the output (stdio/stderr) ... – Big Tummy Feb 17 '16 at 15:41
  • Then go read how to do that in Java from the many examples on Google and on StackOverflow, then change the syntax to Kotlin. It is not a Kotlin topic, but a JVM and Java stdlib topic. And it has been exhaustively covered already. This is a dupe. – Jayson Minard Feb 17 '16 at 15:46
  • I know how to do it in Java, thank you. However, I find it's non-trivial to convert Java solution to a concise Kotlin one. – Big Tummy Feb 17 '16 at 17:47
  • Try it, and if you have issues or specific questions, show your code. You can also write it as Java, then ask IntelliJ IDEA to do the conversion and work from there. It is likely trivial, I think you are setting expectations wrong. And in Stack Overflow it is assumed the question asker makes a good attempt before asking, and shows their work. (I see close votes collecting on the question, probably due to being too generic "write this for me") – Jayson Minard Feb 17 '16 at 19:04

10 Answers10

215

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
    }
}
Jonathan Schneider
  • 26,852
  • 13
  • 75
  • 99
  • 44
    This answer gets the bonus points since it tries to do what the OP asked, namely to _do this in Kotlin_ (or, in a way natural to Kotlin) - instead of just saying "this is the same as in Java", this answer tries to find a way within "idiomatic Kotlin" to achieve this. – Per Lundberg Jun 23 '18 at 17:18
  • I'm using the code from 2. Capturing output as a String. My IDE suggests two imports for IOException. Should I use IOException (java.io) or IOException (ktlinx.io.errors) ? – Osvald Laurits Nov 19 '19 at 10:42
  • 3
    @Michael_H `java.io.IOException` – Jonathan Schneider Nov 19 '19 at 19:32
  • 4
    note - this actually hides the errorStream. You need `val errorStream = proc.errorStream.bufferedReader().readText()` to do something with it – ZakTaccardi Nov 13 '20 at 02:22
  • While this is what OP asked for, it should be noted that in general calls to a shell or just splitting on whitespace should be avoided. This is vulnerable to [shell injection attacks](https://en.wikipedia.org/wiki/Code_injection#Shell_injection), similar to SQL injections. The more secure alternative is to pass the command to be executed and the list of parameters directly to ProcessBuilder. – clonejo Jul 08 '21 at 16:55
  • This breaks with "|" commands – Ilya Gazman Jan 03 '22 at 22:41
  • 2
    @Ilya It breaks with | because it's not running a bash process. To do so, you need to wrap your command into "bash -c 'foo | bar > bla.txt'" – Holger Brandl Jan 26 '22 at 08:46
  • ProcessBuilder's docs indicate that PIPE is the initial value for output and error, so those lines can be ommitted. https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/ProcessBuilder.html#redirectOutput(java.lang.ProcessBuilder.Redirect) – ConnorWGarvey Mar 16 '22 at 19:19
  • 1
    I'm sorry it's too late to edit the comment I just left. The "standard" and "error" streams are different. To avoid losing the content of "error" stream, as would happen in number 2, it could be returned separately, or do this to merge it into the output stream. `.redirectErrorStream(true)`. – ConnorWGarvey Mar 16 '22 at 19:42
37

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.

hotkey
  • 140,743
  • 39
  • 371
  • 326
Lionel Port
  • 3,492
  • 23
  • 26
28

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
Ebrahim Byagowi
  • 10,338
  • 4
  • 70
  • 81
24

For Kotlin Native:

platform.posix.system("git status")

For JVM

Runtime.getRuntime().exec("git status")
Tarek360
  • 1,251
  • 1
  • 13
  • 19
  • 1
    Where can we find the definition of this function in the repository? How can we get the output or the error returned by the command? Like we do with the inputStream and errorStream for jvm? – GarouDan Jun 13 '20 at 22:16
  • I [posted an example for Kotlin/Native](https://stackoverflow.com/a/64311102/420412) which returns output and throws exception with error – gildor Oct 12 '20 at 02:54
9

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()
}
Love
  • 1,709
  • 2
  • 22
  • 30
  • 2
    Side note: Sadly `command.split(' ')` produces a wrong list as soon as you use commands such as `git log --pretty="%D %s"` which can contain a space character - as you point out in your leading comment. – JJD Sep 19 '19 at 09:14
8

There is a kotlin library for easy run external command

Add this gradle dependency:

implementation("com.sealwu:kscript-tools:1.0.2")

And can run comamnd like this:

"cd ~ && ls".runCommand()

Output:

Applications   Downloads      MavenDep       Pictures       iOSProjects
Desktop        IdeaProjects   Movies         Public         scripts
Documents      Library        Music          StudioProjects

Also you can get the output of command line by using evalBash

val date = "date".evalBash().getOrThrow()  //execute shell command `date` and get the command's output and set the content to date variable
println(date) //This will print Fri Aug 19 21:59:56 CEST 2022 on console
val year = date.substringAfterLast(" ") // will get 2022 and assign to year
println(year)

Output:

Fri Aug 19 21:59:56 CEST 2022
2022

More Info: https://github.com/wuseal/Kscript-Tools

吴海豹
  • 1,322
  • 1
  • 13
  • 9
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
}

edit:

I've since had to use this so often I've made it into a little library: https://github.com/leonschreuder/kotlin-exec
Supports stdin, capturing stderr/stdout, understands (nested) quoted strings (" and ' and mixed) correctly etc, but this answer is obviously still valid too.

Leon S.
  • 3,337
  • 1
  • 19
  • 16
  • 1
    Best answer as far as I am concerned.. I did not need to change a thing. For future users though, if on Windows, escape use `"` instead of `'`. – Daniel Connelly Aug 21 '20 at 18:51
  • 1
    The reason why pipe does not work, is that pipes are a shell feature, and ProcessBuilder does not invoke a shell. – clonejo Jul 08 '21 at 17:00
3

For Kotlin/Native I use this posix-based implementation:

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
}
gildor
  • 1,789
  • 14
  • 19
2

Kotlin with Okio. Clean and simple. Example: get current folder's content:

import okio.*

Runtime.getRuntime().exec("ls -l")
         .inputStream
         .source()
         .buffer()
         .use {
             with( Buffer() ){
                 writeAll(it)
                 println("Current folder's content: ${String(this.readByteArray())}")
             }
        }
Interkot
  • 697
  • 8
  • 10
0

this worked for me, windows 10

ProcessBuilder("cmd /C git status".split(" "))
        .redirectOutput(ProcessBuilder.Redirect.INHERIT)
        .start()
        .waitFor()
Subramanya Rao
  • 151
  • 3
  • 6