Node.js, как и другие среды разработки, предоставляет базовые средства работы с опциями командной строки. В нашем случае это массив process.argv
. Но обычно, кроме простейших случаев типа A + B, обрабатывать опции командной строки вручную очень неудобно. Для этого есть несколько популярных пакетов. Я написал небольшую программу, которая построила сводную таблицу по этим пакетам, выбрал из них три самых популярных и рассмотрел их поближе.
Сводная таблица
Так как я вынужден был разместить здесь эту таблицу как изображение, то ниже привожу список соответствующих ссылок:
- NPM: commander | GitHub: tj/commander.js
- NPM: minimist | GitHub: substack/minimist
- NPM: yargs | GitHub: yargs/yargs
- NPM: optimist | GitHub: substack/node-optimist
- NPM: meow | GitHub: sindresorhus/meow
- NPM: nopt | GitHub: npm/nopt
- NPM: nomnom | GitHub: harthur/nomnom
- NPM: stdio | GitHub: sgmonda/stdio
- NPM: command-line-args | GitHub: 75lb/command-line-args
Эта таблица была сгенерирована небольшой программой на JavaScript. Исходные тексты этого обзора, включая и эту программу, расположены в репозитории на GitHub. Так как через некоторое время эти данные скорее всего устареют, вы можете, загрузив себе эти исходники, перегенерировать эту таблицу, а также пополнить её новыми данными просто добавив соответствующие строки в файл со списком пакетов.
Пакеты в таблице упорядочены по рейтингу, который считается на основе количества звёзд на NPM и GitHub по формуле:
npmStars * k + githubStars
Коэффициент k
понадобился, так как звёзды на NPM выглядят "весомее" звёзд на GitHub. Сам коэффициент считается очень просто: суммируем количество звёзд на NPM и на GitHub, затем делим число звёзд на GitHub на число звёзд на NPM, округляем получившееся число, это и есть наш коэффициент k
:
k = floor( Sgithub / Snpm)
Из получившейся таблицы хорошо видно, что главный фаворит, это пакет commander. Далее идут с близким рейтингом пакеты minimist и yargs. Хороший рейтинг имеет также пакет optimist, но автором он объявлен устаревшим, а на его место он рекомендует им же написанный пакет minimist, а также советует посмотреть yargs и nomnom. В качестве преемника optimist также позиционируется пакет yargs. Авторы объявленного устаревшим nomnom рекомендуют commander.
Таким образом в первую очередь нужно рассмотреть пакеты commander, minimist и yargs. Вероятно есть смысл также обратить внимание на пакеты meow и nopt, но не в этот раз.
commander
Научиться использовать пакет commander
несложно. Автор предоставил, хоть и не всегда ясную, но всё же неплохую документацию. Чтобы разобраться, как использовать этот пакет, нужно было как следует поэкспериментировать. Ниже я опишу основные моменты этого пакета.
Итак, после того как мы загрузили пакет:
const commander = require('commander')
Мы можем, вызывая последовательно или раздельно его функции, настроить его на обработку опций командной строки. При этом пакет обеспечивает:
- короткие опции, например,
-s
; - длинные опции, например,
--source
; - альтернативные названия опций, например,
--source
и-s
; - дополнительные параметры;
- значения по-умолчанию для дополнительных параметров;
- обработчики для дополнительных параметров;
- субкоманды, например,
install package
; - автоматическое формирование подсказки;
- настройку подстказки.
Короткие опции объявляются так:
commander
.option('-a', 'option a')
Первый аргумент функции option
задаёт формат опции, а второй даёт ей словесное описание. Доступ к опции -a
в коде программы осуществляется через соответствующее свойство commander
:
if (commander.a) {
console.log(commander.a)
}
Пример для длинной опции:
commander
.option('--camel-case-option', 'camel case option')
При этом в коде доступ к опции будет происходить по имени camelCaseOption
.
Возможно задание для опций параметров как обязательных, так необязательных:
commander
.option('-s, --source <path>', 'source file')
.option('-l, --list [items]', 'value list', toArray, [])
Во втором случае, параметр у опции list необязателен, для него назначены функция-обработчик и значение по-умолчанию.
Параметры опций могут обрабатываться также с помощью регулярных выражений, например:
commander
.option('--size [size]', 'size', /^(large|medium|small)$/i)
Субкоманда подразумевает, что для неё пишется отдельный модуль. При этом, если основная программа называется program
, а субкоманда command
, то модуль субкоманды должен называться program-command
. Опции, переданные после субкоманды передаются модулю команды.
commander
.command('search <first> [other...]', 'search with query')
.alias('s')
Для автоматической подсказки можно указать версию программы:
commander.version('0.2.0')
Подсказка может быть сопровождена дополнительными действия, например, дополнена нестандартными текстом. Для этого нужно обрабатывать событие --help
.
commander.on('--help', () => {
console.log(' Examples:')
console.log('')
console.log(' node commander.js')
console.log(' node commander.js --help')
console.log(' node commander.js -h')
...
console.log(' node commander.js --size large')
console.log(' node commander.js search a b c')
console.log(' node commander.js -abc')
})
Завершается настройка вызовом функции parse
с параметром process.argv
:
commander.parse(process.argv)
minimist
Автор пакета minimist предоставил весьма минималистичную документацию. Но всё равно попробуем разобраться.
После того как мы загрузили пакет, подключим и воспользуемся им:
const minimist = require('minimist')
const args = minimist(process.argv.slice(2))
console.dir(args)
Этот незамысловатый код позволит нам начать работать с этим пакетом. Поэкспериментируем:
node minimist.js
{ _: [] }
Что мы здесь видим? Набор разобранных опций организуется в объект. Свойство с именем _
содержит список параметров, не связанных с опциями. Например:
node minimist.js a b c
{ _: [ 'a', 'b', 'c' ] }
Продолжим эксперимент:
node minimist.js --help
{ _: [], help: true }
Как видим, minimist не предоставляет автоматического отображения подсказки, а просто определяет наличие данной опции.
Поэкспериментируем ещё:
node minimist.js -abc
{ _: [], a: true, b: true, c: true }
Всё верно. Посмотрим ещё:
node minimist.js --camel-case-option
{ _: [], 'camel-case-option': true }
В отличие от minimist никаких преобразований.
Опция с параметром:
node minimist.js --source path
{ _: [], source: 'path' }
Со знаком равно тоже работает:
node minimist.js --source=path
{ _: [], source: 'path' }
Поддерживается специальный режим передачи опций с использванием --
:
node minimist.js -h -- --size=large
{ _: [ '--size=large' ], h: true }
Аргументы, следующие за --
не обрабатываются и просто помещаются в свойство _
.
Вот в общем-то и всё, что есть в базе. Посмотрим, какие возможности настройки обработки опций предлагает нам minimist.
Для настройки обработки аргументов командной строки мы должны передать парсеру второй параметр с нашими настройками. Рассмотрим на примерах:
const minimist = require('minimist')
const args = minimist(process.argv.slice(2), {
string: ['size'],
boolean: true,
alias: {'help': 'h'},
default: {'help': true},
unknown: (arg) => {
console.error('Unknown option: ', arg)
return false
}
})
console.dir(args)
node minimist-with-settings.js --help
{ _: [], help: true, h: true }
node minimist-with-settings.js -h
{ _: [], h: true, help: true }
Мы задали для опции --help
синоним -h
. Результат, как видим, идентичен.
Опция boolean
, установленная в true
, говорит о том, что все опции без параметров после знака равно будут иметь булево значение. Например:
node minimist-with-settings.js --no-help
{ _: [], help: false, h: false }
Здесь мы увидели, как обрабатываются булевы опции: префикс no
устанавливает значение опции равным false
.
Но такой пример при этом больше не работает, нужен знак равно:
node minimist-with-settings.js --size large
Unknown option: large
{ _: [], size: '', help: true, h: true }
Здесь же мы увидели обработку неизвестной опции и опции по-умолчанию.
Общий вывод: по сравнению с commander
довольно минималистично, но вполне удобно.
yargs
В отличие от minimist и commander yargs предлагает весьма пространную документацию, доступную по ссылке.
Как обычно начнём с минимального примера:
const yargs = require('yargs')
console.dir(yargs.argv)
node yargs.js
{ _: [], '$0': 'yargs.js' }
Здесь мы видим пустой список необработанных опций, а также имя файла нашей программы.
Рассмотрим пример посложней:
node yargs.js -abc --help --size=large 1 2 3
{ _: [ 1, 2, 3 ],
a: true,
b: true,
c: true,
help: true,
size: 'large',
'$0': 'yargs.js' }
Здесь поинтереснее будет: во-первых, переданные опции восприняты верно; во-вторых, для их обработки мы не написали ни строчки кода.
Но уже здесь видно, что опция --help
без предварительной настройки по предназначению не обрабатывается.
Рассмотрим теперь как использовать yargs в более сложных случаях на следующем примере:
const yargs = require('yargs')
yargs
.usage('Usage: $0 -abc [--list 1,2,3] --size large|meduim|small [--help]')
.version('1.0.0')
.demand(['size'])
.choices('size', ['large', 'medium', 'small'])
.default('list', [], 'List of values')
.describe('list', 'value list')
.array('list')
.help('help')
.alias('help', 'h')
.example('$0 --size=medium')
.epilog('(c) 2016 My Name')
console.dir(yargs.argv)
node yargs.js -h
Получаем:
Usage: yargs.js -abc [--list 1,2,3] --size large|meduim|small [--help]
Options:
--version Show version number [boolean]
--list value list [array] [default: List of values]
--help, -h Show help [boolean]
--size [required] [choices: "large", "medium", "small"]
Examples:
yargs.js --size=medium
(c) 2016 My Name
В этом примере мы задали текст, который будет выводиться с опцией help
. Опции help
мы также указали синоним h
. А ещё указали версию программы, которая будет выводиться с опцией version
.
Опция size
обязательная, более того, для неё задан список допустимых значений.
node yargs.js --size large
{ _: [],
version: false,
help: false,
h: false,
size: 'large',
list: [],
'$0': 'yargs.js' }
Если size
передать значение, не соответствующее ни одному из списка, то получим сообщение об ошибке:
node yargs.js --size=middle
...
Invalid values:
Argument: size, Given: "middle", Choices: "large", "medium", "small"
Для опции list
указано значение по умолчанию. Эта опция также трактуется как массив значений:
node yargs.js --list 1 2 3 --size=large
{ _: [],
version: false,
help: false,
h: false,
list: [ 1, 2, 3 ],
size: 'large',
'$0': 'yargs.js' }
Резюме
Пакеты commander и minimist выделяются минимальным числом зависимостей, в то время как yargs поражает не только числом своих зависимостей, но и числом своих возможностей.
Какой пакет лучше, очевидно, сказать нельзя. По мне, minimist вполне достаточен для простейших случаев, но в сложных ситуациях при его использовании придётся написать много кода обработки опций вручную. В этом случае лучше воспользоваться commander или yargs, на ваш вкус.
Все три рассматриваемые здесь пакета имеют определения типов на TypeScript, что позволяет иметь в Code работающий IntelliSense.
Обновление
Добавил в таблицу ещё три пакета, о которых сообщили в комментариях читатели:
- NPM: argparse | GitHub: nodeca/argparse
- NPM: argentum | GitHub: rumkin/argentum
- NPM: getoptie | GitHub: avz/node-getoptie
Соответственно, обновлённая таблица ниже:
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Комментарии (29)
monolithed
14.09.2016 00:54Эм. а почему в табличке нет argparse, у которого с 330k загрузок сутки?
easimonenko
14.09.2016 01:01+1Не знал об этом пакете. В табличку добавить нетрудно. Постараюсь сделать завтра на свежую голову.
easimonenko
14.09.2016 23:48+1Добавил в раздел "Обновление" (в конце статьи) расширенную таблицу с рекомендованным Вами пакетом.
rumkin
14.09.2016 11:49Пользуясь случаем пропиарю свою библиотеку argentum. В общем больше напоминает minimist, но имеет отличия.
- Умеет приводить кебаб к кэмел кейсу:
--some-option -> {someOption: true}
- Парсит вложенные значения:
--obj.prop=true -> {obj: {prop: true}}
- Приводит true, false и числа к JS-типам (опционально)
--value=1.2 -> {value: 1.2}
- Поддерживает массивы:
--arr[] 1 2 3 -> {arr: [1, 2, 3]}
- Удаляет полученные значения из переданного массива:
var argv = ['--a=1', 'hello']; argentum.parse(argv); argv; // -> ['hello']
Основное назначение консольные утилиты с большим количеством параметров. Плюсы простота, гибкость и унификация интерфейса для пользователя. Так же подходит для создания более сложного парсера.
- Умеет приводить кебаб к кэмел кейсу:
Arepo
14.09.2016 11:56А мне как-то ближе подход стандартного POSIX'ового
getopt(3)
, для себя написал getoptie, который умеет всё то же, что иgetopt(3)
, но помимо этого ещё и берёт на себя проверки конфликтующих опций, несколько вхождений одной опции, и необязательные опции, например
"a:b:(C|D)"
—-a
и-b
обязательные и требуют аргументы, а-C
и-D
конфликтуют между собой и не могут задавать одновременно"ab:[c:d:]"
—-a
обязательный и без аргумента,-b
обязательный с аргументом,-c
и-d
оба необязательные и требуют аргумента"[v*]"
—-v
опциональный и может быть указан много раз, например, это используется для управление уровнем подробности лога
длинные опции не поддерживаются и генерация help'а тоже не поддерживается, т.е. это просто парсер аргументов и ничего больше
vintage
Я противник этих ваших заклинаний вида
-xa -o2 --lala -- haha
, поэтому использую тривиальную функцию, которая разбирает аргументы видаflag1 flag2 key1=value1 key1=value2 key2=value3=value4
и всё.arvitaly
Вероятно, даже такая функция не совсем тривиальна? Допустимые символы, кавычки, апострофы, обратные слеши, регулярные выражения? Покажете?
vintage
Надо бы оформить в виде пакета, да. :-D
bromzh
key1="foo=bar"
распарсит неправильно.Плюс, нет никаких страховок от опечаток: например, лишний пробел до знака
=
(например,foo =bar
) добавит в результат поле с пустой строкой в качестве ключа.vintage
Всё правильно распарсит. Если на входе не предполагается коллекция, то нужно будет список значений сджойнить в строку. А вы где-нибудь вообще видели защиту от таких опечаток?
bromzh
Точно? Я вот хочу, чтобы ключу foo (см. ниже) соответствовала строка "bar=baz": Для сравнения я взял yargs:
Т.е. если понадобится передать знак равно в значения какого-нибудь из аргументов, ваш способ выдаст не тот результат, на который рассчитываешь.
Именно защиту — нет. Однако, используя минусы в аргументах можно однозначно идентифицировать ключ:
vintage
У вас есть два варианта:
Отделять ключ от значения тем же символом, что и параметры между собой — так себе однозначность. Вы просто привыкли к этому кактусу и научились получать от этого удовольствие.
alibertino
Так можно на все ответить. Например, я привык к удобству коммандера, который позволяет создавать суб-команды, вставлять описание, иметь встроенные команды (типа, help), удобные action. И все это из коробки. Конечно, все это излишество, но тогда и ваши ключи тоже излишество, просто передавайте строку, а лучше gzip-ованную, и пользователь и программист пусть сами разбираются.
vintage
Необходимость писать два дефиса перед каждым ключом — так себе удобство.
monolithed
Правда тут есть нюанс с пустыми опциями, но это всего-лишь нюанс…
vintage
Ваш вариант отбрасывает всё, что после второго символа равенства в параметре. Вообще говоря, нет смысла использовать прокси в данном случае.
bromzh
Так себе варианты. А если требуется передать список, который может содержать произвольные символы, в т.ч. и равно? Т.е. такое:
Ваш вариант просто сольёт всё в 1 строку, хотя я ожидаю массив:
{ foo: [ 'bar', 'baz', 'a=b' ] }
Да. И не только я получаю, опции с 1 и 2-мя минусами (короткие и длинные) и знаками равно — вроде как стандарт в мире nix и все его придерживаются. Он довольно гибкий и удобный. И, по-моему, даже на win избавляются от слешей в опциях и используют минусы.
vintage
Первый вариант сольёт, второй — не сольёт.
То, что это стандарт де факто не делает его менее кривым. Я сторонник правильных решений и эволюции стандартов, а не форматирования мозгов под древние костыли.
bromzh
Это до тех пор, пока не захочется писать список так, как вы указали сначала:
В чём заключается кривость стандарта? Да и ваш предложенный вариант намного хуже. Строки с символом = по-прежнему вызывают боль. Если аргумент будет списком произвольных строк, то придётся дублировать ключ:
foo=b foo="c=d"
, что убивает читаемость. Вариант-foo b "c=d"
выглядит менее громоздко (к слову, вместо двух минусов для длинных ключей можно использовать 1, как в java, например).vintage
Очевидно, во втором варианте так писать будет нельзя. Вы что доказать-то пытаетесь?
В минусах, двойных минусах, двойных минусах отключающих обработку двойных минусов, в однобуквенных ключах, в отсутствии группировки ключа и соответствующего ему его значения.
В "стандартном" варианте его тоже придётся дублировать.
Это нигде не поддерживается.
Ага, давайте запутаем всех окончательно. Есть негласное соглашение — однобуквенные ключи предварять одним дефисом, а словесные — двумя.
JVM — вообще чудесный пример костылей:
bromzh
del
bromzh
Нет, просто вы критикуете стандартный подход, а я — ваш. Конечно, ваш подход хорош для простых случаев, когда всяких передаваемых опций мало и они тривиальны. Однако, с ростом числа и типа опций ваш подход вызывает лишь проблемы.
Очень многие консольные программы используют такой синтаксис:
Команды и их аргументы идут без минусов. А минус(ы) перед аргументом указывают, что это лишь дополнение к команде. И это очень удобно по крайней мере по 2-м причинам: Во-первых, сразу можно отличить где сама команда, а где доп. флаги к ней. Во-вторых, позволяет вставлять опции в любое место при наборе команды:
Тут сразу видно что где: save и only — не названия пакетов, а опции к основной команде install, prod — значение опции only, а foo и bar — не название опций, а имена пакетов.
Но ок, допустим, я хочу реализовать некую альтернативу npm и использовать ваш метод передачи аргументов. Сравним:
Пока вроде нормально, хотя можно запутаться, что есть команда, а что дополнение к ней. А знак равно в качестве разделителя в списках выглядит странно, что, опять же, затрудняет восприятие. Пробелы или запятые выглядят привычнее для многих, да и визуально они лучше разбивают строку, в отличие от =.
А теперь пример посложнее. В аргументах команды install можно указывать версию, и там используется знак равно. Очень хочется увидеть, как тогда будет выглядеть аналог такой команды:
vintage
То есть отошли в своё время от стандарта, посчитав, что совмещать несколько команд в одной программе проще, чем делать по отдельной программе на каждое действие, как было принято в *nix.
i — это install или info или init?
S, E и G — это что за параметры? Чем они отличаются от s, e и g?
npm -l
ничего про них не говорит, гугл тоже молчит.Очевидно, мы будем использовать второй вариант:
vintage
Но формат версий можно сделать менее кривым, что позволит использовать и первый вариант:
При типичном использовании будет вполне компактно:
bromzh
В unix-way — это когда 1 программа на 1 задачу.
Вот есть гит. Гит выполняет 1 задачу — управляет репозиторием. Для каждой его команды надо было делать отдельную программу? И для каждой npm-команды надо было отдельную программу писать?
Хотя вы вполне можете это сделать через alias'ы в баше (или его аналогах).
Ну что-то совсем несерьёзно. Во-первых, мы говорим не про имена опций и что они означают, а про их формат. А во-вторых, научитесь пользоваться хелпом. Вроде на js пишете много, а вопросы задаёте, как будто ноду в первый раз видите:
И чем это лучше обычной версии (ну кроме того, что её придумали вы?)
Отлично! Вместо того, чтобы улучшить свой код и сделать его более универсальным, и реализовать наконец-таки экран знака равно, вы предлагаете заменить semver на свой никому неизвестный формат.
Чем теперь semver не угодил? Фатальный недостаток?
vintage
Именно так и было 10 лет назад
Так тут и проблема в этом коротком формате.
Ага, не догадался, что
npm install --help
илиnpm help install
— совершенно разные вещи, вот дурак, наверно. :-)Тут понятно за что отвечает каждый параметр, хотя способ указания версии всё портит, да.
Это не semver, а кривой велосипед от разраработчиков NPM. Я предложил более вменяемый вариант, использующий математическую нотацию диапазонов.