Почему C++ — идеальный язык для CLI-утилит?
Когда речь заходит о создании CLI утилит на C++, многие разработчики пожимают плечами: "Зачем? Есть же Python, Go, Rust". Но давайте посмотрим правде в глаза. Если вам нужна молниеносная скорость, минимальное потребление памяти и полный контроль над железом — C++ вне конкуренции. Системные администраторы, DevOps-инженеры и разработчики встраиваемых систем до сих пор пишут свои консольные инструменты именно на этом языке. И знаете что? Это правильно.
Сегодня мы разберем создание CLI утилиты на C++ с нуля — от парсинга аргументов до сборки бинарника. Вы узнаете, как написать настоящий, полезный инструмент, который не стыдно выложить на GitHub. Поехали.
Инструментарий: что нам понадобится
Для комфортной работы нам потребуется:
- Компилятор C++17/20 — GCC (MinGW на Windows) или Clang. Лично я рекомендую GCC 11+.
- Текстовый редактор или IDE — VS Code с плагином C++, CLion, или даже Vim (для хардкорщиков).
- CMake — для удобной сборки. Хотя можно и Makefile, но CMake — это современный стандарт.
- Git — куда же без контроля версий.
Также нам понадобится библиотека для парсинга аргументов командной строки. Мы не будем изобретать велосипед и возьмем легендарную getopt (она есть в POSIX-системах) или напишем свой минималистичный парсер. Второй вариант даже интереснее — вы поймете, как это работает "под капотом".
Структура проекта
Прежде чем писать код, давайте определимся, что именно мы создаем. Пусть это будет утилита fileutils — консольный инструмент для работы с файлами. Она будет уметь:
- Подсчитывать количество строк, слов и символов в файле (аналог
wc). - Искать строку по паттерну (аналог
grepв минимальном виде). - Выводить справку (
--help).
Структура папок:
fileutils/
├── src/
│ ├── main.cpp
│ ├── parser.cpp
│ └── parser.h
├── CMakeLists.txt
└── README.md
Шаг 1: Собственный парсер аргументов
Напишем простой, но рабочий парсер. Он будет обрабатывать длинные флаги (--lines, --words) и позиционные аргументы (имя файла).
Файл parser.h
#ifndef PARSER_H
#define PARSER_H
#include <string>
#include <vector>
#include <map>
struct Args {
bool show_lines = false;
bool show_words = false;
bool show_chars = false;
std::string pattern;
std::vector<std::string> files;
};
Args parse_args(int argc, char* argv[]);
#endif
Файл parser.cpp
#include "parser.h"
#include <iostream>
Args parse_args(int argc, char* argv[]) {
Args args;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--help" || arg == "-h") {
std::cout << "Usage: fileutils [OPTIONS] FILE...\n"
<< "Options:\n"
<< " --lines, -l Print line count\n"
<< " --words, -w Print word count\n"
<< " --chars, -c Print character count\n"
<< " --search PAT Search for pattern\n"
<< " --help, -h Show this help\n";
exit(0);
} else if (arg == "--lines" || arg == "-l") {
args.show_lines = true;
} else if (arg == "--words" || arg == "-w") {
args.show_words = true;
} else if (arg == "--chars" || arg == "-c") {
args.show_chars = true;
} else if (arg == "--search" && i + 1 < argc) {
args.pattern = argv[++i];
} else if (arg[0] != '-') {
args.files.push_back(arg);
} else {
std::cerr << "Unknown option: " << arg << "\n";
exit(1);
}
}
// Если не указано ни одного флага, показываем всё
if (!args.show_lines && !args.show_words && !args.show_chars && args.pattern.empty()) {
args.show_lines = args.show_words = args.show_chars = true;
}
return args;
}
Шаг 2: Основная логика утилиты
Теперь самое интересное — реализация функционала. Мы будем использовать современные возможности C++17: std::filesystem для работы с путями и std::ifstream для чтения файлов.
Файл main.cpp
#include "parser.h"
#include <iostream>
#include <fstream>
#include <filesystem>
#include <string>
namespace fs = std::filesystem;
void process_file(const std::string& filename, const Args& args) {
std::ifstream file(filename);
if (!file.is_open()) {
std::cerr << "Error: Cannot open file " << filename << "\n";
return;
}
size_t lines = 0, words = 0, chars = 0;
std::string line;
while (std::getline(file, line)) {
lines++;
chars += line.length() + 1; // +1 за символ новой строки
// Подсчёт слов: разделяем по пробелам
bool in_word = false;
for (char ch : line) {
if (std::isspace(ch)) {
in_word = false;
} else if (!in_word) {
in_word = true;
words++;
}
}
// Поиск паттерна
if (!args.pattern.empty() && line.find(args.pattern) != std::string::npos) {
std::cout << filename << ": " << line << "\n";
}
}
// Вывод статистики
if (args.show_lines) std::cout << "Lines: " << lines << "\n";
if (args.show_words) std::cout << "Words: " << words << "\n";
if (args.show_chars) std::cout << "Chars: " << chars << "\n";
}
int main(int argc, char* argv[]) {
Args args = parse_args(argc, argv);
if (args.files.empty()) {
std::cerr << "Error: No input files specified.\n";
return 1;
}
for (const auto& f : args.files) {
if (!fs::exists(f)) {
std