$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогСборка CLI-утилит как продукта: команды, конфиги, help-text и обработка ошибок

Сборка CLI-утилит как продукта: команды, конфиги, help-text и обработка ошибок

$ sudo teach IT
·19 июля 2026 г.·9 мин·56
Сборка CLI-утилит как продукта: команды, конфиги, help-text и обработка ошибок

Разберём, как превращать скрипты в удобные CLI-инструменты: структура команд, конфиги, версии, понятные ошибки и документация. Практика на сценариях.

Содержание
Команды как продукт: структура, нейминг и «контракты» интерфейсаДоговоримся о целевой аудитории и контексте использованияКомандная модель: tool <command> [args]Нейминг: последовательность и предсказуемостьИдемпотентность и повторяемостьВерсионирование интерфейса и данныхКонфиги: хранение, приоритеты источников и миграцииГде должен жить конфигПриоритет источников конфигурацииФормат конфигурации и выбор сериализацииМиграции схемы конфигурацииHelp-text и документация: как сделать интерфейс самообъясняющимсяHelp-text — это часть UXПравила написания help-textAuto-completion и форматирование ошибок в helpОбработка ошибок: архитектура, exit-коды и сообщения без паникиПочему «плохие ошибки» — это главная причина фрустрацииКатегории ошибокДоговорённость по exit-кодамСообщения об ошибках: минимально достаточный контентПрактический сценарий: проектируем утилиту seedmgrСтруктура проектаТипы ошибок и exit-кодыЗагрузка конфигурации с приоритетамиМиграции схемы (минимальный каркас)Клиентский интерфейс: команды, help-text, ошибки

CLI-утилиты редко живут в вакууме. Они появляются в цепочках автоматизации, попадают в скрипты CI/CD, становятся «текущей инфраструктурой» для коллег, и в какой-то момент начинают требовать того, чего скрипты обычно не обещают: предсказуемости интерфейса, стабильности аргументов, нормальной документации и ошибок, по которым можно действовать.

В этой статье разберём CLI как продукт: как проектировать команды, как работать с конфигами, как организовать версии, как писать help-text, как строить систему ошибок и какие практические сценарии стоит прогнать, прежде чем выпускать утилиту «в люди». Будет много прикладного: структуры команд, пример конфигурации, договорённости по exit-кодам, типичные ошибки проектирования и рабочие примеры кода.


Команды как продукт: структура, нейминг и «контракты» интерфейса

Договоримся о целевой аудитории и контексте использования

Один и тот же инструмент можно «собрать» как набор хаотичных флагов или как продукт с понятным контрактом. Ключевые различия:

  • Кто будет пользоваться? Разработчики настраивают пайплайны, админы запускают утилиты в ночные смены, аналитики запускают команды «по памяти». Интерфейс должен соответствовать уровню пользователя.
  • Где инструмент будет жить? В CI важны повторяемость и отсутствие неожиданностей. В локальном окружении важна вменяемая диагностика и удобство автодополнения.
  • Как часто меняются требования? Если утилита будет жить годами, заранее проектируем версионирование и обратную совместимость.

Из этих вопросов рождается основной принцип CLI-продукта: интерфейс — это API. Его нельзя «ломать без причины», а любые изменения нужно делать предсказуемо.

Командная модель: tool <command> [args]

Для утилит чаще всего хорошо работает иерархия:

  • tool — «корень»,
  • <command> — действие (операция),
  • [args] — параметры операции.

Примеры:

  • tool init — создать конфиг/проект,
  • tool status — проверить состояние,
  • tool run — выполнить процесс,
  • tool config set — изменить параметр конфигурации.

Это проще для пользователя, чем единая куча флагов. Плюс команда позволяет развивать интерфейс без взрывного роста количества опций.

Нейминг: последовательность и предсказуемость

Соблюдайте согласованность:

  • Глаголы для команд (init, run, update, validate)
  • Единые правила для имён опций:
    • короткие (-c) всегда означают одно и то же,
    • длинные (--config) отражают смысл.
  • Для параметров выбора используйте очевидные названия:
    • --profile, --environment, --format.

