getopt-like CLI Options in node.js

getopt-like CLI Options in node.js

Parsing CLI options with getopt-ish configuration

#!/usr/bin/env node

function argvAddValue(ret, id, val) {
    if (ret[id] === undefined) {
        ret[id] = val;
        return;
    }

    if (!Array.isArray(ret[id])) ret[id] = [ ret[id] ];
    ret[id].push(val);
}

function argvParser(options, argv) {
    if (!argv) argv = process.argv;

    const { length } = argv;

    const sym = Symbol.for('input');
    const ret = {};

    for(let idx = 2, needValue = false; idx < length; idx++){
        const arg = argv[idx];

        if(arg[0] !== '\-') {
            if (ret[sym] === undefined) ret[sym] = [];
            if (!needValue) ret[sym].push(arg);
            continue;
        }

        for(const key of options){
            let token = key;

            needValue = Boolean(token.match(/(:$)/));
            if (needValue) token = token.replace(/:$/, '');

            const argIsLongOpt = Boolean(arg.match(/^\-\-/));
            const baseToken = `(^` + token.replace(/\-/g, '\\-');
            const regexp = argIsLongOpt
                ? new RegExp(baseToken + '(?:=|\$))')
                : new RegExp(baseToken + ')');
            const hasMatch = arg.match(regexp);

            if (!hasMatch) continue;

            // take long options name or first char of the token
            const id = token.match(/(?:^|\|)\-\-(.*?)(?:=|$)/)?.[1] ?? arg[1];

            if (!needValue) {
                argvAddValue(ret, id, true);
                break;
            }

            // no longer necessary, revert now
            needValue = false;

            if (argIsLongOpt) {
                const afterEq = arg.match(/=(.*$)/);
                if (afterEq) {
                    const value = afterEq[1];
                    argvAddValue(ret, id, value);
                    break;
                }
            }

            if (!argIsLongOpt && arg.length > 2) {
                const value = arg.replace(hasMatch[1], '');
                argvAddValue(ret, id, value);
                break;
            }

            const value = argv[++idx];
            argvAddValue(ret, id, value);
        }
    }

    return ret;
}

/* ***************************** EXAMPLE ************************************ */
const parsed = argvParser([
    '-c',  // short opt switch
    '-b:', // short opt with value
    '--long',      // long opt switch
    '--long-arg:', // long opt with option
    '-v|--verbose',  // short and long opt switch
    '-e|--example:', // short and long opt with value
]);

console.log(parsed);
console.table(parsed);
console.table(parsed[Symbol.for('input')]);

/*******************************************************************************
 * INPUT:
 * ./argvParser.js          \
 *     file1                \
 *     -v                   \
 *     -bIPv4               \
 *     --long               \
 *     --long-arg arg1      \
 *     --long-arg=arg2      \
 *     -e example           \
 *     file2
 *
 * OUTPUT:
 * {
 *   verbose: true,
 *   b: 'IPv4',
 *   long: true,
 *   'long-arg': [ 'arg1', 'arg2' ],
 *   example: 'example',
 *   [Symbol(input)]: [ 'file1', 'file2' ]
 * }
 * ┌──────────┬────────┬────────┬───────────┐
 * │ (index)  │   0    │   1    │  Values   │
 * ├──────────┼────────┼────────┼───────────┤
 * │ verbose  │        │        │   true    │
 * │    b     │        │        │  'IPv4'   │
 * │   long   │        │        │   true    │
 * │ long-arg │ 'arg1' │ 'arg2' │           │
 * │ example  │        │        │ 'example' │
 * └──────────┴────────┴────────┴───────────┘
 * ┌─────────┬─────────┐
 * │ (index) │ Values  │
 * ├─────────┼─────────┤
 * │    0    │ 'file1' │
 * │    1    │ 'file2' │
 * └─────────┴─────────┘
 * */