Создание CLI на Node.js с Commander.js: практическое руководство

CLI (интерфейс командной строки) — текстовые приложения для терминала, выполняющие конкретные задачи. Они широко используются разработчиками и системными администраторами. CLI обычно взаимодействуют с ОС или удалёнными API и реагируют на команды, опции и аргументы пользователя.
Что такое командная строка и как она работает
Командная строка позволяет общаться с программой через текстовые команды. Поведение CLI часто зависит от подкоманды, опций и аргументов.
Пример: команда ls показывает содержимое каталога:
ls -l /homeКомпоненты команды:
- имя программы: ls
- опция/флаг: -l (показывает подробный вывод)
- аргумент: /home (путь к каталогу)
Следование принятым соглашениям делает ваш CLI предсказуемым для знакомых с командной строкой пользователей.
Знакомство с Commander.js
Commander.js — популярная библиотека для создания CLI в Node.js. Она упрощает определение подкоманд, опций, аргументов и автоматическую генерацию справки. В сочетании с библиотеками для оформления (например, Chalk) и HTTP-запросов (Axios) можно быстро получить полноценный инструмент.
Ключевая идея: Commander занимается парсингом и помощью по использованию, вы реализуете бизнес-логику.
Пример проекта: urbanary-cli
Мы создаём утилиту urbanary-cli, которая получает значения слов и сленга из Urban Dictionary через публичный API (RapidAPI). Цели проекта:
- изучить структуру CLI с подкомандами и опциями;
- реализовать HTTP-запросы на Axios;
- подготовить пакет к глобальной установке и публикации в npm.
Создайте папку проекта и инициализируйте проект:
mkdir urbanary-cli
cd urbanary-cli
npm init -yЭтот CLI будет использовать Axios для запросов к Urban Dictionary. Для проверки доступных эндпоинтов и ключей удобно пользоваться RapidAPI.
Простая структура: подкоманда и автоматическая справка
Установим зависимости:
npm install commander axiosСоздайте папку bin и файл index.js:
mkdir bin
cd bin
touch index.jsФайл bin/index.js будет точкой входа для CLI.
Импортируем объект program:
const { program } = require('commander');Определим подкоманду find, которая принимает обязательный аргумент
// index.js
program
.command('find ')
.description('find meaning of a word or abbreviation or slang') Угловые скобки означают обязательный аргумент; используйте квадратные скобки [] для необязательного.
Добавьте парсинг команд, чтобы увидеть вывод справки:
program.parse()Запуск программы с –help или -h выдаст автоматически сгенерированную справку.
Опции и окончательная конфигурация программы
Опции добавляются методом option. Примеры:
program.option('-e, --example', "Display examples")
program.option(
'-c, --count [amount]',
'amount of definitions to display (max is 10)'
)Здесь [amount] — параметр со значением. Опция может быть короткой и длинной формой.
Добавим действие для подкоманды find с обработкой опций:
program
.command('find ')
.description('find meaning of a word or abbreviation or slang')
.option('-e, --example', "Display examples")
.option(
'-c, --count [amount]',
'amount of definitions to display (max is 10)'
)
.action(async (word, options) => {}); Примеры использования:
urbanary-cli find lol -e -c 3
# или
urbanary-cli find lol --example --count 3См. страницу Commander на npm для дополнительных возможностей.
Реализация логики с Axios
Импортируйте Axios в index.js:
const axios = require('axios');Пример подготовки запроса к RapidAPI:
let requestOptions = {
method: 'GET',
URL: "https://mashape-community-urban-dictionary.p.rapidapi.com/define",
params: { term: word },
headers: {
'X-RapidAPI-Key': YOUR_RAPID_API_KEY,
'X-RapidAPI-Host': 'mashape-community-urban-dictionary.p.rapidapi.com'
}
}Выполнение запроса и обработка ответа:
try {
let resp = await axios.request(requestOptions);
console.log(`Definitions for ${word} fetched`);
wordData = resp.data.list;
} catch (err) {
console.error(err.message)
}Далее — форматирование вывода в зависимости от опций:
if (options.example && options.count) {
let cnt = 1;
let definitions = wordData.slice(0, options.count);
definitions.forEach((elem) => {
console.log(`Definition ${cnt++}: ${elem.definition}`);
console.log(`Example:\n${elem.example}\n`);
});
} else if (options.count && !options.example) {
let cnt = 1;
let definitions = wordData.slice(0, options.count);
definitions.forEach((elem) => {
console.log(`Definition ${cnt++}: ${elem.definition}`);
});
} else if (options.example) {
console.log(`Definition: ${wordData[0].definition}`);
console.log(`Example:\n${wordData[0].example}`);
} else {
console.log(`Definition: ${wordData[0].definition}`);
}Этот код определяет поведение: комбинированный вывод с примерами, только дефиниции, одна дефиниция с примером или дефолтный вывод.
Сделать CLI исполняемым и опубликовать
Добавьте shebang в начало bin/index.js:
#!/usr/bin/env nodeОтредактируйте package.json: укажите main и bin:
"main": "./bin/index.js",
"bin": {
"urbanary-cli": "./bin/index.js"
},Имя под ключом bin — это команда, которую вы будете запускать в терминале.
Установите глобально для теста:
npm install -gПосле этого команда urbanary-cli будет доступна глобально. Для публикации — npm publish из директории проекта.
Node.js упрощает создание и публикацию CLI по сравнению с низкоуровневыми языками, так как экосистема npm предоставляет механизмы упаковки и распространения.
Лучшие практики разработки CLI
- Ясные имена команд и опций. Пользователь должен угадать назначение команды без чтения документации.
- Подробная помощь и примеры в –help.
- Корректный код выхода (exit codes): 0 — успех, >0 — ошибка.
- Обработка ошибок сети и таймауты.
- Валидация аргументов и границ (например, max count).
- Логирование и уровень Verbose/Quiet (добавьте -v/–verbose и –quiet).
Безопасность и управление секретами
- Никогда не хардкодьте API-ключи в репозиторий. Храните в переменных окружения или в файлах .env (с .gitignore).
- Для локальной разработки используйте dotenv:
npm install dotenvВ коде:
require('dotenv').config();
const KEY = process.env.RAPIDAPI_KEY;- При публикации в npm убедитесь, что package.json не содержит чувствительных данных. Добавьте .npmignore или укажите files.
Альтернативы Commander.js и когда их выбрать
- yargs — хорош для сложного парсинга опций и совместимости с POSIX; имеет автогенерацию справки.
- oclif — фреймворк от Heroku для крупных CLI-проектов с плагинами и автотестами.
- minimist — минимальная библиотека для простых задач.
Выбор зависит от масштаба: для простых утилит достаточно Commander, для крупных проектных CLI с плагинами oclif может быть удобнее.
Тестирование и CI
- Юнит-тесты: выделяйте бизнес-логику в модули, не зависящие от process.argv, и тестируйте их отдельно.
- Интеграционные тесты: запускайте bin-файлы в child_process.exec и проверяйте stdout/stderr и код выхода.
- CI: в GitHub Actions добавьте шаги для установки Node, npm ci и выполнения тестов, линтинга и возможного publish при релизе.
Пример простого теста запуска команды:
const { exec } = require('child_process');
exec('node ./bin/index.js find lol --count 1', (err, stdout, stderr) => {
// assert stdout содержит ожидаемую строку
});Критерии приёмки
- Команда urbanary-cli find
возвращает статус 0 при успешном выполнении. - Корректно обрабатываются опции -e/–example и -c/–count.
- При некорректных параметрах выводится человекочитаемая ошибка и код выхода > 0.
- API-ключ загружается из переменных окружения и не попадает в репозиторий.
- Документация в README включает примеры использования и команду установки.
Отказоустойчивость и обработка ошибок
- Сетевые ошибки: retry с экспоненциальной задержкой (сколько попыток — настраиваемо).
- Таймауты: задавайте ограничение для axios (timeout) и корректно завершайте запрос.
- Пустой результат: информируйте пользователя, что слово не найдено.
Пример axios с таймаутом и retry-политикой (упрощённо):
const axios = require('axios');
async function fetchWithRetry(opts, retries = 2) {
for (let i = 0; i <= retries; i++) {
try {
return await axios.request(opts);
} catch (err) {
if (i === retries) throw err;
await new Promise(r => setTimeout(r, 500 * Math.pow(2, i)));
}
}
}Производительность и кеширование
Если ваш CLI часто делает одинаковые запросы, добавьте локальное кеширование (file-based или memory). Для короткой жизни процесса достаточно in-memory кеша. Для длительного кеширования используйте файл в ~/.cache/urbanary-cli.
Развёртывание и распространение
Опции установки:
- глобальная установка: npm install -g
- локальная установка и использование через npx: npx urbanary-cli find lol
- публикация в npm: npm publish (убедитесь в конфигурации package.json и доступе на npm)
Перед публикацией проверьте:
- правильную версию (semver);
- наличие полей repository, license, description;
- отсутствие приватных ключей в пакете.
Модели зрелости CLI (уровни)
- Уровень 0: одноразовый скрипт без опций.
- Уровень 1: парсинг опций и минимальная справка (–help).
- Уровень 2: подкоманды, тесты, CI.
- Уровень 3: плагины/пакеты, расширяемость, документация и локализация.
urbanary-cli стремится к уровню 1–2.
Примеры неудач и когда подход не подходит
- Если приложение требует высокопроизводительных потоковых операций и управлением памятью — лучше использовать языки со статической типизацией (Rust, Go).
- Если нужен графический интерфейс — CLI не заменит GUI.
- Если аудит безопасности крайне строгий (сертификации), возможно, придётся избегать сторонних рантаймов.
Рекомендации по локализации и i18n
Если хотите поддержать разные языки, отделяйте тексты интерфейса от логики и используйте JSON/YAML-файлы с переводами. Для CLI важно также локализовать сообщения об ошибках и справочную информацию.
Чеклист для ролей
Разработчик:
- Разделил бизнес-логику и обработку CLI
- Добавил валидные тесты
- Не хранит ключи в коде
Поддерживающий инженер:
- Настроил CI для тестов и линтинга
- Проверил процесс выпуска релизов
QA:
- Прогнал интеграционные тесты для всех подкоманд
- Проверил поведение при отсутствии сети
Оператор/DevOps:
- Проверил передачу переменных окружения
- Настроил кеширование и мониторинг ошибок
Примеры тест-кейсов и сценариев приёма
- Запуск: urbanary-cli find hello -> возвращает код 0 и одну дефиницию.
- Параметры: urbanary-cli find foo -c 3 -> выводит 3 дефиниции.
- Ошибка сети: симулировать недоступность API и проверить код выхода и сообщение.
- Без ключа: при отсутствии API-ключа — программа даёт дружелюбную подсказку и не падает с неконтролируемой ошибкой.
Маленькая методология разработки CLI
- Спроектируйте команды и опции перед кодированием.
- Выделите ядро логики в чистые функции.
- Реализуйте CLI-обёртку, используя Commander.
- Добавьте логирование и обработку ошибок.
- Напишите тесты и настроьте CI.
- Подготовьте README с примерами и инструкциями по установке.
Мини-FAQ
Как сохранить API-ключ безопасно?
Храните ключи в переменных окружения или в .env файле, который указан в .gitignore. Для CI используйте защищённые секреты.
Нужна ли глобальная установка для тестирования?
Нет. Можно использовать npx для запуска без глобальной установки или npm link для локальной разработки.
Как поддерживать версии и совместимость?
Используйте семантическое версионирование (semver) и документируйте изменений в changelog.
Часто используемые команды и шаблоны
- Установка зависимостей: npm install commander axios
- Линтинг: npm run lint
- Тесты: npm test
- Публикация: npm publish
Резюме
- Commander.js значительно упрощает создание CLI-приложений на Node.js.
- Выделяйте логику из обёртки CLI и тестируйте её отдельно.
- Безопасно храните ключи и готовьте пакет к публикации.
- Добавьте обработку ошибок, таймауты и возможность кеширования для устойчивости.
Ключевые шаги: спроектировать интерфейс команд, реализовать HTTP-запросы, добавить опции и справку, настроить package.json и опубликовать пакет.
JSON-LD для FAQ добавлен ниже.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone