Класс ConsoleCommandsParser
- Демо
- Минимальные теоретические знания
- Пошаговое руководство по созданию консольного интерфейса
Класс, предназначенный для разбора (парсинга) векторов аргументов консольных команд с валидацией и преобразованием их к объектным TypeScript-типам. Возможно также сгенерировать справочный текст для всех поддерживаемых команд.
Демо
Нижеследующей пример пример будет полностью разобран.
import { RawObjectDataProcessor } from "@yamato-daiwa/es-extensions";
import { ConsoleCommandsParser } from "@yamato-daiwa/es-extensions-nodejs";
namespace ApplicationConsoleLineInterface {
export enum CommandPhrases {
projectBuilding = "build",
packingOfBuild = "pack",
projectDeploying = "deploy",
referenceGenerating = "help"
}
export type SupportedCommandsAndParametersCombinations =
ProjectBuildingConsoleCommand |
PackingOfBuildConsoleCommand |
ProjectDeployingConsoleCommand |
ReferenceGeneratingConsoleCommand;
export type ProjectBuildingConsoleCommand = Readonly<{
phrase: CommandPhrases.projectBuilding;
requiredStringOption: string;
optionalStringOption?: string;
}>;
export type PackingOfBuildConsoleCommand = Readonly<{
phrase: CommandPhrases.packingOfBuild;
enumerationLikeStringOption: "FOO" | "BAR" | "BAZ";
numericOption?: number;
limitedNumericOption?: number;
}>;
export type ProjectDeployingConsoleCommand = Readonly<{
phrase: CommandPhrases.projectDeploying;
booleanOption: boolean;
JSON5_Option?: Readonly<{ foo: string; bar?: number; }>;
}>;
export type ReferenceGeneratingConsoleCommand = Readonly<{
phrase: CommandPhrases.referenceGenerating;
}>;
export const specification: ConsoleCommandsParser.CommandLineInterfaceSpecification = {
applicationName: "Example task manager",
applicationDescription: "Executes various tasks.",
commandPhrases: {
[CommandPhrases.projectBuilding]: {
isDefault: true,
description: "Builds the project for specified mode.",
options: {
requiredStringOption: {
description: "Example required string option",
type: ConsoleCommandsParser.ParametersTypes.string,
required: true,
shortcut: "a"
},
optionalStringOption: {
description: "Example optional string option",
type: ConsoleCommandsParser.ParametersTypes.string,
required: false
}
}
},
[CommandPhrases.packingOfBuild]: {
description: "Create the deployable pack of the project",
options: {
enumerationLikeStringOption: {
description: "Example enumeration like string option",
type: ConsoleCommandsParser.ParametersTypes.string,
defaultValue: "FOO",
allowedAlternatives: [
"FOO",
"BAR",
"BAZ"
]
},
numericOption: {
description: "Example numeric option",
type: ConsoleCommandsParser.ParametersTypes.number,
numbersSet: RawObjectDataProcessor.NumbersSets.naturalNumber,
required: false
},
limitedNumericOption: {
description: "Example numeric option with fixed minimal and maximal value",
type: ConsoleCommandsParser.ParametersTypes.number,
numbersSet: RawObjectDataProcessor.NumbersSets.anyInteger,
required: false,
minimalValue: -10,
maximalValue: 10
}
}
},
[CommandPhrases.projectDeploying]: {
description: "Deploys the project.",
options: {
booleanOption: {
description: "Example boolean option",
type: ConsoleCommandsParser.ParametersTypes.boolean,
shortcut: "b"
},
JSON5_Option: {
description: "Example JSON5 option",
type: ConsoleCommandsParser.ParametersTypes.JSON5,
shortcut: "j",
required: false,
validValueSpecification: {
foo: {
type: RawObjectDataProcessor.ValuesTypesIDs.string,
required: true,
minimalCharactersCount: 1
},
bar: {
type: RawObjectDataProcessor.ValuesTypesIDs.number,
numbersSet: RawObjectDataProcessor.NumbersSets.anyInteger,
required: false,
minimalValue: 1
}
}
}
}
},
[CommandPhrases.referenceGenerating]: {}
}
};
}
Минимальные теоретические знания
В отношении анатомии консольных команд официальная терминология и стандарты
отсутствуют — имеются лишь более-менее устоявшиеся соглашения.
В сухом остатке, process.argv
(сокращение от «arguments vector»
— «вектор аргументов»,
унаследованная от языка C++ концепция) — всего лишь массив строк, а что с этими строками
делать — решает разработчик приложения и/или его заказчики.
По сути, разбор консольных команд — это частный случай задачи анализа произвольных внешних структурированных данных, в котором такими данными будет индексный массив строк, а значит последовательность элементов играет ключевое значение. Node.js не предоставляет никакой функциональности для разбора таких массивов и их валидации, а сделать это безопасно (насколько возможно) с точки зрения TypeScript-типизации может ещё и не каждая сторонняя библиотека для работы с консольными командами.
Соглашения по терминологии
В общем виде консольная команда представляет собой последовательность строчных значений, разделённых пробелом:
команда аргумент1 аргумент2 ... аргументN
На примере утилиты Webpack, конкретная команда может иметь вид:
webpack build --mode development
- Команда
- По пути, это имя приложения — полное или сокращённое.
Например, у консольного интерфейса фреймворка Angular это
ng
, но чаще команды либо совпадают с полным именем приложения, либо близки к нему, напримерwebpack
,gulp
,lerna
. - Опция (option) / Ключ опции (option key)
- Начинается с двойного дефиса (как
--mode
в примере выше). - Параметр
- Значение опции.
Например,
development
является параметром опции--mode
.
- Если опция не имеет параметра, то она
рассматривается как опция булевского типа со
значением
true
. Соответственно, когда этой опции не указано, это равносильно значениюfalse
. - Опция может иметь сокращение, состоящее из одного дефиса и одной буквы (например,
-d
). Примечательно, но для приведённого выше в качестве примераwebpack
-а сокращение-m
имеется, только одно не является сокращением по отношению к--mode
. Как видно, сокращения хотя и быстры для ввода, но требуют повседневного использования для запоминания. - Аргументы могут иметь пробелы, но при этом они должны быть обёрнуты в кавычки.
--опция1=параметр1 --опция2=параметр2 ... --опцияN=параметрN
.
На данный момент такой синтаксис не поддерживается
ConsoleCommandsParser, однако если эта поддержка окажется востребована,
то вероятно её добавление в будущих версиях.
Чем же тогда является build
в примере выше?
Хороший вопрос и он заслуживает отдельного подраздела.
Термин «командная фраза» («command phrase»)
Командная фраза («command phrase») — первый аргумент
консольной команды, не являющиеся опцией и ссылающийся
на конкретную функциональность консольного приложения.
Например, в yda build --mode DEVELOPMENT
командной фразой
является build
, однако ввиду того, что она не обязана
состоять из одного слова, она и называется «фразой».
В случаях, когда командная фраза состоит из нескольких слов, помимо оборачивания её в кавычки
можно использовать слитные способы записи нескольких слов, такие как верблюжий регистр.
Командная фраза может быть явной или неявной (командной фразой по умолчанию).
Например, в webpack build --mode development
командная фраза может
build
является командной фразой по умолчанию, а потому может
быть опущена (webpack --mode development
).
Дискриминантное объединение в TypeScript
Для использования ConsoleCommandsParser необходимо понимать концепцию дискриминантного объединения в языке TypeScript. Суть её в том, что некоторый тип (данных) может быть одним из нескольких объектных типов с определённым набором свойств, одно из которых отвечает за идентификацию конкретного подтипа. Таким образом, дискриминантное объединения — это обобщение для нескольких конкретных объектных типов, подобно тому, как под «легковым автомобилем» может подразумеваться «седан», «хэтчбэк», «минивэн» и другие, при этом набор типов в классификации должен быть конечным, а в документации к автомобилю имеется графа «тип», благодаря которой можно однозначно сказать, к какому типу принадлежит конкретный автомобиль.
Рассмотрим это на примере, имеющему отношение ConsoleCommandsParser.
Допустим, разрабатываемая консольная утилита имеет командные фразы
build
, pack
,
deploy
и help
.
Какой именно функциональности приложения они соответствуют — сейчас неважно, однако если для
пользователей консольного приложения желательны короткие идентификаторы, а для разработчиков — содержательные,
то можно короткие поместить в значение перечисления:
enum CommandPhrases {
projectBuilding = "build",
packingOfBuild = "pack",
projectDeploying = "deploy",
referenceGenerating = "referenceGenerating"
}
Создадим объект для каждой командной фразы, который будет
включать в себя свойство phrase
с одним из значений определённого
выше перечисления CommandPhrases
, а так же опции,
актуальные для соответствующих командных фраз.
Пускай все командные фразы, кроме последней, будут
иметь опции.
type ProjectBuildingConsoleCommand = {
phrase: CommandPhrases.projectBuilding;
requiredStringOption: string;
optionalStringOption?: string;
};
type PackingOfBuildConsoleCommand = {
phrase: CommandPhrases.packingOfBuild;
enumerationLikeStringOption: "FOO" | "BAR" | "BAZ";
numericOption?: number;
limitedNumericOption?: number;
};
type ProjectDeployingConsoleCommand = {
phrase: CommandPhrases.projectDeploying;
booleanOption: boolean;
JSON5_Option?: Readonly<{ foo: string; bar?: number; }>;
};
type ReferenceGeneratingConsoleCommand = {
phrase: CommandPhrases.referenceGenerating;
};
Какую именно командную фразу введёт пользователь — мы заранее знать не можем, однако при правильно описанных правилах валидации (мы разберём, как это делать) это будет одна из приведённых выше командных фраз с соответствующим ей набором опций:
type SupportedCommandsAndParametersCombinations =
ProjectBuildingConsoleCommand |
PackingOfBuildConsoleCommand |
ProjectDeployingConsoleCommand |
ReferenceGeneratingConsoleCommand;
Метод parse
класса
ConsoleCommandsParser
вернёт объект определённого выше
типа SupportedCommandsAndParametersCombinations
с
двумя дополнительными свойствами:
NodeJS_InterpreterAbsolutePath
и
executableFileAbsolutePath
— это первые
2 элемента массива
process.argv
.
По сути, TypeScript-обобщение ParsedCommand
в
ParsedCommand<SupportedCommandsAndParametersCombinations>
сообщает TypeScript-y, что к одному из подтипов
объединения SupportedCommandsAndParametersCombinations
будут добавлены эти два свойства:
const parsedConsoleCommand: ConsoleCommandsParser.
ParsedCommand<SupportedCommandsAndParametersCombinations> =
ConsoleCommandsParser.parse(ApplicationConsoleLineInterface.specification);
Но как мы узнаем, какую именно командную фразу ввёл пользователь приложения?
Поскольку у всех подтипов дискриминантного объединения
SupportedCommandsAndParametersCombinations
имеется играющее роль идентификатора
свойство phrase
с уникальными
в пределах этого объединения значением, то с помощью
условных конструкций (в данном случае идеально подходит
switch/case
) мы можем определить конкретный подтип:
switch (parsedConsoleCommand.phrase) {
case ApplicationConsoleLineInterface.CommandPhrases.projectBuilding: {
console.log("Build project");
console.log(parsedConsoleCommand.requiredStringOption);
console.log(parsedConsoleCommand.optionalStringOption);
break;
}
case ApplicationConsoleLineInterface.CommandPhrases.packingOfBuild: {
console.log("Pack build");
console.log(parsedConsoleCommand.enumerationLikeStringOption);
console.log(parsedConsoleCommand.numericOption);
console.log(parsedConsoleCommand.limitedNumericOption);
break;
}
case ApplicationConsoleLineInterface.CommandPhrases.projectDeploying: {
console.log("Deploy project")
console.log(parsedConsoleCommand.phrase);
console.log(parsedConsoleCommand.booleanOption);
console.log(parsedConsoleCommand.JSON5_Option);
break;
}
case ApplicationConsoleLineInterface.CommandPhrases.referenceGenerating: {
console.log(ConsoleCommandsParser.generateFullHelpReference(ApplicationConsoleLineInterface.specification));
}
}
В результате, внутри каждого case
-блока мы можем обращаться к
свойствам parsedConsoleCommand
,
соответствующим текущей командной фразе,
а если попытаться вызывать свойство, ссылающееся на опцию другой командной фразы,
TypeScript это заметит в виде ошибки.
Правда, в примере много необязательных свойств, поэтому перед тем, как ими
пользоваться, потребуется проверка на undefined
.
Пошаговое руководство по созданию консольного интерфейса
Шаг 1 — Определение начального набора командных фраз
Решите, какие командные фразы будут доступны в Вашем приложении.
- Возможно, у Вас будет только одна командная фраза. Если Ваше приложение узкоспециализированное, то это вполне нормально.
- В будущем Вы сможете добавить другие командные фразы, потому на этом этапе можете ограничиться теми, назначение которых Вы более-менее осмыслили.
Поместите все Ваши командные фразы в перечисление. Ключи перечисления не обязательно должны совпадать с теми последовательностями символов, которые пользователь будет вводить в консоль: пускай ключи лучше будут подлиннее, но понятными для программистов без документации, а если Вы, как и разработчики большинства консольных приложений, хотите сделать вводимые командные фразы короткими, то поместите их в значения элементов перечисления. К счастью, TypeScript позволяет определять строчные перечисления, что невозможно или ограниченно во многих других языках программирования.
export enum CommandPhrases {
projectBuilding = "build",
packingOfBuild = "pack",
projectDeploying = "deploy",
referenceGenerating = "help"
}
Как видно, в данном примере в качестве ключей перечисления выбраны словосочетания без глаголов — вместо них используются отглагольные существительные, потому если наименование начинается с глагола, но при этом не принадлежит функции или методу, то это создаёт путаницу. Но это это требования стилистические, с которыми Вы можете не согласиться и установить собственные.
- Слово «build» может быть как глаголом, там и существительным, однако за предлогом «of» не может идти одиночного глагола, поэтому в примере выше «build» - существительное со значением «сборка» («собранный проект»).
- «Building» может быть существительным, глаголом или причастием, однако в значение существительного оно может означать как процесс, так и объект. В примере выше, это слово использовано только в значении процесса, а для объекта использовано слово «build» как существительное.
Рекомендуется писать этот код в отдельном файле, например
ApplicationConsoleLineInterface.ts.
Кроме того, для чёткого определения контекста рекомендуем обернуть всё содержимое этого файла в
пространство имён, например ApplicationConsoleLineInterface
.
Хотя некоторые разработчики выступают против использование пространств имён (в
TypeScript), такое негативное отношение к пространствам имён обычно
связано с устаревшими способами разбиения кода на модули, а такое использование, как в нашем примере
(контекстный контейнер для типов и констант) не несёт
в себе никаких проблем.
import { RawObjectDataProcessor } from "@yamato-daiwa/es-extensions";
import { ConsoleCommandsParser } from "@yamato-daiwa/es-extensions-nodejs";
namespace ApplicationConsoleLineInterface {
export enum CommandPhrases {
projectBuilding = "build",
packingOfBuild = "pack",
projectDeploying = "deploy",
referenceGenerating = "help"
}
}
Шаг 2 — Определение опций для командных фраз
Определитесь, какие опции Вы будете поддерживать для каждой командной фразы, а также их тип, обязательность и значения по умолчанию (если таковые имеются) каждой опции.
- Опять же, для того, чтобы быстрее получить обратную связь от своего кода, на начальном этапе можно ограничиться минимальным набором опций, наиболее осмысленных во время проектирования.
- Некоторые командные фразы могут не иметь опций — это нормально,
а если Вы в будущем почувствуете необходимость в опциях, то всегда сможете их добавить.
В нашем примере опций не будет у командной фразы
referenceGenerating
.
Теперь, для каждой командной фразы определите объектный тип со следующими свойствами:
- phrase
- Должно содержать элемент перечисления командных фраз
(
CommandPhrases
в примере выше), соответствующий той же командной фразе, что и тип, который Вы определяете. Это свойство играет роль идентификатора, с помощью которого предстоит определять, какую именно командную фразу ввёл пользователь приложения. В качестве типа данного свойства нужно указывать не само перечисление (CommandPhrases
в примере выше), а его конкретный элемент (например,CommandPhrases.projectBuilding
) — с точки зрения дискриминантных объединений в TypeScript это не бессмысленно. - Опции командны
Все опции для текущей командной фразы. Ключи не обязательно должны совпадать с теми, которые пользователь будет вводить в консоль, поэтому давайте им такие имена, чтобы их смысл был ясен программистам без документации. Тип каждого из этих свойств должен быть одним из следующих поддерживаемых типов:
string
number
boolean
- Объект на основе ParsedJSON из главного пакета (@yamato-daiwa/es-extensions)
Если же опция необязательная и при этом не планируется значения по умолчанию, то перед двоеточием — разделителем ключа и типа значения — необходимо поставить вопросительный знак.
Наконец, объявите TypeScript-объединение, в котором перечислены все только что
созданные объектные типы для каждой командной фразы
(в примере ниже это SupportedCommandsAndParametersCombinations
).
import { RawObjectDataProcessor } from "@yamato-daiwa/es-extensions";
import { ConsoleCommandsParser } from "@yamato-daiwa/es-extensions-nodejs";
namespace ApplicationConsoleLineInterface {
export enum CommandPhrases {
projectBuilding = "build",
packingOfBuild = "pack",
projectDeploying = "deploy",
referenceGenerating = "help"
}
export type SupportedCommandsAndParametersCombinations =
ProjectBuildingConsoleCommand |
PackingOfBuildConsoleCommand |
ProjectDeployingConsoleCommand |
ReferenceGeneratingConsoleCommand;
export type ProjectBuildingConsoleCommand = Readonly<{
phrase: CommandPhrases.projectBuilding;
requiredStringOption: string;
optionalStringOption?: string;
}>;
export type PackingOfBuildConsoleCommand = Readonly<{
phrase: CommandPhrases.packingOfBuild;
enumerationLikeStringOption: "FOO" | "BAR" | "BAZ";
numericOption?: number;
limitedNumericOption?: number;
}>;
export type ProjectDeployingConsoleCommand = Readonly<{
phrase: CommandPhrases.projectDeploying;
booleanOption: boolean;
JSON5_Option?: Readonly<{ foo: string; bar?: number; }>;
}>;
export type ReferenceGeneratingConsoleCommand = Readonly<{
phrase: CommandPhrases.referenceGenerating;
}>;
}
- Снова обращаем Ваше внимание на то, какие могут быть стилистические требования по именованию типов: имя каждого типа, соответствующего конкретной командной фразе, не содержит глаголов и шаблонно заканчивается на ConsoleCommand.
- Использование утилитарного типа Readonly не обязательно, но рекомендуется, потому что тем самым мы явно выражаем, что свойства определённых нами объектных типов не подлежат изменениям в процессе выполнения программы.
Шаг 3 — Определение спецификации консольного приложения
На основе спецификации консольного приложения будет осуществляться разбор (парсинг) и валидация введённой консольной команды, а при необходимости — генерация справки по использованию этого консольного приложения.
Определите константу типа
ConsoleCommandsParser.CommandLineInterfaceSpecification
—
это многоуровневый объект, в котором должна содержатся спецификация
всех командных фраз включая опции каждой из них,
а также данные для генерации справки.
Если Вы последовали совету обернуть код в пространство имён такое как
ApplicationConsoleLineInterface
,
то длинного имени константе наподобие
consoleCommandLineInterfaceSpecification
не требуется: у нас объявлен контекст
ApplicationConsoleLineInterface, поэтому константу можно назвать просто
specification
.
import { RawObjectDataProcessor } from "@yamato-daiwa/es-extensions";
import { ConsoleCommandsParser } from "@yamato-daiwa/es-extensions-nodejs";
namespace ApplicationConsoleLineInterface {
export enum CommandPhrases {
projectBuilding = "build",
packingOfBuild = "pack",
projectDeploying = "deploy",
referenceGenerating = "help"
}
export type SupportedCommandsAndParametersCombinations =
ProjectBuildingConsoleCommand |
PackingOfBuildConsoleCommand |
ProjectDeployingConsoleCommand |
ReferenceGeneratingConsoleCommand;
export type ProjectBuildingConsoleCommand = Readonly<{
phrase: CommandPhrases.projectBuilding;
requiredStringOption: string;
optionalStringOption?: string;
}>;
export type PackingOfBuildConsoleCommand = Readonly<{
phrase: CommandPhrases.packingOfBuild;
enumerationLikeStringOption: "FOO" | "BAR" | "BAZ";
numericOption?: number;
limitedNumericOption?: number;
}>;
export type ProjectDeployingConsoleCommand = Readonly<{
phrase: CommandPhrases.projectDeploying;
booleanOption: boolean;
JSON5_Option?: Readonly<{ foo: string; bar?: number; }>;
}>;
export type ReferenceGeneratingConsoleCommand = Readonly<{
phrase: CommandPhrases.referenceGenerating;
}>;
export const specification: ConsoleCommandsParser.CommandLineInterfaceSpecification = {
applicationName: "Example task manager",
applicationDescription: "Executes various tasks.",
commandPhrases: {
[CommandPhrases.projectBuilding]: {
isDefault: true,
description: "Builds the project for specified mode.",
options: {
requiredStringOption: {
description: "Example required string option",
type: ConsoleCommandsParser.ParametersTypes.string,
required: true,
shortcut: "a"
},
optionalStringOption: {
description: "Example optional string option",
type: ConsoleCommandsParser.ParametersTypes.string,
required: false
}
}
},
[CommandPhrases.packingOfBuild]: {
description: "Create the deployable pack of the project",
options: {
enumerationLikeStringOption: {
description: "Example enumeration like string option",
type: ConsoleCommandsParser.ParametersTypes.string,
defaultValue: "FOO",
allowedAlternatives: [
"FOO",
"BAR",
"BAZ"
]
},
numericOption: {
description: "Example numeric option",
type: ConsoleCommandsParser.ParametersTypes.number,
numbersSet: RawObjectDataProcessor.NumbersSets.naturalNumber,
required: false
},
limitedNumericOption: {
description: "Example numeric option with fixed minimal and maximal value",
type: ConsoleCommandsParser.ParametersTypes.number,
numbersSet: RawObjectDataProcessor.NumbersSets.anyInteger,
required: false,
minimalValue: -10,
maximalValue: 10
}
}
},
[CommandPhrases.projectDeploying]: {
description: "Deploys the project.",
options: {
booleanOption: {
description: "Example boolean option",
type: ConsoleCommandsParser.ParametersTypes.boolean,
shortcut: "b"
},
JSON5_Option: {
description: "Example JSON5 option",
type: ConsoleCommandsParser.ParametersTypes.JSON5,
shortcut: "j",
required: false,
validValueSpecification: {
foo: {
type: RawObjectDataProcessor.ValuesTypesIDs.string,
required: true,
minimalCharactersCount: 1
},
bar: {
type: RawObjectDataProcessor.ValuesTypesIDs.number,
numbersSet: RawObjectDataProcessor.NumbersSets.anyInteger,
required: false,
minimalValue: 1
}
}
}
}
},
[CommandPhrases.referenceGenerating]: {}
}
};
}
- Свойству
applicationName
укажите имя Вашего приложения. Оно не обязано совпадать с вводимым в консоль именем приложения, а если оно состоит из нескольких слов, то можно ввести их с разделением пробелом. Это значение будет использовано при генерации справки. - В отличие от
applicationName
, свойствоapplicationDescription
— описание приложения — необязательное, тем не менее заполнить его коротким описанием рекомендуется, чтобы сгенерированная справка была полноценной. - Свойство
commandPhrases
должно содержать спецификации каждой командной фразы. Представляет собой объект типа «ассоциативный массив», ключи которого должны совпадать с командными фразами. Надёжнее всего с использованием скобочной записи сослать ключи на нужный элемент перечисленияCommandPhrases
, например[CommandPhrases.packingOfBuild]
. Что касается значений этого ассоциативного массива, то:- Если соответствующая командная фраза является
командной фразой по умолчанию, то укажите
булевскому свойству
isDefault
значениеtrue
. Поскольку командная фраза по умолчанию может быть толькоодна
, тоisDefault: true
должно быть указано максимум у одной командной фразы, иначе выброшено исключение при запуске приложения. - Подобно рассмотренному выше
applicationDescription
, для спецификаций командных фраз предусмотрено свойствоdescription
— описание командной фразы, которое хотя и не обязательно, но настоятельно рекомендуется указать, чтобы сгенерировалась качественная справка. В примере выше описание пропущено лишь у командной фразыhelp
(CommandPhrases.referenceGenerating
). - Если у командной фразы есть опции, то их спецификацию необходимо указать
в свойстве
options
, которое также представляет собой объект типа «ассоциативный массив».
- Если соответствующая командная фраза является
командной фразой по умолчанию, то укажите
булевскому свойству
Определение опций командных фраз
Большую часть времени этого этапа займёт именно определение опций комадных фраз, если таковых много. Сложного, однако, ничего нет: через ключи объекта необходимо перечислить опции такими, какими они будут введены в консоль, а через значения (тоже объектного типа) — настройки этих опций, такие как тип, обязательность и так далее. Итак, для каждой опции:
- Укажите тип опции в виде элемента перечисления
ConsoleCommandsParser.ParametersTypes
. На данный момент поддерживаются строчные, числовые и булевские опции, а так же объектные (должны быть введены в консоль в формате JSON5). - Если опция имеет значение по умолчанию, то укажите значение соответствующего
типа свойству
defaultValue
. В противном случае, необходимо явно указать булевское свойствоrequired
(естественноtrue
, если опция обязательна). При этом для необязательной булевской опции указатьdefaultValue
TypeScript не позволит, поскольку пропуск этого значения при вводе в консоль равносилен явно указанному значению false. - Если Вы пишете качественный код, а смысл короткого (в целях скорости ввода) имени опции, которое
будет введено в консоль, неясен без обращения к документации, то определите свойство
newName
более содержательным именем для программистов, которые будут поддерживать Ваш код. - Если Вы хотите определить для опции однобуквенное сокращение, укажите
соответствующее значение свойству
shortcut
. Дефис, который пользователь должен будет указать при использовании этого сокращения, здесь, при определении, указывать не обязательно.
Все остальные свойства зависят от типа опции. Рассмотрим их.
Специальные свойства строчных опций
На данный момент такое свойство только одино —
allowedAlternatives
.
Если Вы хотите, чтобы строковая опция могла принимать только
значения из ограниченного набора вариантов, то укажите их в виде массива
свойству allowedAlternatives
.
Специальные свойства числовых опций
numbersSet
- Множество чисел, обязательное свойство для данного типа
опций.
Его значением должен быть один из элементов перечисления,
заимствованного из главного пакета библиотеки —
RawObjectDataProcessor.NumbersSets
:naturalNumber
- Натуральное число
nonNegativeInteger
- Неотрицательное целое число
negativeInteger
- Отрицательное целое число
negativeIntegerOrZero
- Отрицательное целое число либо ноль
anyInteger
- Любое целое число
positiveDecimalFraction
- Положительная десятичная дробь
negativeDecimalFraction
- Отрицательная десятичная дробь
decimalFractionOfAnySign
- Любая десятичная дробь
anyRealNumber
- Любое действительное число
minimalValue
- Минимальное значение
maximalValue
- Максимальное значение
Специальные свойства объектных опций
На данный момент такое свойство только одно, но
обязательное — validValueSpecification
.
С помощью него необходимо задать правила валидации объекта, в который будет преобразована
строка формата JSON5.
Это свойство имеет тип
RawObjectDataProcessor.PropertiesSpecification , заимствованный из главного пакета библиотеки и очень похоже на спецификацию опций
командной фразы, только здесь используется API
класса RawObjectDataProcessor.
Шаг 4 — Создание логики разбора консольных команд
На этом определение спецификации консольного приложения завершено; теперь её можно использовать при
разборе (парсинге) введённой консольной команды.
Для этого необходимо у класса ConsoleCommandsParser
вызывать статический метод parse
:
import { ConsoleCommandsParser } from "@yamato-daiwa/es-extensions-nodejs";
import ApplicationConsoleLineInterface from "./ConsoleLineInterface";
const parsedConsoleCommand: ConsoleCommandsParser.
ParsedCommand<ApplicationConsoleLineInterface.SupportedCommandsAndParametersCombinations> =
ConsoleCommandsParser.parse(ApplicationConsoleLineInterface.specification);
- Обязательным параметром этого метода является спецификация консольного приложения, которую Вы определили на предыдущем шаге.
- Если не указывать 2го параметра, то
вектор аргументов будет взят из
process.argv
. - В качестве параметра обобщения необходимо указать
TypeScript-объединение
SupportedCommandsAndParametersCombinations
, которое Вы определили на 2-ом шаге.
Теперь нам нужно определить, какая именно командная фраза была введена, а также безопасно
(без типа any
и ошибок TypeScript)
обратиться к её опциям (разве что в случае необязательными
опциями без значений по умолчанию потребуется проверка на
undefined
, прежде чем ими полноценно пользоваться).
Сделать это можно с помощью switch/case
:
switch (parsedConsoleCommand.phrase) {
case ApplicationConsoleLineInterface.CommandPhrases.projectBuilding: {
console.log("Build project", parsedConsoleCommand);
console.log("requiredStringOption", parsedConsoleCommand.requiredStringOption);
console.log("optionalStringOption", parsedConsoleCommand.optionalStringOption);
break;
}
case ApplicationConsoleLineInterface.CommandPhrases.packingOfBuild: {
console.log("Pack project", parsedConsoleCommand);
console.log("enumerationLikeStringOption", parsedConsoleCommand.enumerationLikeStringOption);
console.log("numericOption", parsedConsoleCommand.numericOption);
console.log("limitedNumericOption", parsedConsoleCommand.limitedNumericOption);
break;
}
case ApplicationConsoleLineInterface.CommandPhrases.projectDeploying: {
console.log("Deploy project", parsedConsoleCommand);
console.log("booleanOption", parsedConsoleCommand.booleanOption);
console.log("JSON5_Option", parsedConsoleCommand.JSON5_Option);
break;
}
case ApplicationConsoleLineInterface.CommandPhrases.referenceGenerating: {
console.log(ConsoleCommandsParser.generateFullHelpReference(ApplicationConsoleLineInterface.specification));
}
}
Шаг 5 — Обеспечение запуска приложения по имени
На данный момент приложение уже можно запускать по пути файла с помощью ts-node, например:
ts-node EntryPoint.test.ts --requiredStringOption test --optionalStringOption sample
Хотя такой вариант многих не устроит (потому что хотят запускать консольное приложение по команде, то есть имени приложения — согласно запутанной, но устоявшейся терминологии), рассмотрим вкратце, когда он подходит и на завершённых предыдущих этапах можно остановиться. Прежде всего это случаи, когда разрабатываемый консольный интерфейс планируется применять исключительно в рамках одного проекта без публикации в npm или другими способами. В частности, это:
- Серверные приложения
- Как правило, в таких приложениях одна командная фраза для запуска сервера, а через опции передаются HTTP-порт, уровень логирования и так далее.
- Скрипты для автоматизации
- Обычно это генерация, копирование или модификация файлов, причём задача насколько специфическая, что средства общего назначения, такие как Gulp, не подходят.
Хорошо, но как же всё-таки сделать возможным вызов утилиты по её имени, подобно gulp, webpack и так далее?
Во-первых, поскольку рантайм Node.js не поддерживает TypeScript, то сначала необходимо обеспечить транспайлинг исходного кода в JavaScript. Сделать это можно как с помощью консольного интерфейса пакета TypeScript, так и с помощью специализированных утилит, таких как тот же самый Webpack ( потребуются один или более плагинов для работы с TypeScript).
Однако перед тем, как запускать транспайлинг, необходимо экспортировать из
точки входа функцию или же класс, вызов одного из
методов которого приведёт к выполнению логики приложения.
«Экспортировать из точки входа» — звучит странно, ведь обычно в точку входа много что импортируется,
но ничего не экспортируется.
Объясняется это тем, что
в случае консольных Node.js-утилит точка входа
выполняется не напрямую, а через через исполняемый файл.
Вот пример точки входа, которая экспортирует функцию
executeApplication
:
import { ConsoleCommandsParser } from "@yamato-daiwa/es-extensions-nodejs";
import ApplicationConsoleLineInterface from "./ConsoleLineInterface";
export function executeApplication(): void {
const parsedConsoleCommand: ConsoleCommandsParser.
ParsedCommand<ApplicationConsoleLineInterface.SupportedCommandsAndParametersCombinations> =
ConsoleCommandsParser.parse(ApplicationConsoleLineInterface.specification);
switch (parsedConsoleCommand.phrase) {
case ApplicationConsoleLineInterface.CommandPhrases.projectBuilding: {
// ...
break;
}
case ApplicationConsoleLineInterface.CommandPhrases.packingOfBuild: {
// ...
break;
}
case ApplicationConsoleLineInterface.CommandPhrases.projectDeploying: {
// ...
break;
}
case ApplicationConsoleLineInterface.CommandPhrases.referenceGenerating: {
console.log(ConsoleCommandsParser.generateFullHelpReference(ApplicationConsoleLineInterface.specification));
}
}
}
Выполнив преобразование исходного кода в JavaScript, создайте ещё один JavaScript-файл (скажем, Executable.js) вручную с содержимым подобным следующему:
#!/usr/bin/env node
require("./EntryPoint").executeApplication();
Здесь первая строка содержит шебанг, указывающий на то, что этот файл необходимо выполнять с помощью
Node.js.
Далее следует импортировать функцию, запускающую приложение
(executeApplication
в примере выше) и вызвать её.
Почему нельзя это файл сделать точкой входа? Это же вроде как обычный JavaScript-файл, если не считать шебанга.
Теперь, на этот исполняемый файл необходимо сослаться из файла
package.json Вашего проекта.
Для того, чтобы утилита была доступна по имени, надо заполнить поле
bin
объектом типа
«ассоциативный массив», ключами которого будут команды, а
значениями — относительные пути к соответствующим
исполняемым файлам (расширение «.js» можно опустить):
{
// ...
"bin": {
"my_app": "Executable"
},
// ...
}
Более подробную информацию о поле «bin»
Вы можете получить в
официальной документации npm.
Для того, чтобы устанавливать Вашу утилиту в другие проекты, нужно заполнить и другие поля
package.json, такие как name
и
version
.
Ещё больше полей потребуется заполнить, если Вы собираетесь публиковать утилиту в
npm.
На официальной сайте npm есть
краткое руководство по данной теме.