@adonisjs/ace
Version:
Commandline apps framework used by AdonisJs
288 lines (287 loc) • 9.33 kB
JavaScript
;
/*
* @adonisjs/ace
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Parser = void 0;
const getopts_1 = __importDefault(require("getopts"));
const Exceptions_1 = require("../Exceptions");
/**
* The job of the parser is to parse the command line values by taking
* the command `args`, `flags` and `globalFlags` into account.
*/
class Parser {
constructor(registeredFlags) {
this.registeredFlags = registeredFlags;
}
/**
* Validate all the flags against the flags registered by the command
* or as global flags and disallow unknown flags.
*/
scanForUnknownFlags(parsed, flagsAndAliases) {
Object.keys(parsed).forEach((key) => {
if (key === '_') {
return;
}
const hasFlag = flagsAndAliases.find((value) => value === key);
if (!hasFlag) {
throw Exceptions_1.UnknownFlagException.invoke(key);
}
});
}
/**
* Processes ace command flag to set the options for `getopts`.
* We just define the `alias` with getopts coz their default,
* string and boolean options produces the behavior we don't
* want.
*/
preProcessFlag(flag, options) {
/**
* Register alias (when exists)
*/
if (flag.alias) {
options.alias[flag.alias] = flag.name;
}
}
/**
* Casts a flag value to a boolean. The casting logic is driven
* by the behavior of "getopts"
*/
castToBoolean(value) {
if (typeof value === 'boolean') {
return value;
}
if (value === 'true' || value === '=true') {
return true;
}
return undefined;
}
/**
* Cast the value to a string. The casting logic is driven
* by the behavior of "getopts"
*
* - Convert numbers to string
* - Do not convert boolean to a string, since a flag without a value
* gets a boolean value, which is invalid
*/
castToString(value) {
if (typeof value === 'number') {
value = String(value);
}
if (typeof value === 'string' && value.trim()) {
return value;
}
return undefined;
}
/**
* Cast value to an array of string. The casting logic is driven
* by the behavior of "getopts"
*
* - Numeric values are converted to string of array
* - A string value is splitted by comma and trimmed.
* - An array is casted to an array of string values
*/
castToArray(value) {
if (typeof value === 'number') {
value = String(value);
}
if (typeof value === 'string') {
return value.split(',').filter((prop) => prop.trim());
}
if (Array.isArray(value)) {
/**
* This will also convert numeric values to a string. The behavior
* is same as string flag type.
*/
return value.map((prop) => String(prop));
}
return undefined;
}
/**
* Cast value to an array of numbers. The casting logic is driven
* by the behavior of "getopts".
*
* - Numeric values are wrapped to an array.
* - String is splitted by comma and each value is casted to a number
* - Each array value is casted to a number.
*/
castToNumArray(value) {
if (typeof value === 'number') {
return [value];
}
if (typeof value === 'string') {
return value.split(',').map((one) => Number(one));
}
if (Array.isArray(value)) {
return value.map((prop) => Number(prop));
}
return undefined;
}
/**
* Cast value to a number. The casting logic is driven
* by the behavior of "getopts"
*
* - Boolean values are not allowed
* - A string is converted to a number
*/
castToNumer(value) {
if (typeof value === 'number') {
return value;
}
if (typeof value === 'string') {
// Possibility of NaN here
return Number(value);
}
return undefined;
}
/**
* Casts value of a flag to it's expected data type. These values
* are then later validated to ensure that casting was successful.
*/
processFlag(flag, parsed, command) {
let value = parsed[flag.name];
/**
* Check for the value with the alias, if it undefined
* by the name
*/
if (value === undefined && flag.alias) {
value = parsed[flag.alias];
}
/**
* Still undefined??
*
* It is fine. Flags are optional anyways
*/
if (value === undefined) {
return;
}
/**
* Handle boolean values. It should be a valid boolean
* data type or a string value of `'true'`.
*/
if (flag.type === 'boolean') {
value = this.castToBoolean(value);
if (value === undefined) {
throw Exceptions_1.InvalidFlagException.invoke(flag.name, flag.type, command);
}
}
/**
* Handle string value. It should be a valid and not empty.
* Either remove the flag or provide a value
*/
if (flag.type === 'string') {
value = this.castToString(value);
if (value === undefined) {
throw Exceptions_1.InvalidFlagException.invoke(flag.name, flag.type, command);
}
}
/**
* Handle numeric values. The flag should have a value and
* a valid number.
*/
if (flag.type === 'number') {
value = this.castToNumer(value);
if (value === undefined || isNaN(value)) {
throw Exceptions_1.InvalidFlagException.invoke(flag.name, flag.type, command);
}
}
/**
* Parse the value to be an array of strings
*/
if (flag.type === 'array') {
value = this.castToArray(value);
if (!value || !value.length) {
throw Exceptions_1.InvalidFlagException.invoke(flag.name, flag.type, command);
}
}
/**
* Parse the value to be an array of numbers
*/
if (flag.type === 'numArray') {
value = this.castToNumArray(value);
if (!value || !value.length) {
throw Exceptions_1.InvalidFlagException.invoke(flag.name, flag.type, command);
}
/**
* Find if array has NaN values
*/
if (value.findIndex((one) => isNaN(one)) > -1) {
throw Exceptions_1.InvalidFlagException.invoke(flag.name, flag.type, command);
}
}
parsed[flag.name] = value;
if (flag.alias) {
parsed[flag.alias] = value;
}
}
/**
* Validates the value to ensure that values are defined for
* required arguments.
*/
validateArg(arg, index, parsed, command) {
const value = parsed._[index];
if (value === undefined && arg.required) {
throw Exceptions_1.MissingArgumentException.invoke(arg.name, command);
}
}
/**
* Parses argv and executes the command and global flags handlers
*/
parse(argv, command) {
let options = { alias: {}, boolean: [], default: {}, string: [] };
const flagsAndAliases = [];
const globalFlags = Object.keys(this.registeredFlags).map((name) => this.registeredFlags[name]);
/**
* Build options from global flags
*/
globalFlags.forEach((flag) => {
this.preProcessFlag(flag, options);
flagsAndAliases.push(flag.name);
flag.alias && flagsAndAliases.push(flag.alias);
});
/**
* Build options from command flags
*/
if (command) {
command.flags.forEach((flag) => {
this.preProcessFlag(flag, options);
flagsAndAliases.push(flag.name);
flag.alias && flagsAndAliases.push(flag.alias);
});
}
/**
* Parsing argv with the previously built options
*/
const parsed = (0, getopts_1.default)(argv, options);
/**
* Scan and report unknown flag as exception
*/
if (command) {
this.scanForUnknownFlags(parsed, flagsAndAliases);
}
/**
* Validating global flags (if any)
*/
globalFlags.forEach((flag) => {
this.processFlag(flag, parsed);
});
/**
* Validating command flags (if command is defined)
*/
if (command) {
command.flags.forEach((flag) => {
this.processFlag(flag, parsed);
});
}
return parsed;
}
}
exports.Parser = Parser;