Запущенная программа на Node — это процесс операционной системы. Всё,
что программа знает о своём запуске, лежит в глобальном объекте process:
с какими аргументами её вызвали, какие переменные окружения ей передали,
куда писать вывод. А когда программа заканчивается, она сообщает системе
код выхода — получилось или нет.
process доступен везде без require — как console.
process.argv: аргументы командной строки
Запустим скрипт с аргументами:
node greet.js Аня --loudВнутри скрипта они лежат в массиве process.argv:
console.log(process.argv)
// [
// '/usr/local/bin/node', ← путь к самому node
// '/home/anya/greet.js', ← путь к скрипту
// 'Аня',
// '--loud'
// ]Первые два элемента есть всегда, и ваши аргументы начинаются с индекса 2. Поэтому почти любая утилита начинается так:
const args = process.argv.slice(2) // ['Аня', '--loud']Все аргументы — строки. node sum.js 2 3 даст ['2', '3'], а не числа:
приводить типы нужно самим.
Флаги
У консольных утилит сложились общие договорённости о флагах:
node server.js --port 8080 --verbose --host=0.0.0.0 public--port 8080— флаг со значением: значение идёт следующим аргументом;--verbose— флаг-переключатель без значения: он либо есть, либо нет;--host=0.0.0.0— значение через=, в одном аргументе;public— позиционный аргумент, не флаг.
Разобрать их — обычный цикл по массиву с индексом: увидели --что-то,
заглянули в следующий элемент. Если он есть и не начинается с -- — это
значение, и его надо пропустить на следующем шаге:
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg.startsWith('--')) {
const next = args[i + 1]
// next — значение флага или следующий флаг?
}
}В настоящих проектах флаги разбирают библиотеками (commander, yargs)
или встроенной util.parseArgs. Но устроены они именно так, и написать
такой разбор руками — хорошее упражнение.
process.env: переменные окружения
Настройки, которые меняются от машины к машине — порт, адрес базы данных, секретные ключи, — в код не пишут. Их передают через переменные окружения:
PORT=8080 DEBUG=true node server.jsconsole.log(process.env.PORT) // '8080'
console.log(process.env.DEBUG) // 'true'
console.log(process.env.NOPE) // undefinedГлавное, что нужно помнить: значения в process.env — всегда строки
(или undefined, если переменной нет). 'false' — непустая строка, то есть
истина в if. Поэтому переменные окружения читают в одном месте и сразу
приводят к нужным типам — получается модуль конфигурации:
const port = Number(process.env.PORT || 3000)
if (!Number.isInteger(port)) {
throw new Error(`PORT должен быть целым числом, а не «${process.env.PORT}»`)
}
const config = {
port,
host: process.env.HOST || 'localhost',
debug: process.env.DEBUG === 'true',
}Три правила хорошего конфига:
- значения по умолчанию для всего, без чего можно обойтись;
- приведение типов: число — числом, флаг —
true/false; - падать сразу и громко, если переменная обязательна, а её нет, или в ней мусор. Лучше программа не запустится с понятной ошибкой, чем запустится и через час сломается непонятно где.
Обратите внимание на ловушку parseInt: parseInt('80abc') — это 80,
мусор в конце молча отбрасывается. Number('80abc') честно даёт NaN.
stdout и stderr
У процесса два канала вывода: stdout — для результата и stderr —
для ошибок и диагностики. console.log пишет в stdout, console.error — в
stderr:
console.log('42') // результат
console.error('Ошибка: нет файла') // сообщение об ошибкеЗачем два? Результат утилиты часто передают дальше — в файл или другой
программе: node sum.js 1 2 > result.txt. В файл уйдёт только stdout, а
ошибки останутся на экране и не испортят результат.
Есть и низкоуровневые process.stdout.write(текст) и
process.stderr.write(текст) — они не добавляют перевод строки сами.
Код выхода
Когда процесс завершается, он отдаёт системе число — код выхода.
Договорённость простая: 0 — успех, всё остальное — ошибка (чаще всего
1). По коду выхода скрипты, CI и && в терминале понимают, всё ли
прошло хорошо:
node check.js && echo 'всё хорошо' # echo выполнится, только если код 0Задать код можно двумя способами:
process.exitCode = 1 // запомнить код: процесс выйдет с ним, когда доделает работу
process.exit(1) // выйти прямо сейчасprocess.exit() обрывает программу немедленно: недописанный вывод,
незавершённые запросы и таймеры просто бросаются. Поэтому в обычном коде
предпочитают process.exitCode — сообщить об ошибке, а программа спокойно
закончится сама:
function main(argv) {
const [file] = argv.slice(2)
if (!file) {
console.error('Использование: node cat.js <файл>')
process.exitCode = 1
return
}
// ...основная работа
}
main(process.argv)Удобный приём — функция main(argv), которая получает аргументы
параметром, а не читает process.argv сама. Такую функцию легко
проверить тестом: передал массив — посмотрел на вывод и код выхода.
Если необработанное исключение долетает до верха, Node сам печатает его в
stderr и выходит с кодом 1.