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
}