Создание CLI утилит на C++: от идеи до готового инструмента

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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                

Блоги

Рекомендаций по книгам