UNPKG

raptor-args

Version:

A concise command line arguments parser with robust type handling

419 lines (334 loc) 12.2 kB
var optionRegExp = /^--?(.+)$/; var argNameRegExp = /^(--?)(no-)?([^\s]+)$/; var arrayTypeRegExp = /(.*)\[\]$/; function DEFAULT_ON_ERROR(err) { throw err; } function leftPad(str, len) { while (str.length < len) { str = ' ' + str; } return str; } function removeDashes(str) { return str.replace(/-([a-z])/g, function(match, lower) { return lower.toUpperCase(); }); } function getTargetPropertyForOption(option, arg) { var targetProperty = option.targetProperty; if (arg && targetProperty === '*') { targetProperty = arg; if (option.removeDashes !== false) { targetProperty = removeDashes(targetProperty); } } return targetProperty; } function Options(config) { this._lookup = {}; this._options = []; if (config) { this.add(config); } } Options.prototype = { add: function(config) { for (var optionsString in config) { if (config.hasOwnProperty(optionsString)) { var optionConfig = config[optionsString]; if (!optionConfig) { optionConfig = {}; } else if (typeof optionConfig === 'string') { optionConfig = { type: optionConfig }; } optionConfig.optionsString = optionsString; this._options.push(optionConfig); var aliases = optionsString.split(/\s+/); if (!optionConfig.targetProperty) { var optionNameMatches = optionRegExp.exec(aliases[0]); if (optionNameMatches) { optionConfig.targetProperty = optionNameMatches[1]; } else { optionConfig.targetProperty = aliases[0]; } if (optionConfig.removeDashes !== false) { optionConfig.targetProperty = removeDashes(optionConfig.targetProperty); } } if (optionConfig.type) { var arrayTypeMatches = arrayTypeRegExp.exec(optionConfig.type); if (arrayTypeMatches) { optionConfig.type = arrayTypeMatches[1] || null; optionConfig.isArray = true; } } if (optionConfig.options) { optionConfig.options = new Options(optionConfig.options); } for (var i = 0, len = aliases.length; i < len; i++) { this._lookup[aliases[i]] = optionConfig; } } } }, getAllowedOptions: function() { return Object.keys(this._lookup); }, getProperties: function() { return this._options.map(function(optionConfig) { return optionConfig.targetProperty; }); }, get: function(optionName) { return this._lookup[optionName]; } }; function Parser() { this._options = new Options(); this._usage = null; this._examples = []; this._validators = []; this._onError = null; } Parser.prototype = { options: function(config) { this._options.add(config); return this; }, forEachOption: function(fn) { this._options._options.forEach(fn); }, getProperties: function() { return this._options.getProperties(); }, usage: function(usage) { this._usage = usage; return this; }, example: function(label, command) { if (arguments.length === 1) { command = label; label = null; } this._examples.push({ label: label, command: command }); return this; }, validate: function(validateFunc) { this._validators.push(validateFunc); return this; }, onError: function(handlerFunc) { this._onError = handlerFunc; return this; }, getUsageString: function() { var lines = []; var i; var $0 = require('path').basename(process.argv[1]); var usage = (this._usage || 'Usage: $0 [OPTIONS]').replace(/\$0/g, $0); lines.push(usage); lines.push(''); if (this._examples.length) { lines.push('Examples:'); lines.push(''); for (i = 0; i < this._examples.length; i++) { var example = this._examples[i]; var label = example.label; var command = example.command.replace(/\$0/g, $0); if (label) { lines.push(' ' + label + ':'); lines.push(' ' + command); } else { lines.push(' ' + command); } lines.push(''); } } var optionsString; var optionConfigs = this._options._options; if (optionConfigs.length) { // Figure out the width for the left column var maxOptionLength = -1; for (i = 0; i < optionConfigs.length; i++) { optionsString = optionConfigs[i].optionsString; if (maxOptionLength === -1 || optionsString.length > maxOptionLength) { maxOptionLength = optionsString.length; } } lines.push('Options:'); lines.push(''); for (i = 0; i < optionConfigs.length; i++) { var option = optionConfigs[i]; optionsString = option.optionsString; var leftColumn = leftPad(optionsString, maxOptionLength); var indent = leftPad('', maxOptionLength + 1); var description = option.description || ''; if (option.type) { description += (description ? ' ' : '') + '[' + option.type + ']'; } var rightColumn = description.replace(/\n/g, '\n' + indent); lines.push(leftColumn + ' ' + rightColumn); lines.push(''); } } return lines.join('\n'); }, printUsage: function() { console.log(this.getUsageString()); }, parse: function(args) { if (args === undefined) { args = process.argv.slice(2); } var state = { option: null, targetProperty: null, result: {}, options: this._options }; var stack = [state]; function finishLastOption() { if (state.option) { var targetProperty = state.targetProperty; // Finish off the previous option... if (state.result[targetProperty] == null) { state.result[targetProperty] = true; } } state.option = null; } var onError = this._onError ? this._onError.bind(this) : DEFAULT_ON_ERROR; function addValue(arg) { var targetProperty = state.targetProperty; var option = state.option; if (!option) { option = state.option = state.options.get('*'); } if (!option) { return onError('Illegal argument: "' + arg + '"'); } if (!targetProperty) { targetProperty = state.targetProperty = getTargetPropertyForOption(option); } if (option.isArray || targetProperty === '*') { if (state.result[targetProperty] == null) { state.result[targetProperty] = []; } } var existingValue = state.result[targetProperty]; if (arg === 'true') { arg = true; } else if (arg === 'false') { arg = false; } else if (option.type === 'number') { arg = parseFloat(arg); } else if (option.type === 'int' || option.type === 'integer') { arg = parseInt(arg, 10); } else if (option.type === 'boolean') { arg = arg.toLowerCase(); arg = arg === '1' || arg === 'true' || arg === 'yes' || arg === 'y'; } if (existingValue == null) { state.result[targetProperty] = arg; } else { if (Array.isArray(existingValue)) { existingValue.push(arg); } else { state.result[targetProperty] = [existingValue, arg]; } } } for (var i = 0, len = args.length; i < len; i++) { var arg = args[i]; var argNameMatches; var option; if (arg === '[') { var nestedOptions = state.option.options; if (!nestedOptions) { nestedOptions = new Options({ '*': null }); } state = { option: null, targetProperty: null, result: {}, options: nestedOptions }; stack.push(state); } else if (arg === ']') { finishLastOption(); var complexResult = stack[stack.length - 1].result; // Restore parser state stack.pop(); state = stack[stack.length - 1]; addValue(complexResult); } else if( (argNameMatches = argNameRegExp.exec(arg)) ) { // We hit either a '--' or '-' arg finishLastOption(); var prefix = argNameMatches[1]; var no = argNameMatches[2]; var argName = argNameMatches[3]; if (no) { if (state.options.get(arg)) { no = false; } else { no = true; arg = prefix + argName; } } var splitArgs = null; if (prefix === '-') { /* jshint loopfunc:true */ splitArgs = argName.split('').map(function(shortArg) { return { argName: shortArg, arg: '-' + shortArg }; }); } else { splitArgs = [{ argName: argName, arg: arg }]; } for (var j = 0, len2 = splitArgs.length; j < len2; j++) { arg = splitArgs[j].arg; argName = splitArgs[j].argName; option = state.options.get(arg) || state.options.get('--*') || state.options.get('-*'); if (!option) { return onError('Illegal argument: "' + arg + '" (allowed: ' + state.options.getAllowedOptions().join(', ') + ')'); } state.option = option; state.targetProperty = getTargetPropertyForOption(option, argName); if (splitArgs.length > 1) { state.result[state.targetProperty] = true; } else if (no) { // Prefixed with 'no-' state.result[state.targetProperty] = false; } } } else { // This is an argument that is not prefixed with '--' or '-' addValue(arg); } } finishLastOption(); // Run the validators try { for (i = 0, len = this._validators.length; i < len; i++) { this._validators[i].call(this, state.result, this); } } catch (e) { onError(e); } return state.result; } }; module.exports = Parser;