JavaRush /Курсы /Kotlin SELF /Запуск программы: args, переменные окружения и рабочая ди...

Запуск программы: args, переменные окружения и рабочая директория

Kotlin SELF
17 уровень , 3 лекция
Открыта

1. Что значит «запустить программу»: код + окружение

Когда вы нажимаете кнопку Run в IDE или запускаете программу из терминала, кажется, что происходит простое: «выполни main». Но на практике запуск почти всегда включает внешние параметры. Программа может стартовать с аргументами командной строки, с переменными окружения, в определённой рабочей директории — и всё это влияет на поведение даже самого простого приложения.

Представьте, что вы написали игру «Угадай число» (мы её уже делали раньше). У неё есть настройки: диапазон чисел и число попыток. Можно «зашить» их в код, и это будет работать. Но как только вам захотелось быстро поменять диапазон без переписывания программы, появляется вопрос: «а как передать настройки при запуске?». Сегодня мы как раз учимся это делать по‑взрослому, но без боли.

Чтобы не расползаться по темам, будем держать в голове учебное приложение GuessNumberApp: игра «Угадай число», которую теперь можно настраивать при запуске.

2. Аргументы командной строки: main(args: Array<String>)

Когда программа запускается, операционная система (и/или среда запуска) может передать ей список строк — аргументы. В Kotlin для этого существует форма main, которая принимает массив строк: fun main(args: Array<String>). Это официально предусмотренная «вторая форма» точки входа.

Важно сразу поймать интонацию: аргументы — это всегда строки. Даже если вы передали 10, в программе это "10", и если вам нужно число — придётся парсить, а значит, возможны ошибки.

Самый первый контакт: просто выведем аргументы

Сначала сделаем то, что делает любой разработчик в непонятной ситуации: выведем, что вообще пришло.

package app

fun main(args: Array<String>) {
    println("Args count = ${args.size}")            // например: Args count = 2
    println("Args = ${args.contentToString()}")     // например: Args = [100, 7]
}

Здесь args.size — количество аргументов, а contentToString() помогает красиво вывести массив.
Если вы запускаете без аргументов, size будет 0, и это нормально: программа не должна обижаться на мир за то, что ей ничего не передали.

Почему нельзя сразу делать args[0]

Потому что args может быть пустым. Тогда обращение по индексу приведёт к ошибке выхода за границы массива. И это тот случай, когда программа упадёт ещё до того, как успеет сказать: «Эй, пользователь, ты забыл аргументы».

Поэтому правило простое: сначала проверяем args.size, потом читаем args[i].

package app

fun main(args: Array<String>) {
    if (args.size == 0) {
        println("No args. Running with defaults.")  // No args. Running with defaults.
        return
    }

    println("First arg = ${args[0]}")               // например: First arg = 100
}

Обратите внимание: мы не используем ничего «волшебного». Просто if и индексы — вы это уже умеете.

Разбор аргументов: «сырые строки → подготовка → парсинг»

Аргументы командной строки — это классический пример «грязного ввода». Их легко забыть, легко перепутать порядок, легко передать «сто» вместо 100. Поэтому мы применяем знакомый паттерн: прочитали строку → подготовили → попробовали распарсить → обработали null.

Простая схема: max и attempts позиционно

Пусть договор такой:
args[0] — верхняя граница (max)
args[1] — число попыток (attempts)

Сделаем парсинг аккуратно:

package app

fun main(args: Array<String>) {
    val maxRaw = if (args.size > 0) args[0] else null
    val attemptsRaw = if (args.size > 1) args[1] else null

    val max = maxRaw?.trim()?.toIntOrNull() ?: 100
    val attempts = attemptsRaw?.trim()?.toIntOrNull() ?: 7

    println("max=$max attempts=$attempts")          // например: max=100 attempts=7
}

Здесь важно сразу три вещи.

Во‑первых, trim() спасает от случайных пробелов (да, их можно «случайно» передать, особенно в некоторых оболочках или при копипасте).
Во‑вторых, toIntOrNull() не бросает исключение, а возвращает null, если строка не число.
В‑третьих, Elvis-оператор ?: даёт значения по умолчанию. Это делает программу «живучей»: даже если аргументы кривые, она всё равно стартует.

Подключаем параметры к приложению

Представим, что где-то в проекте у вас уже есть функция игры (мы не переписываем игру целиком — мы учимся запускать её с параметрами):

package app

fun runGuessNumberGame(max: Int, attempts: Int) {
    println("Game started: max=$max, attempts=$attempts")
    // Здесь была бы логика игры, которую вы уже писали раньше
}

Теперь main становится «входной дверью»: он читает окружение запуска и передаёт нормальные параметры в логику.

