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 только собирает входные параметры, а основная логика живёт в отдельной функции и получает уже готовые значения.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