JavaRush /Курси /C++ SELF /argc/argv — базовий розбір аргументів

argc/argv — базовий розбір аргументів

C++ SELF
Рівень 67 , Лекція 0
Відкрита

1. Аргументи командного рядка: навіщо вони потрібні й звідки беруться

Уявіть, що ваша програма — це кавомашина. Можна підійти, натиснути кнопку, а далі «вести діалог» в інтерактивному режимі. А можна заздалегідь сказати: «зроби капучино, 300 мл, без цукру» — і піти чекати результату. Аргументи командного рядка — це саме такі попередньо передані налаштування, які програма отримує у момент запуску.

На практиці це виглядає так: ви запускаєте програму й додаєте до команди параметри.

Наприклад, умовно:

app --name Alice --repeat 3

І програма має зрозуміти: імʼя — Alice, повторити — 3 рази.

Чому це зручно? Тому що саме так працюють майже всі «справжні» консольні утиліти: компілятори, архіватори, форматери коду, лінтери. І так, ваш майбутній проєкт теж може так уміти — без жодної магії, просто через argc/argv.

Сигнатура main: argc і argv

Коли ви писали перші програми, main() виглядав так:

int main() {
    // ...
}

Тепер увімкнемо «режим дорослого консольного життя» й використаємо інший варіант main:

int main(int argc, char* argv[]) {
    // ...
}

Тут важливі дві речі: argc і argv. Звучить так, ніби це імена двох гномів із підвалу лінкера, але насправді все просто.

Суть така:

Елемент Що це Як це розуміти
argc
кількість аргументів «скільки рядків нам передали»
argv
масив C-рядків «де лежать ці рядки»

Причому argv — це масив, тому до нього звертаються за індексом: argv[0], argv[1], …, argv[argc - 1].

argv — це масив, і він має межі

Дуже важливий момент для безпеки: argv — це звичайний масив вказівників. А масиви, як ми вже знаємо, не пробачають самовпевненості.

Якщо argc == 3, то допустимі індекси лише такі:

  • argv[0]
  • argv[1]
  • argv[2]

Якщо ви полізете в argv[3], це вже вихід за межі. А вихід за межі — це як крок у темряву: інколи «пощастить», але частіше ви просто впадете.

Ще один практичний факт: argv[0] зазвичай містить імʼя програми (або шлях до неї). Тому розбір користувацьких аргументів найчастіше починається з i = 1.

Мініприклад: «Подивімося, що взагалі прийшло».

#include <iostream>

int main(int argc, char* argv[]) {
    for (int i = 0; i < argc; ++i) {
        std::cout << i << ": " << argv[i] << '\n';
    }
}

Якщо запустити програму як app hello world, ви побачите приблизно таке:

0: app          // імʼя програми (часто)
1: hello
2: world

2. Рядки аргументів: чому це не std::string

Чому argv[i] — це не std::string і чим допоможе std::string_view

Коли ви читаєте argv[i], то отримуєте не std::string, а C-рядок: char*, який закінчується нульовим символом '\0'. Тому не можна просто порівнювати argv[i] як «рядки» ось так:

// ПОГАНО (порівняння адрес, а не тексту)
if (argv[i] == "--help") { /* ... */ }

Це порівняння вказівників, тобто адрес у памʼяті, а не вмісту тексту. Новачки часто дивуються: «Чому не працює? Я ж бачу --help!» А причина в тому, що ви порівняли не напис на папірці, а місце, де лежить сам папірець.

Найзручніше — перетворити argv[i] на std::string_view. Це дешево: без копіювання, зате дає змогу порівнювати рядки за вмістом.

#include <string_view>

std::string_view arg = argv[i];
if (arg == "--help") { /* ... */ }

Тут string_view просто «дивиться» на вже наявний C-рядок. І це нормально, тому що аргументи командного рядка зазвичай живуть увесь час роботи програми.

3. Безпечний розбір: головне правило про межі

Головне правило: не чіпайте argv[i + 1], доки не перевірили межі

Зараз буде правило, яке рятує нерви, час і, можливо, клавіатуру від удару чолом:

Якщо опція потребує значення, наприклад --name Alice, то перш ніж читати argv[i + 1], ви спершу зобовʼязані перевірити:

i + 1 < argc

Інакше станеться класика жанру: користувач напише --name наприкінці, а програма полізе «за край масиву».