package app

fun main(args: Array<String>) {
    val maxRaw = if (args.size > 0) args[0] else null
    val attemptsRaw = if (args.size > 1) args[1] else null

    val max = maxRaw?.trim()?.toIntOrNull() ?: 100
    val attempts = attemptsRaw?.trim()?.toIntOrNull() ?: 7

    runGuessNumberGame(max, attempts)
}

Это важное архитектурное ощущение: main — место, где вы «склеиваете» внешний мир и вашу программу. А сама логика (функции) должна получать уже нормальные значения.

3. Переменные окружения: System.getenv

Аргументы — это удобно, когда параметров мало и их часто меняют «на лету». Но есть другой канал: переменные окружения (environment variables). Это пары KEY=VALUE, которые система передаёт процессу при запуске. На бытовом уровне это выглядит как «настройки рядом с запуском», которые не обязательно каждый раз писать в командной строке.

В Kotlin/JVM переменные окружения читаются через System.getenv("NAME"). И здесь есть нюанс, который идеально ложится на тему null-safety: если переменной нет, вы получите null.

Минимальный пример: читаем и диагностируем

package app

fun main() {
    val raw = System.getenv("GUESS_MAX")
    println("GUESS_MAX = $raw") // например: GUESS_MAX = 200 (или: GUESS_MAX = null)
}

Если увидели null — это не ошибка. Это просто значит: «переменная не задана».

Парсим число из env-переменной безопасно

Сделаем то же, что и с аргументами: нормализация → парсинг → дефолт.

package app

fun main() {
    val max = System.getenv("GUESS_MAX")
        ?.trim()
        ?.toIntOrNull()
        ?: 100

    println("max=$max") // например: max=100
}

Здесь очень «котлиновый» стиль: цепочка ?. читается как «если значение существует — обработай, иначе не трогай».

Приоритет источников: args важнее env, env важнее дефолта

В реальных приложениях часто делают такую логику:

1) если пользователь явно передал аргумент — уважаем его;
2) иначе смотрим переменную окружения;
3) иначе берём дефолт.

Давайте реализуем это, не вводя никаких новых страшных концепций. Сделаем функцию-помощник, которая выбирает max.

package app

fun resolveMax(args: Array<String>): Int {
    val fromArgs = if (args.size > 0) args[0].trim().toIntOrNull() else null
    if (fromArgs != null) return fromArgs

    val fromEnv = System.getenv("GUESS_MAX")?.trim()?.toIntOrNull()
    if (fromEnv != null) return fromEnv

    return 100
}

И используем в main:

package app

fun main(args: Array<String>) {
    val max = resolveMax(args)
    println("Resolved max = $max") // например: Resolved max = 150
}

Почему это удобно? Потому что вы «упаковали» политику выбора в одну функцию. main не превращается в простыню из if.

4. Рабочая директория: System.getProperty("user.dir")

Рабочая директория (working directory) — это папка, относительно которой интерпретируются относительные пути. Даже если вы пока не работаете с файлами, это понятие стоит понять сейчас, потому что оно регулярно всплывает в стиле «у меня на компьютере работает, а у друга — нет».

На JVM рабочую директорию часто удобно прочитать так: System.getProperty("user.dir"). Это не «магия Kotlin», это просто доступ к свойствам платформы, а мы используем System.* как готовый инструмент, не углубляясь, как JVM это реализует.

Печатаем рабочую директорию для диагностики

package app

fun main() {
    val dir = System.getProperty("user.dir")
    println("Working directory: $dir") // например: Working directory: /Users/alice/projects/guess
}

Если вы запускаете из IDE, рабочая директория часто (но не всегда) будет корнем проекта. Если запускаете из терминала, она обычно равна «текущей папке терминала».

Мысленный эксперимент: относительные пути «привязаны» к "user.dir"

Пока мы не трогаем файлы (это будет в других днях), но можем сделать демонстрацию строками: представим, что вы хотите обратиться к "data/config.txt". Относительный путь можно «мысленно» превратить в абсолютный, склеив его с рабочей директорией.

package app

fun main() {
    val dir = System.getProperty("user.dir")
    val relative = "data/config.txt"
    val full = "$dir/$relative"

    println(full) // например: /Users/alice/projects/guess/data/config.txt
}

Да, это не универсальный способ работы с путями (для этого есть отдельные API), но как иллюстрация идеи — отлично: рабочая директория задаёт «точку отсчёта».

5. Схема источников настроек и мини-рефакторинг

Сейчас у нас есть три источника «внешних данных при запуске»: аргументы, переменные окружения и (в крайнем случае) дефолты в коде. Иногда добавляют ещё интерактивный ввод readln(), но это уже «внутри выполнения», а не «параметры запуска».

