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 = ${args.size}") // наприклад: Кількість args = 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("Немає аргументів. Запускаємо зі значеннями за замовчуванням.") // Немає аргументів. Запускаємо зі значеннями за замовчуванням.
return
}
println("Перший аргумент = ${args[0]}") // наприклад: Перший аргумент = 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, якщо рядок не є числом.
По‑третє, оператор Елвіса ?: задає значення за замовчуванням. Це робить програму «живучою»: навіть якщо аргументи некоректні, вона все одно стартує.
Під’єднуємо параметри до застосунку
Уявімо, що десь у проєкті у вас уже є функція гри (ми не переписуємо гру повністю — ми вчимося запускати її з параметрами):
package app
fun runGuessNumberGame(max: Int, attempts: Int) {
println("Гру запущено: 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"). І тут є нюанс, який ідеально лягає на тему роботи з nullable‑значеннями: якщо змінної немає, ви отримаєте null.
Мінімальний приклад: читаємо й діагностуємо
package app
fun main() {
val raw = System.getenv("GUESS_MAX")
println("GUESS_MAX = $raw") // наприклад: GUESS_MAX = 200 (або: GUESS_MAX = null)
}
Якщо побачили null, це не помилка. Це просто означає: «змінну не задано».
Перетворюємо число зі змінної середовища безпечно
Зробімо те саме, що й з аргументами: нормалізація → перетворення → значення за замовчуванням.
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("Визначили max = $max") // наприклад: Визначили 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("Робочий каталог: $dir") // наприклад: Робочий каталог: /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 лише збирає вхідні параметри, а основна логіка живе в окремій функції та отримує вже готові значення.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