Закріпімо це невеликою блок-схемою алгоритму: «Йдемо зліва направо за аргументами».

flowchart TD
    A[Почати: i = 1] --> B{ i < argc ? }
    B -- ні --> Z[Готово]
    B -- так --> C["зчитати arg = argv[i]"]
    C --> D{arg == '--name'?}
    D -- ні --> E[обробити інше / пропустити]
    D -- так --> F{ i+1 < argc ? }
    F -- ні --> G[помилка: немає значення]
    F -- так --> H["name = argv[i+1]; i++"]
    E --> I[i++]
    H --> I
    I --> B

Так, блок-схема виглядає як мінілабіринт. Зате вона чесно показує головну ідею: межі треба перевіряти до читання.

4. Практика: прапорці, опції та позиційні аргументи

Перший корисний кейс: прапорець --help / -h

Тепер зробімо невелику, але корисну річ: програму, яка шукає --help або -h серед аргументів. Ми поки не проєктуємо красивий CLI-контракт, а просто вивчаємо механіку: «знайшов прапорець — виконав дію».

#include <iostream>
#include <string_view>

int main(int argc, char* argv[]) {
    for (int i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];

        if (arg == "--help" || arg == "-h") {
            std::cout << "Help requested\n";
            return 0;
        }
    }

    std::cout << "No help flag\n";
}

Зверніть увагу: прапорець — це опція без значення. Ми не читаємо argv[i + 1], не пересуваємо індекс на два кроки, а просто перевіряємо поточний аргумент.

Опція зі значенням: формат --key value

Далі — найпоширеніший формат опцій, який ви побачите в реальних утилітах: ключ окремо, значення окремо.

Приклад:

app --name Alice

Тут --name — ключ, Alice — значення, і воно лежить у наступному аргументі.

Зробімо програму, яка приймає --name і друкує привітання. За замовчуванням імʼя буде "anonymous".

#include <iostream>
#include <string_view>

int main(int argc, char* argv[]) {
    std::string_view name = "anonymous";

    for (int i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];

        if (arg == "--name") {
            if (i + 1 >= argc) {
                std::cout << "Error: --name requires a value\n";
                return 1;
            }
            name = argv[i + 1];
            ++i; // зʼїли значення
        }
    }

    std::cout << "Hello, " << name << '\n';
}

Тут є два важливі прийоми.

  • Спочатку ми перевірили межу i + 1 >= argc. Це захищає від ситуації, коли значення забули передати.
  • Потім ми зробили ++i. Це означає: «Ми вже обробили наступний аргумент як значення, тож не треба обробляти його ще раз як окремий параметр».

Опція зі значенням: формат --key=value

Іноді зручніше передавати значення просто всередині одного аргументу. Тоді не потрібно читати argv[i + 1], а отже менше ризику вийти за межі.

Приклад:

app --name=Alice

Розберімо цей формат за допомогою starts_with і substr. Тут std::string_view особливо доречний.

#include <iostream>
#include <string_view>

int main(int argc, char* argv[]) {
    std::string_view name = "anonymous";
    constexpr std::string_view prefix = "--name=";

    for (int i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];

        if (arg.starts_with(prefix)) {
            name = arg.substr(prefix.size());
        }
    }

    std::cout << "Hello, " << name << '\n';
}

Тут є тонкий момент: користувач може написати --name=, тобто передати порожнє значення. Тоді name стане порожнім рядком. Це не «помилка мови», а питання контракту програми: чи дозволяєте ви порожнє імʼя.

Позиційні аргументи: коли аргумент — це «не опція»

У реальних програмах зазвичай є не лише опції (--flag, --key value), а й позиційні аргументи. Наприклад, імʼя файлу:

app input.txt --verbose

Поширена проста домовленість — саме домовленість, а не закон Всесвіту: усе, що починається з -, вважаємо опцією, а все інше — позиційним аргументом.

Напишімо найпростіший розбір: знайдемо «перший позиційний аргумент» і збережемо його як filename.

#include <iostream>
#include <string_view>

int main(int argc, char* argv[]) {
    std::string_view filename = "(none)";

    for (int i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];

        if (!arg.empty() && arg[0] != '-') {
            filename = arg;
            break;
        }
    }

    std::cout << "filename=" << filename << '\n';
}

