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. Звучить так, ніби це імена двох гномів із підвалу лінкера, але насправді все просто.
Суть така:
| Елемент | Що це | Як це розуміти |
|---|---|---|
|
кількість аргументів | «скільки рядків нам передали» |
|
масив 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-ів. Краще рухатися маленькими кроками: спочатку один-два формати, лінійний прохід, передбачувані правила, а вже потім розширення.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