I want to invoke an external command from Kotlin code. In C/Perl, I would use system()
function. In Python, I would use the subprocess module. In Go, I would us
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()
}
For Kotlin Native:
platform.posix.system("git status")
For JVM
Runtime.getRuntime().exec("git status")
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
}
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.
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
}
Example of running a git diff by shelling out:
"git diff".runCommand(gitRepoDir)
Here are two implementations of the runCommand
extension function:
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)
}
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
}
}