Идемпотентность и повторяемость

Если команда меняет состояние (создаёт файлы, пишет в базу, меняет конфигурацию), стоит продумать:

  • идемпотентна ли она? Например, init часто делает «если не существует — создай».
  • как откатывать изменения? Даже если откат не реализован, интерфейс должен не оставлять пользователя в «полусостоянии». Обязательна транзакционность на уровне операций или хотя бы понятная схема фиксации.

Версионирование интерфейса и данных

У CLI есть две сущности:

  1. версия самого инструмента (package version),
  2. версия формата конфигурации/схемы данных.

Минимальные практики:

  • команда tool --version и вывод формата вида tool 1.8.3 (python 3.12).
  • для конфигов — явный schema_version и миграции (пусть даже простые).
  • поведение по умолчанию лучше закрепить: например, без --config берём ~/.config/tool/config.toml, но если файла нет — подсказываем tool init.

Конфиги: хранение, приоритеты источников и миграции

Где должен жить конфиг

Чаще всего:

  • пользовательский конфиг: ~/.config/<tool>/config.toml или .yaml,
  • проектный конфиг: ./<file> внутри репозитория (например, ./.tool/config.toml),
  • переменные окружения как «override».

Главная идея: разделить конфиг на слои и сделать приоритеты прозрачными.

Приоритет источников конфигурации

Рекомендуемая схема (от слабого к сильному):

  1. конфиг по умолчанию (вшитые дефолты),
  2. конфиг в проекте (./.tool/config.toml),
  3. конфиг пользователя (~/.config/tool/config.toml),
  4. конфиг, указанный явно (--config path/to/file),
  5. переменные окружения (TOOL_*),
  6. аргументы командной строки (tool run --threads 8).

Если приоритеты не фиксировать, пользователь начнёт «искать магию» — почему значение не поменялось.

Формат конфигурации и выбор сериализации

TOML, YAML, JSON — все варианты возможны. Для CLI-продукта важнее другое:

  • читабельность для человека,
  • поддержка комментариев (TOML/YAML),
  • стабильность парсинга.

Если утилита будет активно конфигурироваться руками — TOML часто практичен.

Миграции схемы конфигурации

В какой-то момент конфигурация будет «устаревать». Минимальный подход:

  • поле schema_version,
  • при загрузке:
    • если версия меньше текущей — попытаться мигрировать,
    • если версия больше — отказ с пояснением: «конфиг создан новой версией, обновите инструмент».

Help-text и документация: как сделать интерфейс самообъясняющимся

Help-text — это часть UX

Пользователь не обязан читать README, чтобы начать работу. Но он должен:

  • понимать, что инструмент умеет,
  • видеть минимальные примеры,
  • понимать, какие значения ожидаются,
  • получать подсказки при ошибках.

Это достигается:

  • адекватными описаниями команд,
  • осмысленными названиями параметров,
  • форматированным help.

Правила написания help-text

  1. Коротко в одну строку: описывайте назначение команды.
  2. Детали — ниже: для сложных режимов добавляйте секции.
  3. Пишите примеры прямо в help:
    • tool init --force
    • tool run --config ./config.toml
  4. Не дублируйте README: help — это оперативная справка.

Auto-completion и форматирование ошибок в help

Если CLI поддерживает shell completion, пользователь быстрее освоится. Многие фреймворки для Python CLI дают это автоматически, но нужно включить генерацию скриптов completion.

Даже без completion — корректный help уже снижает количество «обращений в чат» вида: «не знаю, как задать параметр».


Обработка ошибок: архитектура, exit-коды и сообщения без паники

Почему «плохие ошибки» — это главная причина фрустрации

Пользователь CLI обычно делает одно из двух:

  • запускает команду и видит сообщение «что-то пошло не так»,
  • пытается понять причину по логам.

