Yamato DaiwaE(CMA)S(cript) extensions

ConsoleCommandsParser class

The class is intended to be used for parsing the argument vectors of console commands with validation and transforming to specific TypeScript types constrained to an object. Also, the generation of help (reference) text about console commands functionality is available.

Demo

The following example will be explained in detail.

Minimal Theory

There are no official terminology and standards concerning the console command anatomy — just a more or less established convention. At last, the process.argv (shorthand for the "arguments vector," theconvention inherited from C++ language is an array of strings. What to do with them — the application author and/or his customers decide.

In fact, the parsing of the console command is the kind of problem of the structured external data parsing where the indexed array of strings is such data, thus the elements order is critical. Node.js does not provide any functionality for the parsing and validation of such arrays, and in addition, not every third-party library specialized on console commands parsing can do it type-safely (as far as possible).

Conventional Terminology

Generally, a console command presents itself as a sequence of string values separated by spaces:

On the Webpack utility example, such a command could be like:

Command
In essence, it is the application name — either full or abbreviated. On the example of the console interface of the Angular framework, the ng in the command. However, usually the command is even with the full name of the utility or close to it, for example, webpack, gulp, lerna.
Option / Option key
Begins with a double n-dash (like --mode in the above example).
Parameter
The value of the option. For the above example, the value development is the parameter of the --mode option.
  • If the option has no parameter, it is considered a boolean option with a true value. Accordingly, when this option has not been specified, it is equivalent to false for this option.
  • The option could have an abbreviation consisting of a single n-dash and a letter (for example, -d). Noteworthy, in the Webpack utility, although there is the abbreviated option -m, it is not the shorthand for the --mode. As it appears, though the abbreviations are inputted quickly, to memorize them frequent usage is required.
  • Arguments could include spaces; however, in this case, they must be wrapped in quotes.

What is the build in the above example? Good question, and it deserves to be considered in a separate section.

Command Phrase

The command phase is the non-option first argument of the console command, referring to the certain functionality of the console application. For example, in yda build --mode DEVELOPMENT, the build is the command phrase, but because it must not consist of one word, it is being called by phrase. If the command phrase included multiple words, besides wrapping in quotes, it also possible to use sentence fused writing methods such as camel case.

The command phrase could be explicit or implicit (the command phrase by default). For example, in webpack build --mode development, the default command phrase build could be omitted (webpack --mode development).

Discriminated Unions in TypeScript

To use the ConsoleCommandsParser, it is required to understand the concept of discriminated unions in TypeScript language. In essence, discriminated unions mean that some type could be one of multiple object subtypes with certain properties set, herewith one of these properties identifies the specific subtype. So, the discriminated union is the generalizing of the multiple specific object types, such as the "passenger car" could be "sedan," "hatchback," "minivan," and others, herewith this set must be finite and there is the "type" column in the car documentation, thanks to which the car type could be uniquely known.

Let us consider it on the example related to ConsoleCommandsParser. Assume that the console application developed by us has command phrases build, pack, deploy, help. For now, it is negligible which functionality of the console applications these command phrases represent, but if for the users the short command phrases are desired while for the developers the meaningful command phrases are desired, we can store the short ones to the values of the enumeration:

Let us create the object for each command phrase which will include the phrase property with one of the values of the enumeration CommandPhrases defined above, and also the options actual for the corresponding command phrases. Let all command phrases except the last one have the options.

We could not know in advance which exactly command phrase will be inputted by user, but with correctly defined validation rules (it will be explained how to) it will be one of the command phrases defined above:

The parse method of ConsoleCommandsParser will return the object of the defined above SupportedCommandsAndParametersCombinations with 2 additional properties: NodeJS_InterpreterAbsolutePath and executableFileAbsolutePath — they are the first two elements of process.argv array. In fact, the ParsedCommand generic in ParsedCommand<SupportedCommandsAndParametersCombinations> tells TypeScript that to one of the subtypes of SupportedCommandsAndParametersCombinations union these properties will be added:

But, how will we know which command phrase exactly has been inputted by the user of the application? Because all subtypes of SupportedCommandsAndParametersCombinations discriminated union have the identifier-like phrase property which is unique for each subtype of this union, using conditional statements (for this case switch/case is just right) we can know the specific subtype:

As a result, in each case-block we can access the properties of parsedConsoleCommand corresponding to the current command phrase, and if we try to access the property corresponding to another command phrase, TypeScript will notice it as an error. Admittedly, there are many optional properties in this example, so a non-undefined check will be required before using these properties.

Creating of Console Line Interface — Stepwise Guidance

Step 1. Defining of Command Phrases Set

Decide, which command phrases will be available in your application.

  • Maybe, you will need only one command phrase. If your application is narrow specialized, it is normal.
  • For now, it is better to define only such command phrases which are clear for you. You can add more command phrases later.

Store all your command phrases to the enumeration. The enumeration keys not necessarily must match with the sequences of characters which the user will input to the console: let the keys be longer, but understandable for programmers without documentation. And if you, like the developers of most console applications, want to make the inputted command phrases short, then store them to the values of the enumeration elements. Fortunately, TypeScript allows to define string-type enumerations, which is impossible or limited in many other programming languages.

As you can see, in the example above the compound nouns without verbs are being using if the name begins from verb herewith not belong to function or method, it creates the confusion. However, it is the stylistic requirement, with which you may disagree and set your own.

It is recommended to write this code in a separate file, for example ApplicationConsoleLineInterface.ts. Also, for the clear definition of the context, we recommend to wrap all the content of this file by namespace, for example ApplicationConsoleLineInterface. Although some developers are against the usage of namespaces (in TypeScript), such a negative attitude to namespaces is usually associated with outdated ways of splitting the code into modules, and such usage, as in our example (contextual container for types and constants) not carries any problems.

Step 2. Determination of Options for Command Phrases

Decide which options you will support for each command phrase, as well as their types, requiredness and default values (if any) of each option.

  • Again, to get the feedback from your code faster, for initial phase you can limit yourself to the minimal set of options which are most thoughtful.
  • Some command phrases could have no options at all — it is normal, and if you will feel the need in the future, you can always add them. In our example, the referenceGenerating command phrase has no options.

Now, for each command phrase define the object type with the following properties:

phrase
Must contain the element of the commanded phrases enumeration (CommandPhrases in above example) corresponding to the same command phrase as the type you are defining. This property plays the role of the identifier, with which it will be necessary to determine which exactly command phrase the user has inputted. As the type of this property it is required to specify the certain element of enumeration (for example, CommandPhrases.projectBuilding), not the enumeration itself (from the viewpoint of discriminated unions in TypeScript it is not senselessly).
Command options

All options for the current command phrase. The keys of must not be the same as ones which the user will input to the console, so let their names be clear to the programmers without documentation. The type of each of these properties must be one of the following supported types:

  • string
  • number
  • boolean
  • The object constrained to ParsedJSON from the core package (@yamato-daiwa/es-extensions)

If the option is non-required herewith no default value planned, place the [+Term--YDID question mark] before colon (the key and value's type separator).

Finally, declare the union of all the object types defined above for each command phrase (in the below example, it is the SupportedCommandsAndParametersCombinations).

  • Again, drawing your attention which stylistic requirements could be concerning the naming of types. The name of each type corresponding to specific command phrase does not including the verbs and ends with ConsoleCommand.
  • The usage of Readonly utility type is not required, but recommended, because using it we are explicitly expressing that the properties of the objects types defined by us are not indented to be changed during program execution.

Step 3. Determination of Console Application Specification

Based on the console command specification, the parsing and validation of the inputted console command will be performed, and if necessary — the generation of the help about usage of this console application.

Define the constant of the ConsoleCommandsParser.CommandLineInterfaceSpecification type — it is a multi-level object, in which should be contained the specification of all the command phrases including the options of each of them, as well as the data for the help generation. If you have followed the advice to wrap the code in the namespace such as ApplicationConsoleLineInterface, then the long name of the constant such as consoleCommandLineInterfaceSpecification is not required: we have declared the context ApplicationConsoleLineInterface, so the constant could be named just specification.

  1. Specify the applicationName property with the name of your application. If is not mandatory to be even with the inputted to the console application name, and if it consists of multiple words, then it is possible to input them with the space separation. This value will be used in the generation of the help about the usage of the console application.
  2. Unlike applicationName, property applicationDescription is optional, however it is strongly recommended to fill it with a short description so the generated help will be full-fledged.
  3. The property commandPhrases must contain the specifications of all the command phrases. This specification is the associative array-like object which keys must match with the command phrases. The safest way to guarantee this matching is refer the keys of commandPhrases to the corresponding elements of the enumeration CommandPhrases using the bracket notation (for example, [CommandPhrases.packingOfBuild]). About the values of this associative array,
    • If corresponding command phrase is the default one, specify the true value to isDefault boolean property. Because the default command phrase could be only one, isDefault: true could be specified maximally to one command phrase, otherwise the exception will be thrown once application start.
    • Similar to explained above applicationDescription property, for the command phrases specifications there is the description property, which is optional but strongly recommend to be filled specified for the generating of high quality reference. In the example below, the description has been omitted only for help (CommandPhrases.referenceGenerating) command phrase.
    • In the command phrase has options, its' specification must be specified in the options property which is also the associative array-like object.

Defining the Command Phrases' Options

A majority of time of current step will be taken by the defining of the options of the command phrases if there are many of them. However, it is nothing difficult: it is required specify the options (as inputted) via object keys, and their specifications such as type, requirement, etc. via object-type values with below properties:

  1. Specify option type by the element of ConsoleCommandsParser.ParametersTypes enumeration. Currently, the string, number, boolean, and objects (must be inputted with JSON5 format) types are supported.
  2. If the option has default value, specify defaultValue property with the value of the corresponding type. Otherwise, you have to specify the required property with the boolean value (of course, with true if the option is required). Herewith, no defaultValue could be specified (by the TypeScript types definitions) for the optional boolean option because the optional boolean option omitted during command inputting is equivalent to explicitly specified false value.
  3. If you are writing the high quality code, but for the inputting speed the names of options are too short to be understood without documentation, define the newName property with meaningful name for the code maintainers.
  4. If you want define the single letter abbreviation for the option, then define the shortcut property. The n-dash, which the user will be required to specify when input the abbreviated option, here, when defining of the specification, could be safely omitted.

All other properties are type-dependent. Let us consider them.

Special Properties of String-type Options

Currently there is only one such propertyallowedAlternatives. If you want the string option to be able to accept the values among limited set, specify them as an array to the allowedAlternatives property.

Special Properties of Number-type Options

The required property for this type of options is numbersSet. It must be defined with the element of RawObjectDataProcessor.NumbersSets enumeration adopted from the core package of library:

  • naturalNumber
  • nonNegativeInteger
  • negativeInteger
  • negativeIntegerOrZero
  • anyInteger
  • positiveDecimalFraction
  • negativeDecimalFraction
  • decimalFractionOfAnySign
  • anyRealNumber

Optionally the minimalValue and maximalValue properties could be specified.

Special Properties of Object-type Options

Currently, there is only one such propertyvalidValueSpecification, but it is the required one. Using this, it is required to define the validation rules for the object to which the JSON5 string will be transformed.

This property has type RawObjectDataProcessor.PropertiesSpecification , adopted from the core package. This object-type property is pretty similar to the options specification, but here is the API of RawObjectDataProcessor class is being used.

Step 4. Creating of the Logic of the Console Commands Parsing

The defining of the console command application specification has been finished; now it can be used for the parsing of the inputted console command. To parse the console command, it is required to call the parse static method of ConsoleCommandsParser class:

  • The console application specification defined at previous step is the required parameter.
  • If to omit the second parameter, the arguments vector will be taken from process.argv.
  • It is required to specify the generic parameter with SupportedCommandsAndParametersCombinations defined at the second step.

Now, we need to determine which exactly command phrase was inputted, and also safely (without any type errors in TypeScript) access its options (unless the optional options wihtout default values will require the check for undefined before using them). We can do this with the help of switch/case statement:

Step 5. Launching of the Application by Name

Currently, the application already could be launched by the file path with ts-node, for example:

Although many of you will not be satisfied by this (because want to launch the application by the command, it means the application name according confusing established terminology), let us consider in short when such variant fits to requirements thus completed previous steps will be enough. First of all this are the cases when the developed console interface planned to use exclusively for single project wihtout publication to npm or by other methods. In particular:

Server Applications
Commonly, such application has only one command phrase for the server starting. Here, the HTTP port, logging level etc. are usually been specified via options.
Automation scripts
Usually it is the generating, copying or modifying the files, herewith the target is as specific as the general utilities such Gulp does not fit.

Well, but how to make possible the invocation of the utility by its name like gulp, webpack etc.?

First of all, because the Node.js runtime does not support the TypeScript, it is required to provide the transpiling of the source code to JavaScript. It could be done by the console line interface of the TypeScript package, or the utilities as Webpack mentioned above (one ore more plugins specializing of TypeScript will require).

But before run the transpiling, it is required to export from the entry point the function or class, one of which methods will launch the application's logic. "Export from the entry point" is sounds weird, because normally the entry point has many imports, but exports nothing. The reason is, in the case of the Node.js console utilities, the entry point is being executed indirectly, via executable file. Here is the example of the entry point which exports the executeApplication function:

Once transpiling done, create one more JavaScript file (for example, Executable.js) manually with the content like the following one:

Here the first line contents the shebang specifying that this file must be executed by Node.js. Then, we need to import the function launching the application (executeApplication in above example) and call it.

Question

Why not to make this file the entry point? It seems like the JavaScript file except the shebang line, right?

Answer
Well, it is possible. But, it is not recommended to store in these files more complicated logic that the calling on one function or method. However, because this is the recommendation, some developers following it (for example, the developers of Gulp), and some developers ignores it (for example, the developers of the Webpack; checked on spring of 2024).

Now, it is required to reference to above executable file from the package.json file of your project. To make your utility callable by name, the bin field must to be filled with the associative array-like object, where the keys are the commands, and values are the relative paths to executable files (".js" extension could be omitted):

More details about bin field you can found in the npm official documentation.

To install your utility to another projects, it is required to fill certain other fields of package.json, such as name and version. More fields need to be filled, if you are going to publish your utility by npm. On the npm official website, there is the brief manual about this.