index.js 5.12 KB
'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;