Если сообщение:

  • не содержит контекст (какая команда? какой файл? какой параметр?),
  • не указывает ожидаемое поведение,
  • не различает типы ошибок,

то пользователь будет действовать методом тыка — а это дорогая поддержка.

Категории ошибок

Практичная классификация:

  1. Ошибки конфигурации:
    • не найден файл конфигурации,
    • неверный формат,
    • невалидные значения.
  2. Ошибки ввода:
    • отсутствует обязательный аргумент,
    • некорректное значение флага (например, --threads -1).
  3. Ошибки среды:
    • нет доступа к файлу,
    • отсутствуют необходимые зависимости,
    • тайм-аут сети.
  4. Ошибки выполнения:
    • операция упала,
    • не получилось завершить задачу.

Важно: разные ошибки должны приводить к разным exit-кодам.

Договорённость по exit-кодам

Пример базовой сетки (можно адаптировать):

  • 0 — успех,
  • 2 — ошибка аргументов (usage error),
  • 3 — ошибка конфигурации,
  • 4 — ошибка среды/прав доступа,
  • 5 — ошибка выполнения (runtime).

При этом:

  • в stderr пишем диагностическое сообщение,
  • stdout — только данные для конвейера (если CLI поддерживает вывод).

Сообщения об ошибках: минимально достаточный контент

Хорошее сообщение обычно содержит:

  • краткая причина,
  • какие параметры были проблемными,
  • что сделать пользователю (action),
  • (опционально) как получить подробности --debug.

Практический сценарий: проектируем утилиту seedmgr

Рассмотрим утилиту, которая помогает поддерживать набор «seed’ов» (конфигураций) для тестовых данных и окружений. Сценарии типичные:

  • init — создать шаблон конфигурации,
  • validate — проверить конфиг на валидность,
  • run — применить конфиг,
  • config get/set — управлять ключами.

Сразу заложим:

  • приоритеты конфигов,
  • версионирование схемы,
  • единый слой ошибок,
  • понятный help-text.

Ниже пример на Python с использованием Typer (проект удобен для CLI, а также поддерживает структурирование команд и help-text).

Структура проекта

code
seedmgr/
  seedmgr/
    __init__.py
    cli.py
    config.py
    errors.py
    schema.py
    migration.py
  pyproject.toml

Типы ошибок и exit-коды

seedmgr/errors.py:

code
from dataclasses import dataclass

@dataclass
class CliError(Exception):
    message: str
    exit_code: int = 1
    hint: str | None = None

class UsageError(CliError):
    def __init__(self, message: str, hint: str | None = None):
        super().__init__(message=message, exit_code=2, hint=hint)

class ConfigError(CliError):
    def __init__(self, message: str, hint: str | None = None):
        super().__init__(message=message, exit_code=3, hint=hint)

class EnvironmentError(CliError):
    def __init__(self, message: str, hint: str | None = None):
        super().__init__(message=message, exit_code=4, hint=hint)

class RuntimeErrorCli(CliError):
    def __init__(self, message: str, hint: str | None = None):
        super().__init__(message=message, exit_code=5, hint=hint)

Загрузка конфигурации с приоритетами

seedmgr/config.py (пример с TOML; форматы могут быть другими):

code
from __future__ import annotations

import os
from pathlib import Path

import tomllib  # Python 3.11+. Для старых — tomli

from .errors import ConfigError

DEFAULT_SCHEMA_VERSION = 1

def _load_toml(path: Path) -> dict:
    try:
        with path.open("rb") as f:
            return tomllib.load(f)
    except FileNotFoundError:
        return {}
    except Exception as e:
        raise ConfigError(
            f"Не удалось прочитать конфигурацию: {path}",
            hint=f"Проверьте формат TOML. Детали: {e}",
        )

