Как Node.js обрабатывает аргументы командной строки: от process.argv до сырцов
Представьте: вы написали крутой скрипт на Node.js, который конвертирует изображения. Но каждый раз править код, чтобы указать имя файла — это моветон. Гораздо круче запустить: node convert.js --input photo.jpg --format webp и получить результат. Как сделать такую магию? Именно этому вопросу, одному из самых популярных на Stack Overflow, мы сегодня и посвятим время.
Казалось бы, что может быть проще? Но под капотом Node.js (и сам V8) скрывает несколько слоёв, о которых полезно знать каждому разработчику. Мы не просто покажем, как получить process.argv — мы заглянем в исходники Node.js и узнаем, как аргументы путешествуют из вашего терминала в переменные окружения процесса. Поехали!
Зачем вообще нужны аргументы командной строки?
Аргументы командной строки (CLI arguments) — это основной способ сделать вашу программу гибкой и переиспользуемой без изменения кода. Вместо хардкода вы передаёте данные на вход. Это основа практически всех DevOps-инструментов, сборщиков (Webpack, Vite) и утилит. Умение работать с ними — базовый навык JavaScript-разработчика, который хочет выйти за рамки браузера.
Уровень 1: Основы — process.argv
Самый простой и нативный способ получить аргументы в Node.js — обратиться к глобальному объекту process.argv. Это массив строк. Давайте сразу на примере.
Файл: args-demo.js
// args-demo.js
console.log(process.argv);
Запустим с аргументами:
node args-demo.js hello --name=World 42
Вывод в консоль:
[
'/usr/local/bin/node', // 0: путь к исполняемому файлу Node.js
'/home/user/args-demo.js', // 1: путь к вашему скрипту
'hello', // 2: первый пользовательский аргумент
'--name=World', // 3: второй аргумент
'42' // 4: третий аргумент
]
Вот и вся магия. process.argv — это просто срез того, что операционная система передала процессу. Первые два элемента — это служебная информация. Всё, что идёт после индекса 2 — это ваши аргументы. Важно: все они — строки. Число 42 пришло как строка "42".
Как отбросить первые два элемента?
Для удобства часто используют деструктуризацию:
// Получаем только пользовательские аргументы
const [, , ...userArgs] = process.argv;
console.log(userArgs); // ['hello', '--name=World', '42']
Или просто process.argv.slice(2).
Уровень 2: Парсим вручную — свой велосипед
Работать с сырым массивом строк неудобно. Нам нужно отличать флаги (--name) от значений (World). Давайте напишем простой парсер, чтобы понять логику.
// simple-parser.js
const args = process.argv.slice(2);
const parsed = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith('--')) {
// Убираем '--' и ищем значение
const key = arg.slice(2);
const next = args[i + 1];
// Если следующий аргумент не начинается с '--', это значение
if (next && !next.startsWith('--')) {
parsed[key] = next;
i++; // Пропускаем следующий шаг итерации
} else {
parsed[key] = true; // Булевый флаг
}
} else if (arg.startsWith('-')) {
// Короткие флаги, например -v
const key = arg.slice(1);
parsed[key] = true;
} else {
// Просто позиционный аргумент
if (!parsed._) {
parsed._ = [];
}
parsed._.push(arg);
}
}
console.log(parsed);
// node simple-parser.js --name World -v hello
// { name: 'World', v: true, _: ['hello'] }
Этот код — основа библиотек вроде minimist. Как видите, ничего сложного. Но в реальных проектах лучше использовать готовые решения, чтобы не изобретать велосипед и не обрабатывать краевые случаи (например, флаги с пробелами: --name "John Doe").
Уровень 3: Под капотом — как Node.js получает argv?
А теперь самое интересное. Как вообще process.argv появляется в нашей программе? Откуда берутся эти строки?
Когда вы запускаете node script.js arg1, ваша оболочка (bash, zsh, cmd) вызывает системный вызов execve() (или его аналог в Windows). Этот вызов передаёт ядру ОС путь к исполняемому файлу (Node.js) и массив строк-аргументов. Ядро создаёт новый процесс и передаёт ему этот массив.
Внутри Node.js (написан на C++ и JavaScript) есть слой инициализации. Когда процесс стартует, выполняется код на C++, который вызывает v8::V8::SetFlagsFromCommandLine (для флагов самого V8) и сохраняет оригинальный массив аргументов в глобальной переменной. Затем, на этапе загрузки JavaScript-окружения, этот массив копируется в свойство process.argv.
Кстати, если заглянуть в исходный код Node.js, можно найти функцию SetProcessArgv, которая как раз и отвечает за это преобразование. Она просто берёт сырой C-массив char** argv и конвертирует его в JavaScript-массив строк.
Важный нюанс: Node.js позволяет передавать флаги самому рантайму до имени скрипта. Например: node --inspect script.js. Эти флаги не попадают в process.argv для вашего скрипта, а обрабатываются до его запуска. Это сделано для того, чтобы скрипт не мог случайно переопределить важные настройки движка.
Уровень 4: Профессиональные решения — commander и yargs
Писать парсеры вручную для продакшена — плохая идея. Есть две главные библиотеки, которые используют все:
1. commander (популярна в CLI-инструментах)
Предоставляет декларативный API и автоматическую генерацию справки (--help).
// install: npm install commander
import { Command } from 'commander';
const program = new Command();
program
.name('image-converter')
.description('CLI to convert images')
.version('1.0.0');
program
.option('-i, --input <path>', 'input file path')
.option('-f, --format <type>', '