You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
349 lines
13 KiB
349 lines
13 KiB
"use strict";
|
|
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.parseArguments = exports.parseFreeFormArguments = exports.ParseArgumentException = void 0;
|
|
const core_1 = require("@angular-devkit/core");
|
|
const interface_1 = require("./interface");
|
|
class ParseArgumentException extends core_1.BaseException {
|
|
constructor(comments, parsed, ignored) {
|
|
super(`One or more errors occurred while parsing arguments:\n ${comments.join('\n ')}`);
|
|
this.comments = comments;
|
|
this.parsed = parsed;
|
|
this.ignored = ignored;
|
|
}
|
|
}
|
|
exports.ParseArgumentException = ParseArgumentException;
|
|
function _coerceType(str, type, v) {
|
|
switch (type) {
|
|
case interface_1.OptionType.Any:
|
|
if (Array.isArray(v)) {
|
|
return v.concat(str || '');
|
|
}
|
|
return _coerceType(str, interface_1.OptionType.Boolean, v) !== undefined
|
|
? _coerceType(str, interface_1.OptionType.Boolean, v)
|
|
: _coerceType(str, interface_1.OptionType.Number, v) !== undefined
|
|
? _coerceType(str, interface_1.OptionType.Number, v)
|
|
: _coerceType(str, interface_1.OptionType.String, v);
|
|
case interface_1.OptionType.String:
|
|
return str || '';
|
|
case interface_1.OptionType.Boolean:
|
|
switch (str) {
|
|
case 'false':
|
|
return false;
|
|
case undefined:
|
|
case '':
|
|
case 'true':
|
|
return true;
|
|
default:
|
|
return undefined;
|
|
}
|
|
case interface_1.OptionType.Number:
|
|
if (str === undefined) {
|
|
return 0;
|
|
}
|
|
else if (str === '') {
|
|
return undefined;
|
|
}
|
|
else if (Number.isFinite(+str)) {
|
|
return +str;
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
case interface_1.OptionType.Array:
|
|
return Array.isArray(v)
|
|
? v.concat(str || '')
|
|
: v === undefined
|
|
? [str || '']
|
|
: [v + '', str || ''];
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
function _coerce(str, o, v) {
|
|
if (!o) {
|
|
return _coerceType(str, interface_1.OptionType.Any, v);
|
|
}
|
|
else {
|
|
const types = o.types || [o.type];
|
|
// Try all the types one by one and pick the first one that returns a value contained in the
|
|
// enum. If there's no enum, just return the first one that matches.
|
|
for (const type of types) {
|
|
const maybeResult = _coerceType(str, type, v);
|
|
if (maybeResult !== undefined && (!o.enum || o.enum.includes(maybeResult))) {
|
|
return maybeResult;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
function _getOptionFromName(name, options) {
|
|
const camelName = /(-|_)/.test(name) ? core_1.strings.camelize(name) : name;
|
|
for (const option of options) {
|
|
if (option.name === name || option.name === camelName) {
|
|
return option;
|
|
}
|
|
if (option.aliases.some((x) => x === name || x === camelName)) {
|
|
return option;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
function _removeLeadingDashes(key) {
|
|
const from = key.startsWith('--') ? 2 : key.startsWith('-') ? 1 : 0;
|
|
return key.substr(from);
|
|
}
|
|
function _assignOption(arg, nextArg, { options, parsedOptions, leftovers, ignored, errors, warnings, }) {
|
|
const from = arg.startsWith('--') ? 2 : 1;
|
|
let consumedNextArg = false;
|
|
let key = arg.substr(from);
|
|
let option = null;
|
|
let value = '';
|
|
const i = arg.indexOf('=');
|
|
// If flag is --no-abc AND there's no equal sign.
|
|
if (i == -1) {
|
|
if (key.startsWith('no')) {
|
|
// Only use this key if the option matching the rest is a boolean.
|
|
const from = key.startsWith('no-') ? 3 : 2;
|
|
const maybeOption = _getOptionFromName(core_1.strings.camelize(key.substr(from)), options);
|
|
if (maybeOption && maybeOption.type == 'boolean') {
|
|
value = 'false';
|
|
option = maybeOption;
|
|
}
|
|
}
|
|
if (option === null) {
|
|
// Set it to true if it's a boolean and the next argument doesn't match true/false.
|
|
const maybeOption = _getOptionFromName(key, options);
|
|
if (maybeOption) {
|
|
value = nextArg;
|
|
let shouldShift = true;
|
|
if (value && value.startsWith('-') && _coerce(undefined, maybeOption) !== undefined) {
|
|
// Verify if not having a value results in a correct parse, if so don't shift.
|
|
shouldShift = false;
|
|
}
|
|
// Only absorb it if it leads to a better value.
|
|
if (shouldShift && _coerce(value, maybeOption) !== undefined) {
|
|
consumedNextArg = true;
|
|
}
|
|
else {
|
|
value = '';
|
|
}
|
|
option = maybeOption;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
key = arg.substring(0, i);
|
|
option = _getOptionFromName(_removeLeadingDashes(key), options) || null;
|
|
if (option) {
|
|
value = arg.substring(i + 1);
|
|
}
|
|
}
|
|
if (option === null) {
|
|
if (nextArg && !nextArg.startsWith('-')) {
|
|
leftovers.push(arg, nextArg);
|
|
consumedNextArg = true;
|
|
}
|
|
else {
|
|
leftovers.push(arg);
|
|
}
|
|
}
|
|
else {
|
|
const v = _coerce(value, option, parsedOptions[option.name]);
|
|
if (v !== undefined) {
|
|
if (parsedOptions[option.name] !== v) {
|
|
if (parsedOptions[option.name] !== undefined && option.type !== interface_1.OptionType.Array) {
|
|
warnings.push(`Option ${JSON.stringify(option.name)} was already specified with value ` +
|
|
`${JSON.stringify(parsedOptions[option.name])}. The new value ${JSON.stringify(v)} ` +
|
|
`will override it.`);
|
|
}
|
|
parsedOptions[option.name] = v;
|
|
}
|
|
}
|
|
else {
|
|
let error = `Argument ${key} could not be parsed using value ${JSON.stringify(value)}.`;
|
|
if (option.enum) {
|
|
error += ` Valid values are: ${option.enum.map((x) => JSON.stringify(x)).join(', ')}.`;
|
|
}
|
|
else {
|
|
error += `Valid type(s) is: ${(option.types || [option.type]).join(', ')}`;
|
|
}
|
|
errors.push(error);
|
|
ignored.push(arg);
|
|
}
|
|
if (/^[a-z]+[A-Z]/.test(key)) {
|
|
warnings.push('Support for camel case arguments has been deprecated and will be removed in a future major version.\n' +
|
|
`Use '--${core_1.strings.dasherize(key)}' instead of '--${key}'.`);
|
|
}
|
|
}
|
|
return consumedNextArg;
|
|
}
|
|
/**
|
|
* Parse the arguments in a consistent way, but without having any option definition. This tries
|
|
* to assess what the user wants in a free form. For example, using `--name=false` will set the
|
|
* name properties to a boolean type.
|
|
* This should only be used when there's no schema available or if a schema is "true" (anything is
|
|
* valid).
|
|
*
|
|
* @param args Argument list to parse.
|
|
* @returns An object that contains a property per flags from the args.
|
|
*/
|
|
function parseFreeFormArguments(args) {
|
|
const parsedOptions = {};
|
|
const leftovers = [];
|
|
for (let arg = args.shift(); arg !== undefined; arg = args.shift()) {
|
|
if (arg == '--') {
|
|
leftovers.push(...args);
|
|
break;
|
|
}
|
|
if (arg.startsWith('--')) {
|
|
const eqSign = arg.indexOf('=');
|
|
let name;
|
|
let value;
|
|
if (eqSign !== -1) {
|
|
name = arg.substring(2, eqSign);
|
|
value = arg.substring(eqSign + 1);
|
|
}
|
|
else {
|
|
name = arg.substr(2);
|
|
value = args.shift();
|
|
}
|
|
const v = _coerce(value, null, parsedOptions[name]);
|
|
if (v !== undefined) {
|
|
parsedOptions[name] = v;
|
|
}
|
|
}
|
|
else if (arg.startsWith('-')) {
|
|
arg.split('').forEach((x) => (parsedOptions[x] = true));
|
|
}
|
|
else {
|
|
leftovers.push(arg);
|
|
}
|
|
}
|
|
if (leftovers.length) {
|
|
parsedOptions['--'] = leftovers;
|
|
}
|
|
return parsedOptions;
|
|
}
|
|
exports.parseFreeFormArguments = parseFreeFormArguments;
|
|
/**
|
|
* Parse the arguments in a consistent way, from a list of standardized options.
|
|
* The result object will have a key per option name, with the `_` key reserved for positional
|
|
* arguments, and `--` will contain everything that did not match. Any key that don't have an
|
|
* option will be pushed back in `--` and removed from the object. If you need to validate that
|
|
* there's no additionalProperties, you need to check the `--` key.
|
|
*
|
|
* @param args The argument array to parse.
|
|
* @param options List of supported options. {@see Option}.
|
|
* @param logger Logger to use to warn users.
|
|
* @returns An object that contains a property per option.
|
|
*/
|
|
function parseArguments(args, options, logger) {
|
|
if (options === null) {
|
|
options = [];
|
|
}
|
|
const leftovers = [];
|
|
const positionals = [];
|
|
const parsedOptions = {};
|
|
const ignored = [];
|
|
const errors = [];
|
|
const warnings = [];
|
|
const state = { options, parsedOptions, positionals, leftovers, ignored, errors, warnings };
|
|
for (let argIndex = 0; argIndex < args.length; argIndex++) {
|
|
const arg = args[argIndex];
|
|
let consumedNextArg = false;
|
|
if (arg == '--') {
|
|
// If we find a --, we're done.
|
|
leftovers.push(...args.slice(argIndex + 1));
|
|
break;
|
|
}
|
|
if (arg.startsWith('--')) {
|
|
consumedNextArg = _assignOption(arg, args[argIndex + 1], state);
|
|
}
|
|
else if (arg.startsWith('-')) {
|
|
// Argument is of form -abcdef. Starts at 1 because we skip the `-`.
|
|
for (let i = 1; i < arg.length; i++) {
|
|
const flag = arg[i];
|
|
// If the next character is an '=', treat it as a long flag.
|
|
if (arg[i + 1] == '=') {
|
|
const f = '-' + flag + arg.slice(i + 1);
|
|
consumedNextArg = _assignOption(f, args[argIndex + 1], state);
|
|
break;
|
|
}
|
|
// Treat the last flag as `--a` (as if full flag but just one letter). We do this in
|
|
// the loop because it saves us a check to see if the arg is just `-`.
|
|
if (i == arg.length - 1) {
|
|
const arg = '-' + flag;
|
|
consumedNextArg = _assignOption(arg, args[argIndex + 1], state);
|
|
}
|
|
else {
|
|
const maybeOption = _getOptionFromName(flag, options);
|
|
if (maybeOption) {
|
|
const v = _coerce(undefined, maybeOption, parsedOptions[maybeOption.name]);
|
|
if (v !== undefined) {
|
|
parsedOptions[maybeOption.name] = v;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
positionals.push(arg);
|
|
}
|
|
if (consumedNextArg) {
|
|
argIndex++;
|
|
}
|
|
}
|
|
// Deal with positionals.
|
|
// TODO(hansl): this is by far the most complex piece of code in this file. Try to refactor it
|
|
// simpler.
|
|
if (positionals.length > 0) {
|
|
let pos = 0;
|
|
for (let i = 0; i < positionals.length;) {
|
|
let found = false;
|
|
let incrementPos = false;
|
|
let incrementI = true;
|
|
// We do this with a found flag because more than 1 option could have the same positional.
|
|
for (const option of options) {
|
|
// If any option has this positional and no value, AND fit the type, we need to remove it.
|
|
if (option.positional === pos) {
|
|
const coercedValue = _coerce(positionals[i], option, parsedOptions[option.name]);
|
|
if (parsedOptions[option.name] === undefined && coercedValue !== undefined) {
|
|
parsedOptions[option.name] = coercedValue;
|
|
found = true;
|
|
}
|
|
else {
|
|
incrementI = false;
|
|
}
|
|
incrementPos = true;
|
|
}
|
|
}
|
|
if (found) {
|
|
positionals.splice(i--, 1);
|
|
}
|
|
if (incrementPos) {
|
|
pos++;
|
|
}
|
|
if (incrementI) {
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
if (positionals.length > 0 || leftovers.length > 0) {
|
|
parsedOptions['--'] = [...positionals, ...leftovers];
|
|
}
|
|
if (warnings.length > 0 && logger) {
|
|
warnings.forEach((message) => logger.warn(message));
|
|
}
|
|
if (errors.length > 0) {
|
|
throw new ParseArgumentException(errors, parsedOptions, ignored);
|
|
}
|
|
return parsedOptions;
|
|
}
|
|
exports.parseArguments = parseArguments;
|