def load_config(
    config_path: str | None,
    schema_version: int = DEFAULT_SCHEMA_VERSION,
) -> dict:
    """
    Приоритеты:
    1) дефолт
    2) проект: ./.tool/config.toml
    3) пользователь: ~/.config/seedmgr/config.toml
    4) --config path
    5) env TOOL_*
    6) позже можно переопределять значениями из аргументов командной строки
    """
    merged: dict = {
        "schema_version": schema_version,
        "environment": "local",
        "seeds": {},
    }

    project_cfg = Path.cwd() / ".tool" / "config.toml"
    user_cfg = Path.home() / ".config" / "seedmgr" / "config.toml"

    merged.update(_load_toml(project_cfg))
    merged.update(_load_toml(user_cfg))

    if config_path:
        explicit = Path(config_path).expanduser().resolve()
        merged.update(_load_toml(explicit))
        merged["__config_source__"] = str(explicit)

    # Env overrides
    # Пример: TOOL_ENVIRONMENT=prod
    env_prefix = "TOOL_"
    for k, v in os.environ.items():
        if not k.startswith(env_prefix):
            continue
        key = k[len(env_prefix):].lower()
        # Очень простой маппинг. В реальном проекте лучше делать строгую типизацию.
        if key == "environment":
            merged["environment"] = v

    # Минимальная проверка схемы
    if "schema_version" not in merged:
        raise ConfigError(
            "В конфигурации отсутствует поле schema_version.",
            hint="Сгенерируйте конфиг командой `seedmgr init`.",
        )

    return merged

Миграции схемы (минимальный каркас)

seedmgr/migration.py:

code
from .errors import ConfigError

def migrate_config(config: dict, current_schema: int) -> dict:
    version = config.get("schema_version")
    if version == current_schema:
        return config

    if version is None:
        raise ConfigError("Неизвестная версия схемы конфигурации.")

    if version > current_schema:
        raise ConfigError(
            f"Конфиг создан для schema_version={version}, "
            f"а текущий seedmgr поддерживает только до {current_schema}.",
            hint="Обновите seedmgr до более новой версии.",
        )

    # Пример миграции: при version 0 -> 1 добавляем недостающие поля
    if version == 0 and current_schema == 1:
        config = dict(config)
        config.setdefault("environment", "local")
        config.setdefault("seeds", {})
        config["schema_version"] = 1
        return config

    raise ConfigError(
        f"Миграция schema_version={version} -> {current_schema} не реализована.",
        hint="Проверьте документацию или создайте новый конфиг командой init.",
    )

Клиентский интерфейс: команды, help-text, ошибки

seedmgr/cli.py:

code
from typing import Optional

import typer
import sys

from .config import load_config, DEFAULT_SCHEMA_VERSION
from .errors import CliError, UsageError, ConfigError, EnvironmentError
from .migration import migrate_config

app = typer.Typer(
    add_completion=True,
    no_args_is_help=True,
    help="seedmgr — утилита для управления конфигами seed’ов и окружений."
)

@app.callback(invoke_without_command=False)
def main(
    ctx: typer.Context,
):
    # Общий хук можно использовать для логирования/telemetry,
    # но тут оставим минимально.
    return

@app.command(help="Создаёт шаблон конфигурации в проекте и/или в домашнем каталоге.")
def init(
    config_path: Optional[str] = typer.Option(
        None,
        "--config",
        "-c",
        help="Путь для записи конфигурации. Если не указан — создаст ./ .tool/config.toml."
    ),
    force: bool = typer.Option(False, "--force", help="Перезаписать существующий конфиг."),
):
    cfg_path = config_path
    if cfg_path is None:
        cfg_path = str((typer.get_app_dir("seedmgr") / ".." / ".tool" / "config.toml").resolve())

    # Для примера допустим, что init создаёт файл в указанном месте.
    # В реальном коде — продумать структуру директорий.
    from pathlib import Path
    path = Path(cfg_path)

    if path.exists() and not force:
        raise UsageError(
            f"Файл конфигурации уже существует: {path}",
            hint="Используйте --force, если вы уверены.",
        )

    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        "schema_version = 1\n"
        "environment = \"local\"\n"
        "[seeds]\n"
        "  example = \"value\"\n",
        encoding="utf-8"
    )
    typer.echo(f"OK: конфиг создан: {path}")