Чому break? Тому що ми вирішили: нам потрібен лише один файл. В іншій програмі ви могли б збирати список файлів, але це вже інший контракт і інша логіка розбору.

5. Практичний мініприклад: додаємо argc/argv у навчальний застосунок

Тепер зробімо ще один маленький крок до «реального» застосунку: зберемо структуру налаштувань і навчимося заповнювати її на основі argv. Нехай наш навчальний застосунок називається studyapp: він друкує «план мінісесії» — тему та імʼя користувача.

Ми поки не обговорюємо багатий UX, коди виходу й --help як повноцінний контракт. Тут інша мета: навчитися безпечно витягувати значення з масиву argv і не плутатися в індексах.

Спочатку опишемо «налаштування запуску» як структуру:

#include <string_view>

struct AppArgs {
    std::string_view name = "anonymous";
    std::string_view topic = "C++";
    bool help = false;
};

Тепер напишемо функцію parse_args. Зверніть увагу: вона приймає argc/argv і повертає вже заповнену структуру.

#include <string_view>

AppArgs parse_args(int argc, char* argv[]) {
    AppArgs a{};

    for (int i = 1; i < argc; ++i) {
        std::string_view arg = argv[i];

        if (arg == "--help" || arg == "-h") {
            a.help = true;
        } else if (arg == "--name" && i + 1 < argc) {
            a.name = argv[i + 1];
            ++i;
        } else if (arg == "--topic" && i + 1 < argc) {
            a.topic = argv[i + 1];
            ++i;
        }
    }
    return a;
}

І використаємо її в main:

#include <iostream>

int main(int argc, char* argv[]) {
    AppArgs args = parse_args(argc, argv);

    if (args.help) {
        std::cout << "Usage: studyapp [--name NAME] [--topic TOPIC]\n";
        return 0;
    }

    std::cout << "User:  " << args.name << '\n';
    std::cout << "Topic: " << args.topic << '\n';
}

Приклад запуску:

studyapp --name Alice --topic "argc/argv"

Виведення буде приблизно таким:

User:  Alice
Topic: argc/argv

(Якщо ви передаєте аргументи через реальний командний рядок, лапки потрібні для того, щоб тема з пробілами передалася як один аргумент. Але нюанси цитування в shell ми сьогодні свідомо не розбираємо.)

Зверніть увагу на важливе обмеження нашої функції parse_args: якщо користувач напише --name без значення, ми поки що просто мовчки це «проковтнемо» (бо умова i + 1 < argc не виконається). Для сьогоднішньої теми це нормально: ми тренуємо саме механіку обходу.

6. Типові помилки під час роботи з argc/argv

Помилка № 1: читати argv[i + 1] без перевірки i + 1 < argc.
Це найчастіша причина падінь у простих CLI. Користувач випадково або спеціально залишає --name останнім аргументом, і програма лізе за межі масиву. Лікування тут нудне, але ефективне: спочатку перевіряємо межу, потім читаємо.

Помилка № 2: забувати ++i після обробки формату --key value.
Якщо не збільшити i, наступний аргумент, який був значенням, потрапить у цикл ще раз і почне інтерпретуватися як окрема опція або позиційний аргумент. У результаті ви отримаєте «фантомні параметри» й дивну поведінку, яку складно налагоджувати.

Помилка № 3: починати розбір з i = 0 і намагатися трактувати argv[0] як опцію.
argv[0] — це зазвичай імʼя програми. Буває, що воно починається з - або містить дивні символи, і ваш алгоритм розбору раптом вирішує, ніби це невідома опція. За замовчуванням розбір опцій починають із i = 1.

Помилка № 4: порівнювати char* як вказівники, а не як текст.
Конструкція if (argv[i] == "--help") порівнює адреси, а не рядки. Правильний шлях — порівнювати вміст: через std::string_view(arg) == "--help" або через std::string(argv[i]) == "--help" (але string_view зазвичай простіший і дешевший).

Помилка № 5: змішувати формати опцій без чіткої логіки.
Новачки часто намагаються підтримати і --name value, і --name=value, і -nvalue, і ще «про всяк випадок» позиційні параметри — але без чітких правил. У підсумку розбір перетворюється на кашу з if-ів. Краще рухатися маленькими кроками: спочатку один-два формати, лінійний прохід, передбачувані правила, а вже потім розширення.

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