Avatar ·

Как выполнить внешнюю команду (запустить процесс) из кода Kotlin?

Всем привет! Я только начал изучать Kotlin и пишу небольшое консольное приложение. Мне нужно выполнить внешнюю команду, например, запустить ping или скопировать файл с помощью cp. В Java я использовал Runtime.getRuntime().exec(), но в Kotlin я вижу, что есть какие-то встроенные функции вроде ProcessBuilder, но не до конца понимаю, как ими пользоваться. Пробовал написать такой код: ```kotlin fun main() { val command = "ping -c 4 google.com" val process = Runtime.getRuntime().exec(command) val reader = BufferedReader(InputStreamReader(process.inputStream)) var line: String? while (reader.readLine().also { line = it } != null) { println(line) } } ``` Но он выводит только первую строку и зависает. Я что-то делаю не так? Плюс слышал, что в Kotlin есть более идиоматичный способ, чем использовать Java-классы. Подскажите, как правильно вызывать внешние команды? Мне нужно не только запустить, но и прочитать вывод, а также обработать возможные ошибки. Заранее спасибо! <<>>

Правильный способ запуска внешних команд в 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 остаётся лучшим выбором.