@kui-shell/core
Version:
An Electron-based shell for cloud-native development
232 lines • 11.8 kB
JavaScript
/*
* Copyright 2017 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Debug from 'debug';
// TODO esModuleInterop to allow for import
// import * as minimist from 'yargs-parser'
const minimist = require('yargs-parser');
import { UsageError } from '../core/usage-error';
const debug = Debug('repl/enforce-usage');
/** trim the optional suffix e.g. --last [actionName] */
const stripTrailer = (str) => str && str.replace(/\s+.*$/, '');
/** turn --foo into foo and -f into f */
const unflag = (opt) => opt && stripTrailer(opt.replace(/^[-]+/, ''));
/**
* yargs-parser flags that we want to apply to all commands
*
*/
function getUsageOptionsForAll(usage) {
const builtInOptions = [{ name: '--quiet', alias: '-q', hidden: true, boolean: true }];
if (!usage || !usage.noHelp) {
// usage might tell us not to add help, or not to add the -h help alias
const help = {
name: '--help',
hidden: true,
boolean: true
};
if (!usage || !usage.noHelpAlias) {
help.alias = '-h';
}
builtInOptions.push(help);
}
return builtInOptions;
}
/**
* Extract any yargs-parser flags that the command wants us to use
*
*/
function getYargsParserFlagsForCommand(evaluator) {
return ((evaluator.options && evaluator.options.flags) ||
(evaluator.options &&
evaluator.options.synonymFor &&
evaluator.options.synonymFor.options &&
evaluator.options.synonymFor.options.flags) ||
{});
}
function strictChecks(usage, builtInOptions, argvNoOptions, parsedOptions) {
//
// here is where we enforce the usage model
//
if (usage && usage.strict) {
// strict: command wants *us* to enforce conformance
// required and optional parameters
const { strict: cmd, onlyEnforceOptions = false, required = [], oneof = [], optional: _optional = [] } = usage;
const optLikeOneOfs = oneof.filter(({ command, name = command }) => name.charAt(0) === '-'); // some one-ofs might be of the form --foo
const positionalConsumers = _optional.filter(({ name, alias, consumesPositional }) => consumesPositional && (parsedOptions[unflag(name)] || parsedOptions[unflag(alias)]));
const optional = builtInOptions.concat(_optional).concat(optLikeOneOfs);
const positionalOptionals = optional.filter(({ positional }) => positional);
const nPositionalOptionals = positionalOptionals.length;
// just introducing a shorter variable name, here
const args = argvNoOptions;
const nPositionalsConsumed = positionalConsumers.length;
const nRequiredArgs = required.length + (oneof.length > 0 ? 1 : 0) - nPositionalsConsumed;
const optLikeActuals = optLikeOneOfs.filter(({ name, alias = '' }) => Object.prototype.hasOwnProperty.call(parsedOptions, unflag(name)) ||
Object.prototype.hasOwnProperty.call(parsedOptions, unflag(alias)));
const nOptLikeActuals = optLikeActuals.length;
const cmdArgsStart = args.indexOf(cmd);
const nActualArgs = args.length - cmdArgsStart - 1 + nOptLikeActuals;
// did the user pass an unsupported optional parameter?
for (const optionalArg in parsedOptions) {
// skip over minimist's _
if (optionalArg === '_' || parsedOptions[optionalArg] === false) {
// minimist nonsense
continue;
}
// should we enforce this option?
const enforceThisOption = onlyEnforceOptions === undefined || typeof onlyEnforceOptions === 'boolean'
? true
: !!onlyEnforceOptions.find(_ => _ === `-${optionalArg}` || _ === `--${optionalArg}`);
if (!enforceThisOption) {
// then neither did the spec didn't mention anything about enforcement (!onlyEnforceOptions)
// nor did the spec said only to enforce options, but enforce them all (onlyEnforceOptions === true)
// nor did the spec enumerated options to enforce, and this is one of them
continue;
}
// find a matching declared optional arg
const match = optional.find(({ name, alias }) => {
return (stripTrailer(alias) === `-${optionalArg}` ||
stripTrailer(name) === `-${optionalArg}` ||
stripTrailer(name) === `--${optionalArg}`);
});
if (!match) {
//
// then the user passed an option, but the command doesn't accept it
//
debug('unsupported optional paramter', optionalArg);
const message = `Unsupported optional parameter ${optionalArg}`;
const err = new UsageError({ message, usage });
err.code = 499;
debug(message, args, parsedOptions, optional); // args is argv with options stripped
throw err;
}
else if ((match.boolean && typeof parsedOptions[optionalArg] !== 'boolean') ||
(match.file && typeof parsedOptions[optionalArg] !== 'string') ||
(match.booleanOK &&
!(typeof parsedOptions[optionalArg] === 'boolean' || typeof parsedOptions[optionalArg] === 'string')) ||
(match.numeric && typeof parsedOptions[optionalArg] !== 'number') ||
(match.narg > 1 && !Array.isArray(parsedOptions[optionalArg])) ||
(!match.boolean &&
!match.booleanOK &&
!match.numeric &&
(!match.narg || match.narg === 1) &&
!(Array.isArray(parsedOptions[optionalArg]) ||
typeof parsedOptions[optionalArg] === 'string' ||
typeof parsedOptions[optionalArg] === 'number' ||
typeof parsedOptions[optionalArg] === 'boolean')) ||
// is the given option not one of the allowed options
(match.allowed &&
!match.allowed.find(_ => _ === parsedOptions[optionalArg] ||
_ === '...' ||
(match.allowedIsPrefixMatch && parsedOptions[optionalArg].toString().indexOf(_.toString()) === 0)))) {
//
// then the user passed an option, but of the wrong type
//
debug('bad value for option', optionalArg, match, parsedOptions, args);
const expectedMessage = match.boolean
? ', expected boolean'
: match.numeric
? ', expected a number'
: match.file
? ', expected a file path'
: '';
const message = `Bad value for option ${optionalArg}${expectedMessage}${typeof parsedOptions[optionalArg] === 'boolean' ? '' : ', got ' + parsedOptions[optionalArg]}${match.allowed ? ' expected one of: ' + match.allowed.join(', ') : ''}`;
const error = new UsageError({ message, usage });
debug(message, match);
error.code = 498;
throw error;
}
}
//
// user passed an incorrect number of positional parameters?
//
if (!onlyEnforceOptions && nActualArgs < nRequiredArgs) {
const message = nRequiredArgs === 0 && nPositionalOptionals === 0
? 'This command accepts no positional arguments'
: nPositionalOptionals > 0
? 'This command does not accept this number of arguments'
: `This command requires ${nRequiredArgs} parameter${nRequiredArgs === 1 ? '' : 's'}, but you provided none`;
const err = new UsageError({ message, usage });
err.code = 497;
debug(message, cmd, nActualArgs, nRequiredArgs, args, optLikeActuals);
throw err;
}
} /* strict usage model conformance checking */
}
/**
* Do the given arguments (both positional and optional) conform to
* the minimal requirements of the usage model of the given
* `evaluator`?
*
*/
export default function enforceUsage(argv, evaluator, execOptions) {
//
// fetch the usage model for the command
//
const _usage = evaluator.options && evaluator.options.usage;
const usage = _usage && _usage.fn ? _usage.fn(_usage.command) : _usage;
if (execOptions && execOptions.failWithUsage && !usage) {
debug('caller needs usage model, but none exists for this command', evaluator);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return false;
}
const builtInOptions = getUsageOptionsForAll(usage);
// here, we encode some common aliases, and then overlay any flags from the command
// narg: any flags that take more than one argument e.g. -p key value would have { narg: { p: 2 } }
const commandFlags = getYargsParserFlagsForCommand(evaluator);
const optional = builtInOptions.concat((evaluator.options && evaluator.options.usage && evaluator.options.usage.optional) || []);
const optionalBooleans = optional && optional.filter(({ boolean }) => boolean).map(_ => unflag(_.name));
const optionalAliases = optional &&
optional
.filter(({ alias }) => alias)
.reduce((M, { name, alias }) => {
M[unflag(alias)] = unflag(name);
return M;
}, {});
const allFlags = {
configuration: Object.assign({ 'camel-case-expansion': false }, (evaluator.options && evaluator.options.flags && evaluator.options.flags.configuration) ||
(usage && usage.configuration) ||
{}),
string: Object.assign({}, commandFlags.string || {}),
boolean: (commandFlags.boolean || []).concat(optionalBooleans || []),
alias: Object.assign({}, commandFlags.alias || {}, optionalAliases || {}),
narg: Object.assign({}, commandFlags.narg || {}, // narg from registrar.listen(route, handler, { flags: { narg: ... }})
(optional &&
optional.reduce((N, { name, alias, narg }) => {
// narg from listen(route, handler, { usage: { optional: [...] }})
if (narg) {
N[unflag(name)] = narg;
N[unflag(alias)] = narg;
}
return N;
}, {})) ||
{})
};
// now use minimist to parse the command line options
// minimist stores the residual, non-opt, args in _
const parsedOptions = minimist(argv, allFlags);
const argvNoOptions = parsedOptions._;
//
// if the user asked for help, and the plugin registered a
// usage model, we can service that here, without having
// to involve the plugin. this lets us avoid having each
// plugin check for options.help
//
if ((!usage || !usage.noHelp) && parsedOptions.help && evaluator.options && evaluator.options.usage) {
throw new UsageError({ usage: evaluator.options.usage });
}
strictChecks(usage, builtInOptions, argvNoOptions, parsedOptions);
return true;
}
//# sourceMappingURL=enforce-usage.js.map