Zero-Dependency argv Parser
A small argv parser supporting long flags, key=value and key value pairs, and positional arguments, with strict unknown-flag detection.
Project
node-argv-parser
Entry point: argv.mjs
Install
node --versionRun
node argv.mjs --verbose --count 3 file.txtTest
node --test argv.test.mjsExpected output
The demo parses --verbose --count 3 file.txt into flags, numbers, and positionals and prints the structured result as JSON.Dependencies
- node >=20
What it does
Parses long flags, key=value and spaced key value pairs, and positionals, rejecting unknown options and non-numeric values for declared numeric keys.
export function parseArgv(argv, { flags = [], numberKeys = [] } = {}) {
const result = { flags: {}, values: {}, positionals: [] };
const knownFlags = new Set(flags);
const numeric = new Set(numberKeys);
for (let i = 0; i < argv.length; i++) {
const token = argv[i];
if (token === '--') {
result.positionals.push(...argv.slice(i + 1));
break;
}
if (token.startsWith('--')) {
const body = token.slice(2);
const eq = body.indexOf('=');
if (eq !== -1) {
const key = body.slice(0, eq);
if (!knownFlags.has(key) && !numeric.has(key)) throw new Error('unknown option --' + key);
result.values[key] = coerce(key, body.slice(eq + 1), numeric);
} else {
if (!knownFlags.has(body) && !numeric.has(body)) throw new Error('unknown option --' + body);
if (knownFlags.has(body)) {
result.flags[body] = true;
} else {
const next = argv[i + 1];
if (next === undefined || next.startsWith('--')) {
result.values[body] = true;
} else {
result.values[body] = coerce(body, next, numeric);
i += 1;
}
}
}
} else {
result.positionals.push(token);
}
}
return result;
}
function coerce(key, value, numeric) {
if (!numeric.has(key)) return value;
const n = Number(value);
if (Number.isNaN(n)) throw new Error('option --' + key + ' expects a number, got ' + value);
return n;
}
const parsed = parseArgv(process.argv.slice(2), { flags: ['verbose'], numberKeys: ['count'] });
console.log(JSON.stringify(parsed));
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseArgv } from './argv.mjs';
const opts = { flags: ['verbose', 'color'], numberKeys: ['count', 'depth'] };
test('flags, key=value, spaced values, and positionals', () => {
const r = parseArgv(['--verbose', '--count=3', '--depth', '2', 'file.txt'], opts);
assert.equal(r.flags.verbose, true);
assert.equal(r.values.count, 3);
assert.equal(r.values.depth, 2);
assert.deepEqual(r.positionals, ['file.txt']);
});
test('unknown options are rejected', () => {
assert.throws(() => parseArgv(['--nope'], opts), /unknown option --nope/);
});
test('numeric options reject non-numeric values', () => {
assert.throws(() => parseArgv(['--count', 'abc'], opts), /expects a number/);
});
test('double dash sends the rest to positionals', () => {
const r = parseArgv(['--verbose', '--', 'a', '--b'], opts);
assert.deepEqual(r.positionals, ['a', '--b']);
});
Run and verify
node --test argv.test.mjs
node argv.mjs --verbose --count 3 file.txt
Run the tests for the parsing table, then the demo that prints the structured parse result as JSON.
Expected Output
The test run should print four passing tests covering the parsing table. The demo’s console.log output for node argv.mjs --verbose --count 3 file.txt should print the structured parse result as a single JSON line:
{"flags":{"verbose":true},"values":{"count":3},"positionals":["file.txt"]}
The --verbose flag becomes flags.verbose: true, --count 3 is coerced to the number 3 under values.count, and file.txt lands in positionals — every token is classified exactly once, and an unknown option would exit with the unknown option error instead.
Limitations
Short flags like -v are not supported, and negated –no- flags are absent; only the long forms documented in the spec object parse.