@app.command(help="Проверяет конфигурацию на валидность и совместимость со схемой.")
def validate(
    config: Optional[str] = typer.Option(None, "--config", help="Явный путь к конфигурации."),
):
    raw = load_config(config_path=config, schema_version=DEFAULT_SCHEMA_VERSION)
    # Миграции
    cfg = migrate_config(raw, current_schema=DEFAULT_SCHEMA_VERSION)

    seeds = cfg.get("seeds", {})
    if not isinstance(seeds, dict):
        raise ConfigError("Поле [seeds] должно быть таблицей (ключ-значение).")

    typer.echo("OK: конфигурация валидна.")
    typer.echo(f"environment={cfg.get('environment')}")

@app.command(help="Применяет конфигурацию и выполняет сценарий генерации/обновления seed’ов.")
def run(
    config: Optional[str] = typer.Option(None, "--config", help="Явный путь к конфигурации."),
    dry_run: bool = typer.Option(False, "--dry-run", help="Не вносить изменения; только проверить план."),
    environment: Optional[str] = typer.Option(
        None,
        "--environment",
        help="Переопределить environment

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

Typer PRO

Курс по теме

Typer PRO

С нуля до уверенной работы с CLI-проектами на Python. Подходит для начинающих и тех, кто уже пишет на Python. Разберитесь с Typer раз и навсегда — и перестаньте копировать команды вслепую.

Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Простыми словами о защищенном соединении

Простыми словами о защищенном соединении

Базовый курс для обычных людей: что такое защищённое соединение, как оно устроено, что оно умеет и чего не умеет, как выбрать сервис и настроить его на своих устройствах.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Сервис, который не падает - как построить безопасную зашифрованную сеть

Сервис, который не падает - как построить безопасную зашифрованную сеть

Практический курс по безопасности сети: как собрать собственный сервис защищённого доступа - от пустого сервера до подписки с автовыбором - и, что важнее, как потом не уронить его своими руками.

Перейти к курсу

Другие статьи

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта
пишем

Пишем CLI для разработчика на Python: команды, конфиги и корректные exit codes по стандарту проекта

Построим удобный интерфейс командной утилиты: структура подкоманд, чтение конфигов, валидация входных параметров и единая логика ошибок с понятными exit code. Сфокусируемся на UX для тех, кто запускает CLI в CI и в скриптах.

15 сентября 2026 г.
1030
Docker Compose для реальных проектов: конфиги, сети, volume и воспроизводимые окружения
docker

Docker Compose для реальных проектов: конфиги, сети, volume и воспроизводимые окружения

Научимся поднимать набор сервисов одним файлом, правильно разделять окружения и делать так, чтобы «у меня работает» исчезло.

20 июля 2026 г.
730
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1431
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1090
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
630

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Команды как продукт: структура, нейминг и «контракты» интерфейсаДоговоримся о целевой аудитории и контексте использованияКомандная модель: tool <command> [args]Нейминг: последовательность и предсказуемостьИдемпотентность и повторяемостьВерсионирование интерфейса и данныхКонфиги: хранение, приоритеты источников и миграцииГде должен жить конфигПриоритет источников конфигурацииФормат конфигурации и выбор сериализацииМиграции схемы конфигурацииHelp-text и документация: как сделать интерфейс самообъясняющимсяHelp-text — это часть UXПравила написания help-textAuto-completion и форматирование ошибок в helpОбработка ошибок: архитектура, exit-коды и сообщения без паникиПочему «плохие ошибки» — это главная причина фрустрацииКатегории ошибокДоговорённость по exit-кодамСообщения об ошибках: минимально достаточный контентПрактический сценарий: проектируем утилиту seedmgrСтруктура проектаТипы ошибок и exit-кодыЗагрузка конфигурации с приоритетамиМиграции схемы (минимальный каркас)Клиентский интерфейс: команды, help-text, ошибки