An argv Parser Without yargs or commander

Eighty percent of the time I just need --flag and --opt=value parsing for a one-off script. Adding yargs feels heavy. This is the 30-line zero-dependency parser I copy into every Node script.

JavaScript
Frontend
3 snippets
input-output
code-template
functional-programming
nehanasser

By @nehanasser

May 1, 2026

·

Updated August 19, 2026

461 views

8

4.4 (12)

// parseArgs(argv) returns { flags: Set, options: {}, positional: [] }.
// Supports: --flag, --no-flag, --opt=value, --opt value, -short=v, -short v.
// Stops parsing after a literal '--', the rest is positional.

function parseArgs(argv) {
    const flags = new Set();
    const options = {};
    const positional = [];
    let i = 0;
    while (i < argv.length) {
        const arg = argv[i];
        if (arg === '--') { positional.push(...argv.slice(i + 1)); break; }
        if (arg.startsWith('--')) {
            const body = arg.slice(2);
            const eq = body.indexOf('=');
            if (eq !== -1) {
                options[body.slice(0, eq)] = body.slice(eq + 1);
            } else if (body.startsWith('no-')) {
                flags.add(body.slice(3)); options[body.slice(3)] = false;
            } else if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
                options[body] = argv[++i];
            } else {
                flags.add(body); options[body] = true;
            }
        } else if (arg.startsWith('-') && arg.length > 1) {
            const body = arg.slice(1);
            const eq = body.indexOf('=');
            if (eq !== -1) {
                options[body.slice(0, eq)] = body.slice(eq + 1);
            } else if (i + 1 < argv.length && !argv[i + 1].startsWith('-')) {
                options[body] = argv[++i];
            } else {
                flags.add(body); options[body] = true;
            }
        } else {
            positional.push(arg);
        }
        i++;
    }
    return { flags, options, positional };
}

const sample = ['--verbose', '--out=build.json', '-n', '10', 'input.txt', '--', '--not-a-flag'];
const parsed = parseArgs(sample);
console.log({
    flags: [...parsed.flags],
    options: parsed.options,
    positional: parsed.positional,
});

The parser handles the four shapes I actually use: bare --flag, --key=value, --key value, and --no-flag for negation. The -- sentinel is critical for any script that forwards args to a child process (think npm run start -- --port=3000); without it, downstream tools see your script's flag parser eating their flags. The positional collection at the end keeps trailing arguments simple: anything that does not look like a flag, or anything after --, is positional. I deliberately do not support clumped short flags (-abc meaning -a -b -c) because it is rarely worth the parser complexity.