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 = ${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 лише збирає вхідні параметри, а основна логіка живе в окремій функції та отримує вже готові значення.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