/** * @license * Copyright Google Inc. 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 * */ import { strings } from '@angular-devkit/core'; import { Arguments, Option, OptionType, Value } from './interface'; function _coerceType(str: string | undefined, type: OptionType, v?: Value): Value | undefined { switch (type) { case 'any': if (Array.isArray(v)) { return v.concat(str || ''); } return _coerceType(str, OptionType.Boolean, v) !== undefined ? _coerceType(str, OptionType.Boolean, v) : _coerceType(str, OptionType.Number, v) !== undefined ? _coerceType(str, OptionType.Number, v) : _coerceType(str, OptionType.String, v); case 'string': return str || ''; case 'boolean': switch (str) { case 'false': return false; case undefined: case '': case 'true': return true; default: return undefined; } case 'number': if (str === undefined) { return 0; } else if (Number.isFinite(+str)) { return +str; } else { return undefined; } case 'array': return Array.isArray(v) ? v.concat(str || '') : [str || '']; default: return undefined; } } function _coerce(str: string | undefined, o: Option | null, v?: Value): Value | undefined { if (!o) { return _coerceType(str, OptionType.Any, v); } else if (o.type == 'suboption') { return _coerceType(str, OptionType.String, v); } else { return _coerceType(str, o.type, v); } } function _getOptionFromName(name: string, options: Option[]): Option | undefined { const cName = strings.camelize(name); for (const option of options) { if (option.name == name || option.name == cName) { return option; } if (option.aliases.some(x => x == name || x == cName)) { return option; } } return undefined; } function _assignOption( arg: string, args: string[], options: Option[], parsedOptions: Arguments, _positionals: string[], leftovers: string[], ) { let key = arg.substr(2); let option: Option | null = 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 maybeOption = _getOptionFromName(key.substr(3), options); if (maybeOption && maybeOption.type == 'boolean') { value = 'false'; option = maybeOption; } } else if (key.startsWith('no')) { // Only use this key if the option matching the rest is a boolean. const maybeOption = _getOptionFromName(key.substr(2), 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) { // Not of type boolean, consume the next value. value = args[0]; // Only absorb it if it leads to a value. if (_coerce(value, maybeOption) !== undefined) { args.shift(); } else { value = ''; } option = maybeOption; } } } else { key = arg.substring(0, i); option = _getOptionFromName(key, options) || null; if (option) { value = arg.substring(i + 1); if (option.type === 'boolean' && _coerce(value, option) === undefined) { value = 'true'; } } } if (option === null) { if (args[0] && !args[0].startsWith('--')) { leftovers.push(arg, args[0]); args.shift(); } else { leftovers.push(arg); } } else { const v = _coerce(value, option, parsedOptions[option.name]); if (v !== undefined) { parsedOptions[option.name] = v; } } } /** * 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. */ export function parseFreeFormArguments(args: string[]): Arguments { const parsedOptions: Arguments = {}; 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: string; let value: string | undefined; 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); } } parsedOptions['--'] = leftovers; return parsedOptions; } /** * 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}. * @returns An object that contains a property per option. */ export function parseArguments(args: string[], options: Option[] | null): Arguments { if (options === null) { options = []; } const leftovers: string[] = []; const positionals: string[] = []; const parsedOptions: Arguments = {}; for (let arg = args.shift(); arg !== undefined; arg = args.shift()) { if (!arg) { break; } if (arg == '--') { // If we find a --, we're done. leftovers.push(...args); break; } if (arg.startsWith('--')) { _assignOption(arg, args, options, parsedOptions, positionals, leftovers); } 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]; // 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) { _assignOption('--' + flag, args, options, parsedOptions, positionals, leftovers); } 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); } } // Deal with positionals. 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, we need to remove it. if (option.positional === pos) { if (parsedOptions[option.name] === undefined) { parsedOptions[option.name] = positionals[i]; 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]; } return parsedOptions; }