Дизайн CLI для разработчиков: команды как API, а не как “кнопки”
Научимcя проектировать интерфейс команд: структуру subcommands, удобные флаги, конфиги, автодополнение и прогнозируемое поведение при ошибках.
Содержание
Дизайн CLI для разработчиков: команды как API, а не как “кнопки”
CLI (command-line interface) для разработчиков часто воспринимают как «обвязку» вокруг утилиты: нажал команду — получил результат. Но практики, которые дают удобство на долгом горизонте, ближе не к консольной магии, а к дизайну API: у команды есть контракт, входные параметры, схема ошибок, поведение при сбоях и — что особенно важно — совместимость между версиями.
Если относиться к CLI как к API, меняется всё: структура subcommands становится похожей на роутинг, флаги — на сериализацию аргументов, конфиги — на источник правды, а автодополнение и формализованные сообщения об ошибках — на «документацию в моменте». В этой статье разберём, как проектировать интерфейс команд так, чтобы он был удобным, прогнозируемым и устойчивым.
CLI как API: базовая модель контракта
Команда = операция с контрактом
API разработчикам нужен не «инструмент», а предсказуемая операция. В CLI операция — это конкретная комбинация:
- бинарь (например,
mytool) - subcommand (например,
deploy) - параметры и флаги (например,
--env prod --dry-run) - конфигурация (из файлов, переменных окружения, значений по умолчанию)
- контекст (текущий каталог, cwd, файлы в рабочей директории и т.д.)
Чем лучше вы формализуете этот контракт, тем меньше пользователь будет «угадывать», а тем больше будет полагаться на документацию и автодополнение.
Стабильность важнее «удобства одной сессии»
Пользователь CLI живёт в сценариях: автоматизация, скрипты, CI/CD, повторное выполнение. Поэтому дизайн должен учитывать:
- совместимость CLI между версиями
- недвусмысленные правила приоритетов (что важнее: конфиг или флаг?)
- предсказуемое поведение ошибок
- возможность автоматизированного парсинга выводов (минимум на уровне exit code, иногда JSON)
Exit code — аналог статусов API
В API статусы — часть контракта. В CLI — exit code и формат сообщений об ошибках.
Хороший минимум:
0— успешное завершение1— общая ошибка/непредвиденное поведение2— ошибки использования (неверные аргументы, неизвестный subcommand)- отдельные коды для типичных классов: авторизация, отсутствие ресурсов, таймаут и т.д.
Стабильные exit codes — критически важно для CI. Если вы меняете их, ломаются автоматизации так же, как при смене HTTP-кодов.
Проектирование структуры subcommands: от “команд” к “путям”
Правило: группируйте по домену, а не по внутренней реализации
Subcommands должны соответствовать бизнес-действиям или смысловым операциям:
init,build,test,deployadd,remove,list,updateauth login,auth logoutuser create,user delete,user list
Плохо, когда структура повторяет внутренние модули:
core-buildвместоbuildengine-recompileвместоrebuild
Пользователь не обязан понимать вашу архитектуру. Он должен понимать задачу.
Избегайте плоского списка “всё в одном”
Плоский интерфейс из десятков команд (mytool run-xyz, mytool do-abc, mytool handle-...) ухудшает:
- запоминаемость
- автодополнение
- читаемость help
- discoverability
Разумнее вводить иерархию:
mytool auth loginmytool auth refreshmytool project init
Грамматика: согласуйте названия и глаголы
Выберите стиль раз и придерживайтесь:
- либо глаголы (
deploy,rollback) - либо существительные + действия (
release create,release list) - либо REST-подобная логика (
resource get,resource update)
Главное — единообразие. Когда list используется для одного ресурса, а для другого вдруг show, люди начнут обращаться к документации чаще, чем к памяти — и это признак проблем в дизайне.
Флаги и аргументы: как сделать “удобно” и “неоднозначно”
Определите тип каждого параметра
Флаги и аргументы должны соответствовать типам данных.
- Сущностные параметры (идентификаторы ресурса)
--id <id>--user <name>--project <path>
- Параметры поведения (режимы)
--dry-run--force--verbose--format json
- Параметры конфигурации (значения)
--timeout 30s--region eu-central-1
- Параметры повторяемости
--label key=value(повторяемый флаг)--exclude <pattern>(повторяемый)
Это важно, потому что от типа зависит:
- где будет ошибка при неверном вводе
- как будет валидироваться значение
- как будет выглядеть автодополнение
- как объяснять пользователю «что не так»
Предпочитайте длинные флаги коротким, если есть шанс на рост
Короткие флаги (например, -v) удобны, но плохо масштабируются. Длинные (--verbose) самодокументируются.
Компромисс:
- используйте короткие как алиасы для самых частых настроек
- но делайте длинные основными
Флаг не должен быть “магическим”
Частая ошибка — вводить флаг, который меняет поведение слишком далеко, при этом он неочевиден из названия.
Плохо:
--fast(что именно ускоряется? какой компромисс по качеству?)--smart(по каким правилам? предсказуемо ли?)
Лучше:
--max-parallelism N--skip-tests--strategy incremental--cache enabled|disabled
Если параметр меняет стратегию — назовите стратегию.
Правила приоритета: конфиг vs флаги
CLI неизбежно приходит к конфликтам источников:
- конфигурация в файле (например,
~/.config/mytool/config.toml) - конфигурация в проекте (
./mytool.toml) - переменные окружения (
MYTOOL_TIMEOUT) - аргументы командной строки
Без явного приоритета пользователь будет воспринимать инструмент как «нестабильный».
Типовая схема приоритета, понятная пользователям:
- флаги командной строки > переменные окружения > конфиги > встроенные значения по умолчанию
И это правило стоит:
- описать в
--help - отразить в диагностике, например, в
mytool doctorили в подробном режиме--verbose
Конфиги как источник правды: дизайн, файлы, схемы и миграции
Конфиги не должны быть “темным лесом”
Конфигурация хороша, когда:
- избавляет от повтора флагов
- задаёт окружение проекта
- содержит “длинные значения” (списки, политики)
Но конфигурация плоха, когда:
- пользователь не понимает, какие поля реально применились
- неизвестно, откуда значение пришло
- нет миграции схемы при обновлениях
Стратегия поиска конфигурации
Если конфиг поддерживается, нужно определить предсказуемую логику поиска:
--config path(явно заданный)- иначе конфиг проекта в текущем каталоге
- иначе пользовательский глобальный конфиг
- иначе значения по умолчанию
Важно: сообщать пользователю, какой конфиг был выбран (особенно в verbose-режиме).
Схема и валидация
Конфиг должен быть формализован. Даже если вы используете TOML/YAML/JSON, в реальности у вас есть схема:
- типы полей
- обязательность
- ограничения (
timeout >= 0,regionиз списка и т.д.)
И главное: валидация должна приводить к понятным ошибкам с путём до поля.
Версионирование конфигурации
Если вы измените формат, пользователи получат ошибки в проде. Значит, конфигу нужны:
- поле
schemaVersion - или стратегии миграции (например, автоматически конвертировать старую схему)
- или чёткое сообщение “не поддерживается”, с ссылкой на совместимые версии/миграцию
Автодополнение: интерфейс “в моменте”, а не косметика
Автодополнение — это не бонус. Для CLI-инструмента оно является частью UX и снижает когнитивную нагрузку.
Что автодополнять
- subcommands
- возможные значения для enum-флагов (
--format json|yaml) - имена файлов/директорий для параметров пути (если это безопасно)
- имена сущностей (если инструмент знает контекст; например,
mytool deploy <tab>может предлагать окружения)
Как не сделать автодополнение медленным
Проблема автодополнения — синхронные запросы. Если автодополнение делает сетевые вызовы, оно будет:
- зависать в терминале
- ухудшать интерактивность
Правило:
- автодополнение должно быть быстрым и предсказуемым
- сложные проверки — по факту запуска команды, а не на стадии completion
Формат сообщений о помощи командам
Пользователь смотрит на completions и help. Поэтому справка должна быть структурной:
- одно предложение: что делает операция
- короткий пример
- ключевые параметры
- секция по ошибкам/exit codes (минимально)
Прогнозируемое поведение при ошибках: от “красного текста” к диагностике
Разделяйте ошибки использования и ошибки исполнения
Это критично:
- Ошибки использования (неверная команда/флаг/тип/значение)
- выход:
2(или другой фиксированный код) - сообщение: что именно неверно и как исправить
- подсказка: соответствующий кусок help
- Ошибки исполнения (ресурс недоступен, таймаут, network, permissions)
- выход:
1(или другие коды) - сообщение: что пытались сделать, какой ресурс, на каких параметрах
- возможно: advice “проверь X”
Ошибки должны быть машинно-понятными
Человеческий текст важен, но для автоматизации полезно:
- печатать в stderr
- возвращать стабильный exit code
- опционально предоставлять
--output json(или--format json), особенно в режиме CI
Пример интерфейса:
- по умолчанию: текст
- при
--format json: структурированный объект ошибок
Контекст в сообщениях об ошибках
Плохая ошибка:
failed
Хорошая ошибка:
Failed to parse config: expected integer at
build.timeout, gotabc
Ещё лучше:
- добавить подсказку: “use
--build-timeout 30s” - указать файл и строку (если конфиг поддерживает)
Стек-трейсы: когда они уместны
В обычном режиме CLI не должен печатать внутренний стек. Но в --debug (или MYTOOL_DEBUG=1) стек уместен.
Принцип:
- по умолчанию — кратко и по делу
- debug — детально, для диагностики
Примеры проектирования CLI на практике
Ниже — условный пример “контракта” CLI-инструмента deployctl.
Пример: команды и subcommands
Предположим, у инструмента есть операции:
- управление приложением:
app init,app deploy,app rollback - управление окружениями:
env list,env use - аутентификация:
auth login
Пример структуры:
deployctl app deploydeployctl app rollbackdeployctl env listdeployctl auth login
Пример: флаги и конфиг
Пусть deploy принимает:
--env(enum)--dry-run(boolean)--config(path)--timeout(duration)--format(text|json)
При этом базовые значения должны приходить из конфигурации:
- default env
- timeout
- registry endpoint
Реалистичные сообщения об ошибках
Если --env неизвестен:
Unknown environment
staging2. Supported: dev, staging, prod. Trydeployctl env list.
Если истёк таймаут:
Deploy aborted: request to registry timed out after 30s (endpoint: …). Exit code: 124.
Реализация: как “закрепить” дизайн в коде (пример на Python)
Чтобы дизайн не распался, его нужно поддержать в реализации: валидация, приоритеты, единые коды и формат вывода.
Ниже пример на argparse (упрощённый), который показывает ключевые идеи: subcommands, флаги, чтение конфигов, приоритеты и единый формат ошибок.
# deployctl.py
import argparse
import json
import os
import sys
from pathlib import Path
DEFAULT_CONFIG_PATH = Path.home() / ".config" / "deployctl" / "config.json"
def load_config(path: Path) -> dict:
if not path.exists():
return {}
data = json.loads(path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
def parse_duration(s: str) -> int:
# минимальная версия: "30s"
if not s.endswith("s"):
raise ValueError("duration must end with 's'")
val = int(s[:-1])
if val < 0:
raise ValueError("duration must be >= 0")
return val
def main(argv=None):
argv = argv or sys.argv[1:]
parser = argparse.ArgumentParser(
prog="deployctl",
description="Deploy tool with predictable CLI contract."
)
parser.add_argument("--config", type=Path, default=None, help="Path to config file.")
subparsers = parser.add_subparsers(dest="cmd", required=True)
deploy = subparsers.add_parser("deploy", help="Deploy application.")
deploy.add_argument("--env", type=str, required=False, help="Environment name: dev|staging|prod")
deploy.add_argument("--dry-run", action="store_true", help="Do not apply changes.")
deploy.add_argument("--timeout", type=str, required=False, help="Timeout like 30s")
deploy.add_argument("--format", choices=["text", "json"], default="text")
args = parser.parse_args(argv)
# 1) Конфиг: явный путь > default config
cfg_path = args.config or DEFAULT_CONFIG_PATH
cfg = load_config(cfg_path)
# 2) Источник значений:
# flags > env vars > config > defaults
env = (
args.env
or os.getenv("DEPLOYCTL_ENV")
or cfg.get("env")
or "dev"
)
timeout_raw = (
args.timeout
or os.getenv("DEPLOYCTL_TIMEOUT")
or cfg.get("timeout")
or "30s"
)
try:
timeout_s = parse_duration(timeout_raw)
except ValueError as e:
return fail(2, args.format, f"Invalid timeout '{timeout_raw}': {e}")
supported_envs = ["dev", "staging", "prod"]
if env not in supported_envs:
return fail(
2, args.format,
f"Unknown environment '{env}'. Supported: {', '.join(supported_envs)}."
f" Try `deployctl env list`."
)
# 3) Бизнес-логика (упрощённо)
# Здесь возможны runtime ошибки: сеть/права/ресурсы.
if env == "prod" and not os.getenv("DEPLOYCTL_AUTHED"):
return fail(
1, args.format,
"Authentication required for prod. Run `deployctl auth login`."
)
result = {
"command": "deploy",
"env": env,
"dry_run": bool(args.dry_run),
"timeout_seconds": timeout_s,
"status": "ok"
}
if args.format == "json":
print(json.dumps(result, ensure_ascii=False))
else:
print(f"Deploying to {env} (dry-run={args.dry_run}, timeout={timeout_s}s)")
return 0
def fail(exit_code: int, fmt: str, message: str):
payload = {"error": {"message": message, "code": exit_code}}
if fmt == "json":
print(json.dumps(payload, ensure_ascii=False), file=sys.stderr)
else:
print(f"Error: {message}", file=sys.stderr)
sys.exit(exit_code)
if __name__ == "__main__":
main()
Что здесь отражает дизайн:
deployкак операция с контрактом.- приоритет значений (флаги → env vars → config → default).
- различие между ошибкой использования (
2) и исполнения (1). - единый формат выхода об ошибках (text/json).
--formatполезен для сценариев парсинга.
Даже если ваш стек другой (Go/Rust/Node), принципы те же: контракт, приоритеты, коды, предсказуемые ошибки.
Типичные ошибки дизайна CLI (и как их избегать)
1) “Опции” вместо параметров контракта
Если пользователь не понимает, что делает флаг, вы получаете загадочный инструмент. Решение: называйте поведение стратегически (--strategy incremental), а не эмоционально (--smart).
2) Непредсказуемая конфигурация
Если значение приходит “
Комментарии
Пока нет комментариев