UNPKG

cli.args

Version:

Easy command line arguments handling library for node.js

316 lines (291 loc) 8.44 kB
/** * @preserve Copyright (c) 2014 Anselm Meyn * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions:</p> * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * */ "use strict"; var path = require('path'); /* */ function Option(optionOptions) { for (var index in optionOptions) { if (optionOptions[index]) { this[index] = optionOptions[index]; } } } /* */ function CliArgsError(usageStr, summaryStr) { this.name = "CliArgsError"; this.message = ""; this.usage = usageStr; this.summary = summaryStr; } Object.defineProperty(CliArgsError.prototype, "msg", { get: function() { return this.message; }, set: function(newMsg) { this.message = newMsg; } }); function createArgHelper(optionsString, optionsHelp, argv) { /* */ function setupStringOpts(optString) { var optsList = {}; var matchedOptions = optString.match(/([A-Za-z0-9]{1}:?!?)/g); matchedOptions && matchedOptions.forEach(function(val) { optsList[val[0]] = new Option({ needsArg : val[1] === ':', isRequired : val[2] === '!' || val[1] === '!' }); }); return optsList; } /* */ function setupArrayOpts(optsArray) { var optsList = {}; optsArray && optsArray.forEach(function(val, index, list) { var matchedStrings = val.match(/([A-Za-z0-9]+)(:?!?)/); var opt = matchedStrings[1]; if (opt) { optsList[opt] = new Option({ needsArg : matchedStrings[2][0] === ':', isRequired : matchedStrings[2][1] === '!' || matchedStrings[2][0] === '!', isLong : (opt.length > 1) }); } }); return optsList; } /* */ function parseOptString(optString) { if (typeof optString === "string" || optString instanceof String) { return setupStringOpts(optString); } else if (optString instanceof Array) { return setupArrayOpts(optString); } } /* */ function parseArgs(argv, options, errObj) { var optRegEx = new RegExp("(^-{1,2})([A-Za-z0-9 ,]+)"); var regexMatch; var nonOpt = []; var result = {}; var argvIndex, optStrIndex; var currOptStr; var option; var optionKey; var optionsKeys; var optionMatches; var exactMatch; if (!options || Object.keys(options).length <= 0) { return { 'nonOpt': argv }; } else { optionsKeys = Object.keys(options); } for (argvIndex=0; argvIndex<argv.length; ++argvIndex) { // As per POSIX any args after a '--' should be considered non-optional if (argv[argvIndex] === '--') { nonOpt = nonOpt.concat(argv.slice(argvIndex+1)); break; } // RegExp.exec() returns an array with the following elements, // [0] - the matched character(s) // [1]..[n] - matched substrings (in parenthesis i.e. 2 in our case) // index: index of the match in the string // input: original string // OR // null if no match regexMatch = optRegEx.exec(argv[argvIndex]); if (!regexMatch) { nonOpt.push(argv[argvIndex]); } else { // the option is in the 2nd matched substring currOptStr = regexMatch[2]; // use the first substring match to determine short / long option if (regexMatch[1] === '--') { optionKey = null; // find closest matches optionMatches = optionsKeys.filter(function(word) { return (word.indexOf(currOptStr) === 0); }); if (optionMatches.length === 1) { optionKey = optionMatches[0]; } else if (optionMatches.length > 1) { exactMatch = optionMatches.filter(function(word) { return (currOptStr === word); }); if (!exactMatch || exactMatch.length < 1) { errObj.msg = "option --" + currOptStr + " is ambiguous (--" + optionMatches.join(", --") + ")"; throw errObj; } else { optionKey = exactMatch; } } option = options[optionKey]; if (option) { if (option.needsArg) { if (argvIndex+1 < argv.length) { result[optionKey] = argv[++argvIndex]; } else { errObj.msg = "option needs an argument (" + optionKey + ")"; throw errObj; } } else { result[optionKey] = true; } } else { errObj.msg = "unrecognized option (" + currOptStr + ")"; throw errObj; } } else if(regexMatch[1] === '-') { if (!currOptStr) { // if it was only a '-' nonOpt.push(argv[argvIndex]); } else { for (optStrIndex=0; optStrIndex<currOptStr.length; ++optStrIndex) { var currChar = currOptStr.charAt(optStrIndex); var remainingChars = currOptStr.substring(optStrIndex+1); if (options[currChar]) { if (options[currChar].needsArg) { if (remainingChars) { result[currChar] = remainingChars; } else if (argvIndex+1 < argv.length) { result[currChar] = argv[++argvIndex]; } else { errObj.msg = "option needs an argument (" + currChar + ")"; throw errObj; } break; } else { result[currChar] = true; } } else { errObj.msg = "unrecognized option (" + currChar + ")"; throw errObj; } } } } } } if (nonOpt.length > 0) { result['nonOpt'] = nonOpt; } return result; } /* */ function buildSummaryStr(usage, options, helpObj) { var summaryStr = []; var currOpt; var currOptHelp = []; if (options) { for (var index in helpObj) { currOpt = options[index]; if (currOpt && helpObj[index]) { currOptHelp = ['\t']; currOptHelp.push((currOpt.isLong ? '--' : '-') + index); currOptHelp.push((currOpt.needsArg ? ' ' + helpObj[index][0] + '\n\t\t' + helpObj[index].slice(1).join('\n') : '\t' + helpObj[index].join('\n'))); summaryStr.push(currOptHelp.join('')); } } if (summaryStr.length > 0) { summaryStr.unshift('Options:'); } summaryStr.unshift('Usage: ' + usage); helpObj && helpObj['pre'] && summaryStr.unshift(helpObj['pre']); helpObj && helpObj['post'] && summaryStr.push(helpObj['post']); } return summaryStr.join('\n'); } /* */ function buildUsageStr(argv, options) { var usageStr = []; var currStr; for (var index in options) { var option = options[index]; currStr = ''; if (option) { currStr = (option.isLong ? '--' : '-') + index; if (option.needsArg) { currStr += ' value'; } if (option.isRequired) { usageStr.unshift(currStr); } else { usageStr.push('['+currStr+']'); } } } usageStr.unshift(argv[0], path.basename(argv[1])); return usageStr.join(' '); } // -- main() -- argv = argv || process.argv; var optionsList = parseOptString(optionsString); var usageStr = buildUsageStr(argv, optionsList) var summaryStr = buildSummaryStr(usageStr, optionsList, optionsHelp); var argsError = new CliArgsError(usageStr, summaryStr); var obj = { info: {}, }; var result = parseArgs(argv.slice(2), optionsList, argsError); // do this initially so that if the user wants to use the // 'info' or 'argv' keys for themselves they may do so obj['argv'] = process.argv; obj['info'] = { usage: usageStr, summary: summaryStr }; for (var index in result) { obj[index] = result[index]; } for (var index in optionsList) { var option = optionsList[index]; if (option.isRequired && !obj[index]) { argsError.msg = 'required option missing (' + index + ')'; throw argsError; } } return obj; } module.exports = createArgHelper;