CLI аргументы

CLI-утилиты в Node.js почти всегда опираются на входные аргументы командной строки, которые приходят в виде строк и требуют строгой проверки перед использованием. Основная проблема заключается в отсутствии типизации: числа, булевы значения, списки и флаги передаются как строки, что делает валидацию обязательным этапом.

Библиотека Joi позволяет формализовать структуру CLI-аргументов через декларативные схемы и выполнять их проверку с преобразованием типов, значениями по умолчанию и строгими ограничениями.


Представление CLI-аргументов в Node.js

Аргументы командной строки доступны через:

  • process.argv
  • сторонние парсеры: yargs, commander, minimist

Типичный пример:

node app.js --port=3000 --debug=true --files=a.txt,b.txt

После парсинга получается объект:

{
  port: "3000",
  debug: "true",
  files: "a.txt,b.txt"
}

Основная задача Joi — привести этот объект к строгой структуре:

{
  port: 3000,
  debug: true,
  files: ["a.txt", "b.txt"]
}

Базовая схема Joi для CLI-параметров

Joi позволяет описывать схему входных данных через цепочки методов:

import Joi from 'joi';

const schema = Joi.object({
  port: Joi.number().integer().min(1).max(65535).default(3000),
  debug: Joi.boolean().default(false),
  files: Joi.array().items(Joi.string()).default([])
});

Валидация объекта аргументов

После парсинга CLI-аргументов они передаются в Joi:

const args = {
  port: process.env.PORT,
  debug: process.argv.includes('--debug'),
  files: process.argv
    .find(arg => arg.startsWith('--files='))
    ?.split('=')[1]
    ?.split(',')
};

const result = schema.validate(args, {
  convert: true,
  abortEarly: false
});

Параметры validate() и их значение

convert

Включает автоматическое преобразование типов

  • "3000"3000
  • "true"true

Используется по умолчанию и критически важен для CLI.


abortEarly

Контролирует поведение при ошибках:

  • true — остановка на первой ошибке
  • false — сбор всех ошибок
{
  abortEarly: false
}

allowUnknown и stripUnknown

CLI часто содержит лишние параметры.

{
  allowUnknown: true,
  stripUnknown: true
}
  • allowUnknown — разрешает неизвестные поля
  • stripUnknown — удаляет их из результата

Преобразование строковых флагов CLI

CLI почти всегда передаёт всё как строки:

Булевы значения

debug: Joi.boolean()

Вход:

--debug=true

Результат:

true

Числа

port: Joi.number().integer()

Вход:

--port=8080

Результат:

8080

Списки

CLI:

--files=a.js,b.js,c.js

Схема:

files: Joi.array().items(Joi.string())

Подготовка:

const files = rawFiles?.split(',');

Обязательные параметры CLI

Joi позволяет задавать строгую обязательность:

input: Joi.string().required()

Если параметр отсутствует:

Error: "input" is required

Условные зависимости параметров

CLI часто имеет взаимосвязанные аргументы:

const schema = Joi.object({
  mode: Joi.string().valid('dev', 'prod'),
  debug: Joi.boolean()
}).when(Joi.object({ mode: 'dev' }).unknown(), {
  then: Joi.object({
    debug: Joi.boolean().default(true)
  })
});

Пользовательские сообщения об ошибках

CLI требует понятных сообщений:

const schema = Joi.object({
  port: Joi.number().required().messages({
    'number.base': 'Порт должен быть числом',
    'any.required': 'Не указан порт запуска'
  })
});

Предобработка аргументов перед Joi

Joi работает с уже структурированными данными, поэтому требуется парсинг CLI:

Простой парсер

function parseArgs(argv) {
  const args = {};
  argv.forEach(arg => {
    if (arg.startsWith('--')) {
      const [key, value] = arg.replace('--', '').split('=');
      args[key] = value ?? true;
    }
  });
  return args;
}

Пример интеграции с CLI-приложением

import Joi from 'joi';

const schema = Joi.object({
  port: Joi.number().integer().min(1).max(65535).default(3000),
  host: Joi.string().default('localhost'),
  debug: Joi.boolean().default(false)
});

function parseArgs(argv) {
  const args = {};
  for (const arg of argv) {
    if (arg.startsWith('--')) {
      const [key, value] = arg.slice(2).split('=');
      args[key] = value ?? true;
    }
  }
  return args;
}

const rawArgs = parseArgs(process.argv.slice(2));

const { value, error } = schema.validate(rawArgs, {
  convert: true,
  abortEarly: false,
  stripUnknown: true
});

if (error) {
  console.error(error.details.map(d => d.message).join('\n'));
  process.exit(1);
}

console.log('Конфигурация:', value);

Валидация сложных структур CLI

CLI может содержать вложенные структуры:

--db.host=localhost --db.port=5432

Парсинг:

function deepParse(argv) {
  const result = {};

  argv.forEach(arg => {
    if (!arg.startsWith('--')) return;

    const pathValue = arg.slice(2);
    const [path, value] = pathValue.split('=');

    const keys = path.split('.');
    let current = result;

    keys.forEach((key, index) => {
      if (index === keys.length - 1) {
        current[key] = value ?? true;
      } else {
        current[key] = current[key] || {};
        current = current[key];
      }
    });
  });

  return result;
}

Схема Joi:

const schema = Joi.object({
  db: Joi.object({
    host: Joi.string().required(),
    port: Joi.number().integer().required()
  })
});

Использование stripUnknown для CLI-гигиены

CLI часто содержит вспомогательные флаги:

--verbose --trace --experimental

Чтобы не загрязнять конфигурацию:

stripUnknown: true

Результат будет содержать только описанные в схеме поля.


Комбинирование с environment-переменными

CLI часто дополняется env:

const config = {
  port: process.env.PORT,
  debug: process.env.DEBUG === 'true'
};

Схема Joi:

const schema = Joi.object({
  port: Joi.number().default(3000),
  debug: Joi.boolean().default(false)
});

CLI может переопределять env:

const finalConfig = schema.validate({
  ...config,
  ...rawArgs
}).value;

Валидация флагов без значений

Флаги вида:

--verbose
--silent

Обрабатываются как boolean:

verbose: Joi.boolean().default(false)

Парсер:

args[key] = value === undefined ? true : value;

Ограничение допустимых значений CLI

mode: Joi.string().valid('development', 'production', 'test')

CLI:

--mode=production

При ошибке:

"mode" must be one of [development, production, test]

Кастомные валидаторы для CLI

const schema = Joi.object({
  file: Joi.string().custom((value, helpers) => {
    if (!value.endsWith('.json')) {
      return helpers.error('any.invalid');
    }
    return value;
  })
});

Валидация массивов через повторяющиеся аргументы

CLI:

--tag=js --tag=node --tag=backend

Парсер:

if (!args.tag) args.tag = [];
args.tag = Array.isArray(args.tag) ? args.tag : [args.tag];

Схема:

tags: Joi.array().items(Joi.string())