Skip to content

Instantly share code, notes, and snippets.

@oncomouse
Last active June 2, 2020 14:38
Show Gist options
  • Save oncomouse/a653ad14a7b405b8d170b3028defe042 to your computer and use it in GitHub Desktop.
Save oncomouse/a653ad14a7b405b8d170b3028defe042 to your computer and use it in GitHub Desktop.
ARGV Splitter and Parser for Node.js
/**
Usage:
// test.js
const argv = require('./argv-split')
const arguments = argv(process.argv.slice(2))
console.log(arguments)
> node test.js -abc 123 --foo=bar -de6 --flag1 -r5 file1 file2
{
a: true,
b: true,
c: 123,
foo: 'bar',
d: true,
e: 6,
flag1: true,
r: 5,
_: [ 'file1', 'file2' ]
}
*/
const argv = function (argv) {
// This class tracks a copy of argv and marks arguments for removal as they
// are processed. This allows us to run reduce() on ARGV and still safely do
// read-ahead for key value pairs.
class CleanManager {
constructor(argv) {
this.CLEANED = Symbol('CLEANED');
this.cleaned = argv.slice();
}
clean(index) {
this.cleaned[index] = this.CLEANED;
}
isCleaned(index) {
return this.cleaned[index] === this.CLEANED;
}
removeCleaned() {
return this.cleaned.filter(x => x !== this.CLEANED);
}
}
const clean = new CleanManager(argv);
// Check if there is a multi-key (e.g. -abc)
function getKeys(arg) {
if (arg.indexOf('--') === 0) {
return [arg.replace(/^--/, '')];
} else {
return arg.replace(/^-/, '').split('');
}
}
function extractNumber(value) {
const int = parseFloat(value, 10);
return isNaN(int) ? value : int;
}
function setValue(arg, value) {
return getKeys(arg).reduce((obj, key) => ({...obj, [key]: value}), {})
}
// Handles the two instances where arguments can be multiple short keys (e.g.
// -abc).
function setMultiKey(output, arg, value) {
const subArgv = getKeys(arg);
return Object.assign(output, setValue(subArgv.slice(0, -1).join(''), true), setValue(subArgv[subArgv.length - 1], extractNumber(value)));
}
// Check if the argument is a switch (starts w/ - or --):
function isSwitch(arg, index) {
return !clean.isCleaned(index) && arg.indexOf('-') === 0;
}
const output = argv.reduce((output, arg, index) => {
// If argument has already been processed (usually as a value) or argument
// is a file arg (usually at the end of the argument chain), skip it:
if (!isSwitch(arg, index)) return output;
// Handle a key/value pair (e.g. --key=value or -k=value):
if (arg.indexOf('=') >= 0) {
const [key, value] = arg.split(/=/);
clean.clean(index);
return Object.assign(output, setValue(key, extractNumber(value)));
}
// Handle a short key/number-value pair (e.g. -k9 or -x5):
const keyValueShort = arg.match(/^-([A-Za-z]+)([0-9]+)/);
if (keyValueShort !== null) {
const [, key, value] = keyValueShort;
clean.clean(index);
// This handles arguments of the form: -abc5
// I thought about what happens if you pass something like -abc5d and
// can see a case for why this *should* parse it as all true except c,
// which is 5, but I think we have to draw the line somewhere.
if (key.length > 1) {
return setMultiKey(output, key, extractNumber(value));
}
return Object.assign(output, {[key]: extractNumber(value)});
}
// Handle boolean arguments (e.g. --key or -k, with no following value):
// Also handles combo boolean switches (e.g. -abc with no following value):
if (index + 1 === argv.length || isSwitch(argv[index + 1], index + 1)) {
clean.clean(index);
return Object.assign(output, setValue(arg, true));
}
// Handle a combo argument (e.g. -abc 123, where a & b are flags and c is 123):
if (/^-[^-]{2,}/.test(arg)) {
clean.clean(index);
clean.clean(index + 1);
return setMultiKey(output, arg, extractNumber(argv[index + 1]));
}
// Handle a standard key value pair (e.g. -k value OR --key value):
clean.clean(index);
clean.clean(index + 1);
return Object.assign(output, setValue(arg, extractNumber(argv[index + 1])));
}, {});
return {
...output,
_: clean.removeCleaned(),
}
}
module.exports = argv;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment