167 lines
5.1 KiB
JavaScript
167 lines
5.1 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
const flatten = require('flat');
|
||
|
const camelcase = require('camelcase');
|
||
|
const decamelize = require('decamelize');
|
||
|
const isPlainObj = require('is-plain-obj');
|
||
|
|
||
|
function isAlias(key, alias) {
|
||
|
// TODO Switch to Object.values one Node.js 6 is dropped
|
||
|
return Object.keys(alias).some((id) => [].concat(alias[id]).indexOf(key) !== -1);
|
||
|
}
|
||
|
|
||
|
function hasDefaultValue(key, value, defaults) {
|
||
|
return value === defaults[key];
|
||
|
}
|
||
|
|
||
|
function isCamelCased(key, argv) {
|
||
|
return /[A-Z]/.test(key) && camelcase(key) === key && // Is it camel case?
|
||
|
argv[decamelize(key, '-')] != null; // Is the standard version defined?
|
||
|
}
|
||
|
|
||
|
function keyToFlag(key) {
|
||
|
return key.length === 1 ? `-${key}` : `--${key}`;
|
||
|
}
|
||
|
|
||
|
function parseCommand(cmd) {
|
||
|
const extraSpacesStrippedCommand = cmd.replace(/\s{2,}/g, ' ');
|
||
|
const splitCommand = extraSpacesStrippedCommand.split(/\s+(?![^[]*]|[^<]*>)/);
|
||
|
const bregex = /\.*[\][<>]/g;
|
||
|
const firstCommand = splitCommand.shift();
|
||
|
|
||
|
if (!firstCommand) { throw new Error(`No command found in: ${cmd}`); }
|
||
|
const parsedCommand = {
|
||
|
cmd: firstCommand.replace(bregex, ''),
|
||
|
demanded: [],
|
||
|
optional: [],
|
||
|
};
|
||
|
|
||
|
splitCommand.forEach((cmd, i) => {
|
||
|
let variadic = false;
|
||
|
|
||
|
cmd = cmd.replace(/\s/g, '');
|
||
|
if (/\.+[\]>]/.test(cmd) && i === splitCommand.length - 1) { variadic = true; }
|
||
|
if (/^\[/.test(cmd)) {
|
||
|
parsedCommand.optional.push({
|
||
|
cmd: cmd.replace(bregex, '').split('|'),
|
||
|
variadic,
|
||
|
});
|
||
|
} else {
|
||
|
parsedCommand.demanded.push({
|
||
|
cmd: cmd.replace(bregex, '').split('|'),
|
||
|
variadic,
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return parsedCommand;
|
||
|
}
|
||
|
|
||
|
function unparseOption(key, value, unparsed) {
|
||
|
if (typeof value === 'string') {
|
||
|
unparsed.push(keyToFlag(key), value);
|
||
|
} else if (value === true) {
|
||
|
unparsed.push(keyToFlag(key));
|
||
|
} else if (value === false) {
|
||
|
unparsed.push(`--no-${key}`);
|
||
|
} else if (Array.isArray(value)) {
|
||
|
value.forEach((item) => unparseOption(key, item, unparsed));
|
||
|
} else if (isPlainObj(value)) {
|
||
|
const flattened = flatten(value, { safe: true });
|
||
|
|
||
|
for (const flattenedKey in flattened) {
|
||
|
if (!isCamelCased(flattenedKey, flattened)) {
|
||
|
unparseOption(`${key}.${flattenedKey}`, flattened[flattenedKey], unparsed);
|
||
|
}
|
||
|
}
|
||
|
// Fallback case (numbers and other types)
|
||
|
} else if (value != null) {
|
||
|
unparsed.push(keyToFlag(key), `${value}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function unparsePositional(argv, options, unparsed) {
|
||
|
const knownPositional = [];
|
||
|
|
||
|
// Unparse command if set, collecting all known positional arguments
|
||
|
// e.g.: build <first> <second> <rest...>
|
||
|
if (options.command) {
|
||
|
const { 0: cmd, index } = options.command.match(/[^<[]*/);
|
||
|
const { demanded, optional } = parseCommand(`foo ${options.command.substr(index + cmd.length)}`);
|
||
|
|
||
|
// Push command (can be a deep command)
|
||
|
unparsed.push(...cmd.trim().split(/\s+/));
|
||
|
|
||
|
// Push positional arguments
|
||
|
[...demanded, ...optional].forEach(({ cmd: cmds, variadic }) => {
|
||
|
knownPositional.push(...cmds);
|
||
|
|
||
|
const cmd = cmds[0];
|
||
|
const args = (variadic ? argv[cmd] || [] : [argv[cmd]])
|
||
|
.filter((arg) => arg != null)
|
||
|
.map((arg) => `${arg}`);
|
||
|
|
||
|
unparsed.push(...args);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Unparse unkown positional arguments
|
||
|
argv._ && unparsed.push(...argv._.slice(knownPositional.length));
|
||
|
|
||
|
return knownPositional;
|
||
|
}
|
||
|
|
||
|
function unparseOptions(argv, options, knownPositional, unparsed) {
|
||
|
for (const key of Object.keys(argv)) {
|
||
|
const value = argv[key];
|
||
|
|
||
|
if (
|
||
|
// Remove positional arguments
|
||
|
knownPositional.includes(key) ||
|
||
|
// Remove special _, -- and $0
|
||
|
['_', '--', '$0'].includes(key) ||
|
||
|
// Remove aliases
|
||
|
isAlias(key, options.alias) ||
|
||
|
// Remove default values
|
||
|
hasDefaultValue(key, value, options.default) ||
|
||
|
// Remove camel-cased
|
||
|
isCamelCased(key, argv)
|
||
|
) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
unparseOption(key, argv[key], unparsed);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function unparseEndOfOptions(argv, options, unparsed) {
|
||
|
// Unparse ending (--) arguments if set
|
||
|
argv['--'] && unparsed.push('--', ...argv['--']);
|
||
|
}
|
||
|
|
||
|
// ------------------------------------------------------------
|
||
|
|
||
|
function unparser(argv, options) {
|
||
|
options = Object.assign({
|
||
|
alias: {},
|
||
|
default: {},
|
||
|
command: null,
|
||
|
}, options);
|
||
|
|
||
|
const unparsed = [];
|
||
|
|
||
|
// Unparse known & unknown positional arguments (foo <first> <second> [rest...])
|
||
|
// All known positional will be returned so that they are not added as flags
|
||
|
const knownPositional = unparsePositional(argv, options, unparsed);
|
||
|
|
||
|
// Unparse option arguments (--foo hello --bar hi)
|
||
|
unparseOptions(argv, options, knownPositional, unparsed);
|
||
|
|
||
|
// Unparse "end-of-options" arguments (stuff after " -- ")
|
||
|
unparseEndOfOptions(argv, options, unparsed);
|
||
|
|
||
|
return unparsed;
|
||
|
}
|
||
|
|
||
|
module.exports = unparser;
|