📜 Jelle Pelgrims

Command line argument parsing

I recently found out that node doesn't have a built-in command line argument parsing library. There are some libraries available for that purpose, but I really dislike using external dependencies for something that should be part of the standard library. I ended up writing my own, which, as it turns out, is really easy. It does make me even more confused about why it wasn't added to the stdlib, though.

I encapsulated the parsing functionality in a class using the fluent interface design pattern. I've encountered this pattern in some other argument parsing libraries and I feel like it really improves usability in this specific case.

The code can be found below. The argument parser currently supports long-form arguments (strings, numbers or booleans) with default values. At this point it is 57 lines of code, which could probably be improved. It works fine for now though.

export class CommandLineArgParser {

    possibleOptions = {};

    constructor(){
        return this;
    }

    addOption(name: string, type: string, default_value?: string|number|boolean) {
        this.possibleOptions[name] = { name, type, default_value };
        return this;
    }

    parse(argv) {
        let prevArg;
        let parsedOptions = {};

        // Do the actual parsing
        for (const arg of argv) {
            if (arg.startsWith("-")) {
                const flagName = arg.replace(/-/g, '');

                if (Object.keys(this.possibleOptions).includes(flagName)) {
                    let option = this.possibleOptions[flagName];

                    if (option.type === 'boolean') {
                        parsedOptions[flagName] = true;
                    }
                }
            } else if (prevArg != null && prevArg.startsWith("-")) {
                const flagName = prevArg.replace(/-/g, '');

                if (Object.keys(this.possibleOptions).includes(flagName)) {
                    let type = this.possibleOptions[flagName].type;

                    if (type == 'string') {
                        parsedOptions[flagName] = arg;
                    } else if (type == 'number' && arg.match(/[0-9].*/)) {
                        parsedOptions[flagName] = parseInt(arg);
                    }
                }
            }

            prevArg = arg;
        }

        // Add default values for flags that weren't passed
        for (const optionName of Object.keys(this.possibleOptions)) {
            const option = this.possibleOptions[optionName];
            if (!(optionName in parsedOptions) && option.default_value != null) {
                parsedOptions[optionName] = option.default_value;
            }
        }

        return parsedOptions;
    }
}

The class can then be used like this:

let options = new CommandLineArgParser()
    .addOption("production", "boolean")
    .addOption("port", 'number', 8080)
    .addOption("host", "string", "127.0.0.1")
    .parse(process.argv);

let backendClient = new BackendClient(options['host'], options['port']);