tunnckoCore / opensource

Delivering delightful digital solutions. Monorepo of monorepos of Open Source packages with combined ~100M/month downloads, semantically versioned following @conventional-commits. Fully powered ES Modules, @Airbnb @ESLint + @Prettier, independent & fixed versioning. Quality with @Actions, CodeQL, & Dependabot.
https://tunnckocore.com/opensource
481 stars 18 forks source link

flags/options parser in 100 lines #210

Closed tunnckoCore closed 2 years ago

tunnckoCore commented 2 years ago

On par speed with mri, while nopt (npm's) is way too speedy for us, mri, and minimist. Yargs-parser is the slowest.

Temporary here.

const args = process.argv.slice(2);

// fixture: -aaa -aBcd ---foo --wasm ok --foo --qux=-2 --goo=100 --no-beep --no-raw=yes --input one --wtf --input two -vvv --input three --da as --da=sa --da hi --ho=a,b,c -o=output.js --bar ko -o xaxa.js

const REPLACER = '@@@@__REPLACE_VALUE__@@@@';

module.exports = parse;
function parse(argv) {
  const res = {};

  function store(name, value, replaceValue) {
    res[name] = res[name] || [];

    const idx = res[name].indexOf(REPLACER);
    if (replaceValue && idx !== -1) {
      res[name][idx] = value ?? true;
      return res;
    }

    res[name] = res[name].concat(value);

    return res;
  }

  let prev = null;
  let idx = 0;
  for (const arg of argv) {
    idx += 1;
    const isDashPositional = arg[0] === '-' && arg.length === 1;
    const isShort = arg[0] === '-' && arg[1] !== '-';
    const isLong = arg[0] === '-' && arg[1] === '-';
    const isDoubleDash = isLong && arg.length === 2;
    const isNegate = isLong && arg.indexOf('no-') > 0;
    const isExplicit = arg.includes('=');

    if (isDashPositional) {
      store('-', true);
      continue;
    }

    if (isDoubleDash) {
      store('--', argv.slice(idx));
      break;
    }

    const i = isExplicit ? arg.indexOf('=') : 0;
    if (isShort) {
      const key = isExplicit ? arg.slice(1, i) : arg.slice(1);
      if (isExplicit && key.length === 1) {
        store(key, arg.slice(i + 1) || true);
      } else if (key.length === 1) {
        prev = key;
        store(key, REPLACER);
      } else {
        key.split('').map((k) => store(k, true));
      }
      continue;
    }
    if (isLong) {
      const parts = arg.split('=');
      const key = (isExplicit ? parts[0] : arg).slice(isNegate ? 5 : 2);
      if (isNegate) {
        store(key, false);
        continue;
      }
      if (isExplicit) {
        store(key, parts[1] || true);
        continue;
      } else {
        prev = key;
        const isEql = key === arg.slice(2);

        if (isEql && isNegate) {
          store(key, false);
          continue;
        }

        store(key, REPLACER);
        continue;
      }
    }

    // it is a value
    if (prev && !isLong && !isShort) {
      store(prev, arg, true);
    }
  }

  return Object.entries(res).reduce((acc, [key, value]) => {
    if (value.length === 1) {
      acc[key] = value[0] === REPLACER ? true : value[0];
    } else {
      acc[key] = value.every((x) => x === true) ? value.length : value;
    }
    return acc;
  }, {});
}

/**
 * Example
 */

const argv = parse(args);

let commandParts = [];
let commandArgs = null;
let commandOptions = null;

if (argv['--']) {
  const commandOptionsStart = argv['--'].findIndex((x) => x[0] === '-');
  commandParts = argv['--'].slice(0, commandOptionsStart).join(' ');
  commandArgs = argv['--'].slice(commandOptionsStart);
  commandOptions = parse(commandArgs);
}

console.log('global options:', argv);
console.log('command:', commandParts);
// console.log(commandArgs);
console.log('cmd options:', commandOptions);

/**
 * Type checking and casting features
 */

function typecheck(parsed, settings) {
  if (!settings && typeof settings !== 'object') {
    return parsed;
  }

  const cfg = {
    default: { ...settings.default },
    alias: { ...settings.alias },
    boolean: [settings.boolean].flat().filter(Boolean),
    string: [settings.string].flat().filter(Boolean),
    number: [settings.number].flat().filter(Boolean),
    array: [settings.array].flat().filter(Boolean),
  };

  const res = { ...parsed };

  for (const key of Object.keys(cfg.default)) {
    res[key] = res[key] ?? cfg.default[key];
  }

  for (const flagName of Object.keys(cfg.alias)) {
    const aliases = cfg.alias[flagName];
    for (const key of aliases) {
      res[key] = res[flagName];
    }
  }

  for (const flagName of cfg.number) {
    res[flagName] = Number(res[flagName]);
  }
  for (const flagName of cfg.string) {
    res[flagName] = String(res[flagName]);
  }
  for (const flagName of cfg.array) {
    res[flagName] = String(res[flagName]).split(',').flat();
  }
  for (const flagName of cfg.boolean) {
    res[flagName] = Boolean(res[flagName]);
  }

  return res;
}

// console.log('===============');

// console.log('type casted, aliases, etc:',
//   typecheck(argv, {
//     default: {
//       w: 100,
//       zaz: 'zzz',
//       foo: false,
//       wasm: 'yes',
//     },
//     alias: {
//       input: ['in', 'i'],
//     },
//     number: ['a', 'w', 'goo'],
//     boolean: ['bar'],
//     array: ['ho'],
//   }),
// );