Удобно держать это как небольшой пайплайн:

flowchart TD
    A[Запуск программы] --> B{Есть args?}
    B -->|да| C[Парсим args: String -> Int?]
    B -->|нет| D{Есть env?}
    D -->|да| E[Парсим env: String -> Int?]
    D -->|нет| F[Берём дефолт]
    C --> G["Запускаем логику: runGuessNumberGame(...)"]
    E --> G
    F --> G

И это прям «взрослая» привычка: не смешивать логику игры/приложения с логикой чтения параметров запуска.

Когда программа запускается не из IDE, а где-нибудь ещё, полезно уметь быстро понять: «что она решила?». Поэтому часто добавляют маленькую диагностику: вывод параметров, из какого источника они взяты, какая рабочая директория. Мы сделаем это аккуратно, короткими функциями, без усложнения.

Функция для attempts с тем же приоритетом

package app

fun resolveAttempts(args: Array<String>): Int {
    val fromArgs = if (args.size > 1) args[1].trim().toIntOrNull() else null
    if (fromArgs != null) return fromArgs

    val fromEnv = System.getenv("GUESS_ATTEMPTS")?.trim()?.toIntOrNull()
    if (fromEnv != null) return fromEnv

    return 7
}

Собираем компактный main

package app

fun main(args: Array<String>) {
    val max = resolveMax(args)
    val attempts = resolveAttempts(args)

    val dir = System.getProperty("user.dir")
    println("dir=$dir")                          // например: dir=/Users/alice/projects/guess
    println("max=$max attempts=$attempts")       // например: max=100 attempts=7

    runGuessNumberGame(max, attempts)
}

Обратите внимание: здесь main читается как сценарий. Сначала подготовили параметры, потом чуть‑чуть показали диагностику, потом вызвали основную логику.

Памятка: args vs env vs рабочая директория

Чтобы не путаться, удобно различать эти каналы не по «как в Linux принято», а по смыслу.

Канал Что это Когда удобно Главный риск
args (Array<String>) Явные параметры запуска, список строк Быстро менять значение при каждом запуске Легко забыть аргумент или перепутать порядок
env (System.getenv) Настройки, которые «живут рядом с запуском» Держать дефолты для среды (например, «на этом ПК всегда max=200») Переменной может не быть → получите null
рабочая директория ("user.dir") Контекст «откуда запущено» Диагностика и понимание, почему относительные пути ведут себя странно В IDE и в терминале может быть разной

6. Типичные ошибки

Ошибка №1: обращаться к args[0] без проверки размера массива.
Это одна из самых частых причин падений «на ровном месте»: вы запускаете без аргументов, а код сразу лезет в первый элемент. Правильная привычка: сначала if (args.size > 0), потом доступ по индексу.

Ошибка №2: парсить числа через toInt() без страховки.
Если вы используете toInt() для аргументов или переменных окружения, то при любой нечисловой строке программа получит исключение и завершится. Для входных данных почти всегда лучше toIntOrNull() и дальше нормальная обработка через if (x != null) или ?:.

Ошибка №3: считать, что env‑переменная «точно есть».
System.getenv("NAME") возвращает nullable-значение по очень честной причине: переменной может не существовать. Даже если вы «у себя» её настроили, на другом компьютере или в другом запуске она может отсутствовать. Поэтому ?: и понятный дефолт — это не роскошь, а способ не получать загадочные падения.

Ошибка №4: не учитывать рабочую директорию и удивляться «почему не находится файл/папка».
Даже если вы пока не читаете файлы, вы уже можете попасть в ситуацию, когда относительный путь используется косвенно (например, в параметрах запуска или в выводе). Привычка выводить System.getProperty("user.dir") при отладке часто экономит часы — и немного нервных клеток.

Ошибка №5: смешивать «чтение окружения» и «бизнес-логику» в одной функции.
Если main одновременно парсит args, читает env, печатает диагностику, играет в «угадай число» и ещё шутит шутки — отлаживать это будет тяжело. Гораздо спокойнее, когда main только собирает входные параметры, а основная логика живёт в отдельной функции и получает уже готовые значения.

1
Задача
Kotlin SELF, 17 уровень, 3 лекция
Недоступна
Отчёт запуска
Отчёт запуска
1
Задача
Kotlin SELF, 17 уровень, 3 лекция
Недоступна
Режим витрины
Режим витрины
1
Задача
Kotlin SELF, 17 уровень, 3 лекция
Недоступна
Прямоугольник с дефолтами
Прямоугольник с дефолтами
1
Задача
Kotlin SELF, 17 уровень, 3 лекция
Недоступна
Конфиг запуска
Конфиг запуска
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