Как выполнить внешнюю команду (запустить процесс) из кода Kotlin?
Правильный способ запуска внешних команд в Kotlin
Ваша проблема в том, что вы не перенаправляете поток ошибок (stderr) и не дожидаетесь завершения процесса. В Kotlin лучше всего использовать ProcessBuilder, который даёт гораздо больше контроля. Вот самый надёжный и идиоматичный способ:
import java.io.BufferedReader
import java.io.InputStreamReader
fun executeCommand(command: List<String>): String {
val processBuilder = ProcessBuilder(command)
processBuilder.redirectErrorStream(true) // объединяем stderr и stdout
val process = processBuilder.start()
val reader = BufferedReader(InputStreamReader(process.inputStream))
val output = StringBuilder()
reader.use { br ->
br.lines().forEach { line ->
output.appendLine(line)
}
}
val exitCode = process.waitFor()
if (exitCode != 0) {
throw RuntimeException("Команда завершилась с кодом $exitCode")
}
return output.toString()
}
fun main() {
try {
val result = executeCommand(listOf("ping", "-c", "4", "google.com"))
println("Результат:\n$result")
} catch (e: Exception) {
System.err.println("Ошибка: ${e.message}")
}
}
Почему это работает?
- Список вместо строки:
listOf("ping", "-c", "4", "google.com")— передавайте команду и аргументы как отдельные элементы, это безопаснее и избегает проблем с пробелами в путях. - redirectErrorStream(true): объединяет stderr и stdout, чтобы вы видели все сообщения, включая ошибки.
- process.waitFor(): дожидается завершения процесса, иначе вы рискуете прочитать неполный вывод или зависнуть.
- reader.use: автоматически закрывает ресурс после использования.
- Проверка exitCode: позволяет обработать неудачное выполнение команды.
Расширенный вариант с асинхронным выполнением
Если команда работает долго, лучше запускать её в корутине:
import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.InputStreamReader
suspend fun executeCommandAsync(command: List<String>): String = withContext(Dispatchers.IO) {
val process = ProcessBuilder(command)
.redirectErrorStream(true)
.start()
val reader = BufferedReader(InputStreamReader(process.inputStream))
val output = StringBuilder()
reader.use { br ->
br.lines().forEach { line ->
output.appendLine(line)
}
}
val exitCode = process.waitFor()
if (exitCode != 0) {
throw RuntimeException("Код ошибки: $exitCode")
}
output.toString()
}
fun main() = runBlocking {
try {
val result = executeCommandAsync(listOf("ls", "-la"))
println(result)
} catch (e: Exception) {
println("Ошибка: ${e.message}")
}
}
Работа с stderr отдельно
Если вам нужно разделить stdout и stderr, используйте два потока:
fun executeWithSeparateStreams(command: List<String>): Pair<String, String> {
val process = ProcessBuilder(command).start()
val stdout = StringBuilder()
val stderr = StringBuilder()
// Читаем stdout в отдельном потоке
Thread {
BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
reader.lines().forEach { stdout.appendLine(it) }
}
}.start()
// Читаем stderr
BufferedReader(InputStreamReader(process.errorStream)).use { reader ->
reader.lines().forEach { stderr.appendLine(it) }
}
process.waitFor()
return Pair(stdout.toString(), stderr.toString())
}
Важные замечания
- Для Windows используйте
cmd.exe /cперед командой, если это встроенная команда вродеdir. - Для сложных конвейеров (pipe) используйте
ProcessBuilderсredirectOutputиredirectInputмежду процессами. - Не забывайте про
destroy()для принудительной остановки процесса, если он завис.
Альтернативный подход: использование Kotlin-библиотеки "kotlinx-process"
Если вы не хотите каждый раз писать обвязку вокруг ProcessBuilder, можно использовать библиотеку kotlinx-process (часть проекта kotlinx). Она предлагает более лаконичный синтаксис:
// build.gradle.kts
implementation("org.jetbrains.kotlinx:kotlinx-process:0.3.0")
// Код
import kotlinx.process.*
fun main() {
val result = "ping -c 4 google.com".execute()
println(result.stdout)
println("Exit code: ${result.exitCode}")
}
Библиотека автоматически обрабатывает потоки, коды возврата и таймауты. Пример с обработкой ошибок:
val command = "ls -la /nonexistent"
val result = command.execute()
if (result.isSuccess) {
println(result.stdout)
} else {
println("Ошибка: ${result.stderr}")
}
Также поддерживаются асинхронные вызовы и пайпы между командами. Это удобно, если вы часто работаете с внешними процессами и хотите избежать boilerplate-кода. Однако для простых скриптов или когда не хочется добавлять зависимость, первый подход с ProcessBuilder остаётся лучшим выбором.
Ответов пока нет.
Войдите чтобы оставить ответ