├── .gitignore ├── .jshintrc ├── .travis.yml ├── README.md ├── package.json ├── src ├── Parser.js └── index.js └── test ├── .jshintrc └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /work 2 | /build 3 | /.idea/ 4 | /npm-debug.log 5 | /node_modules 6 | /*.sublime-workspace 7 | *.orig 8 | .DS_Store 9 | coverage 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node" : true, 3 | "es5" : false, 4 | "browser" : true, 5 | "boss" : false, 6 | "curly": false, 7 | "debug": false, 8 | "devel": false, 9 | "eqeqeq": true, 10 | "evil": true, 11 | "forin": false, 12 | "immed": true, 13 | "laxbreak": false, 14 | "newcap": true, 15 | "noarg": true, 16 | "noempty": false, 17 | "nonew": true, 18 | "nomen": false, 19 | "onevar": false, 20 | "plusplus": false, 21 | "regexp": false, 22 | "undef": true, 23 | "sub": false, 24 | "white": false, 25 | "eqeqeq": false, 26 | "latedef": true, 27 | "unused": "vars", 28 | "strict": false, 29 | "eqnull": true 30 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "0.10" 3 | - "0.11" 4 | language: node_js 5 | script: "npm test" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | argly 2 | =========== 3 | A flexible and simple command line arguments parser that generates friendly help messages. 4 | 5 | # Installation 6 | ```bash 7 | npm install argly --save 8 | ``` 9 | 10 | # Usage 11 | 12 | ```javascript 13 | // Create a parser: 14 | var parser = require('argly').createParser(options); 15 | var parsed = parser.parse(argsArray); 16 | 17 | // parsed will be an object with properties corresponding to provided arguments 18 | ``` 19 | 20 | ## Simple Example 21 | 22 | Parse arguments provided by `process.argv`: 23 | 24 | Given the following JavaScript code to parse the args: 25 | ```javascript 26 | // Create a parser and parse process.argv 27 | require('argly').createParser({ 28 | '--foo -f': 'boolean', 29 | '--bar -b': 'string' 30 | }) 31 | .parse(); 32 | ``` 33 | 34 | And the following command: 35 | ```bash 36 | node app.js --foo -b b 37 | ``` 38 | 39 | The output will be: 40 | 41 | ```javascript 42 | //Output: 43 | { 44 | foo: true, 45 | bar: 'baz' 46 | } 47 | ``` 48 | 49 | You can also parse your own array of arguments instead of using `process.argv`: 50 | 51 | ```javascript 52 | // Create a parser and parse provided args 53 | require('argly').createParser({ 54 | '--foo -f': 'boolean', 55 | '--bar -b': 'string' 56 | }) 57 | .parse(['--foo', '-b', 'baz']); 58 | 59 | //Output: 60 | { 61 | foo: true, 62 | bar: 'baz' 63 | } 64 | ``` 65 | 66 | You can also be more descriptive and add usage, examples, error handlers and validation checks: 67 | ```javascript 68 | // Create a parser: 69 | require('argly') 70 | .createParser({ 71 | '--help': { 72 | type: 'string', 73 | description: 'Show this help message' 74 | }, 75 | '--foo -f': { 76 | type: 'string', 77 | description: 'Some helpful description for "foo"' 78 | }, 79 | '--bar -b': { 80 | type: 'string', 81 | description: 'Some helpful description for "bar"' 82 | } 83 | }) 84 | .usage('Usage: $0 [options]') 85 | .example( 86 | 'First example', 87 | '$0 --foo hello') 88 | .example( 89 | 'Second example', 90 | '$0 --foo hello --bar world') 91 | .validate(function(result) { 92 | if (result.help) { 93 | this.printUsage(); 94 | process.exit(0); 95 | } 96 | 97 | if (!result.foo || !result.bar) { 98 | this.printUsage(); 99 | console.log('--foo or --bar is required'); 100 | process.exit(1); 101 | } 102 | }) 103 | .onError(function(err) { 104 | this.printUsage(); 105 | console.error(err); 106 | process.exit(1); 107 | }) 108 | .parse(); 109 | ``` 110 | 111 | Running the above program with the `--help` argument will produce the following output: 112 | 113 | ``` 114 | Usage: args [options] 115 | 116 | Examples: 117 | 118 | First example: 119 | args --foo hello 120 | 121 | Second example: 122 | args --foo hello --bar world 123 | 124 | Options: 125 | 126 | --help Show this help message [string] 127 | 128 | --foo -f Some helpful description for "foo" [string] 129 | 130 | --bar -b Some helpful description for "bar" [string] 131 | ``` 132 | 133 | 134 | ## Aliases 135 | 136 | Aliases can be provided as space-separated values for an option: 137 | ```javascript 138 | // Create a parser: 139 | var parser = require('argly').createParser({ 140 | '--foobar --foo -f': 'string', // "--foobar" has two aliases: "--foo" and "-f" 141 | '--hello -h': 'string', // "--hello" has one alias: "-h" 142 | }); 143 | 144 | parser.parse('--foo FOO -h HELLO'.split(' ')); 145 | // Output: 146 | { 147 | foobar: 'FOO', 148 | hello: 'HELLO' 149 | } 150 | 151 | // **NOTE**: Only the first entry is used to determine the target property name--not the aliases. 152 | ``` 153 | 154 | ## Booleans 155 | 156 | An argument value of "true" or "false" is automatically converted to the corresponding boolean type. If a argument is prefixed with "no-" then it will be set to `false`. 157 | 158 | ```javascript 159 | // Create a parser: 160 | var parser = require('argly').createParser({ 161 | '--foo': 'boolean', 162 | '--bar': 'boolean' 163 | }); 164 | 165 | parser.parse('--foo --no-bar'.split(' ')); 166 | // Output: 167 | { 168 | foo: true, 169 | bar: false 170 | } 171 | ``` 172 | 173 | ## Arrays 174 | 175 | Any argument with multiple values will result in an `Array` value, but if you want to force an array for a single value then you can append "[]" to the option type as shown in the following sample code: 176 | ```javascript 177 | // Create a parser: 178 | var parser = require('argly').createParser({ 179 | '--foo': 'string[]' 180 | }); 181 | 182 | parser.parse('--foo a'.split(' ')); 183 | // Output: 184 | { 185 | foo: ['a'] 186 | } 187 | 188 | parser.parse('--foo a b c'.split(' ')); 189 | // Output: 190 | { 191 | foo: ['a', 'b', 'c'] 192 | } 193 | ``` 194 | 195 | ## Wildcards 196 | 197 | A parser will throw an error for unrecognized arguments unless wildcards are used as shown in the examples below. 198 | 199 | ```javascript 200 | // Create a parser: 201 | var parser = require('argly').createParser({ 202 | '--foo -f *': 'string[]' // Any unrecognized argument at the beginning is an alias for "foo" 203 | }); 204 | 205 | parser.parse('a b --foo c'.split(' ')); 206 | // Output: 207 | { 208 | foo: ['a', 'b', 'c'] 209 | } 210 | ``` 211 | 212 | ```javascript 213 | // Create a parser: 214 | var parser = require('argly').createParser({ 215 | '*': null 216 | }); 217 | 218 | parser.parse('a b --foo FOO --bar BAR'.split(' ')); 219 | // Output: 220 | { 221 | '*': ['a', 'b'], 222 | foo: 'FOO', 223 | bar: 'BAR' 224 | } 225 | ``` 226 | 227 | ## Complex Types 228 | 229 | Square brackets can be used to begin and end complex types: 230 | 231 | ```javascript 232 | // Create a parser: 233 | var parser = require('argly').createParser({ 234 | '--foo -f': 'boolean', 235 | '--plugins --plugin -p': { 236 | options: { 237 | '--module -m *': 'string', 238 | '-*': null 239 | } 240 | } 241 | }); 242 | 243 | var parsed = parser.parse('--foo --plugins [ --module plugin1 -x -y ] [ plugin2 -z Hello ]'.split(' ')); 244 | 245 | // Output: 246 | { 247 | foo: true, 248 | plugins: [ 249 | { 250 | module: 'plugin1', 251 | x: true, 252 | y: true 253 | }, 254 | { 255 | module: 'plugin2', 256 | z: 'Hello' 257 | } 258 | ] 259 | } 260 | ``` 261 | 262 | 263 | # Similar Projects 264 | 265 | * [optimist](https://github.com/substack/node-optimist) - Popular but deprecated. Awkward API and not DRY as shown in the following comparison: 266 | 267 | __optimist:__ 268 | 269 | ```javascript 270 | var result = require('optimist')(args) 271 | .alias('h', 'help') 272 | .describe('h', 'Show this help message') 273 | .boolean('h') 274 | .alias('f', 'foo') 275 | .describe('f', 'Some helpful description for "foo"') 276 | .string('f') 277 | .alias('b', 'bar') 278 | .describe('b', 'Some helpful description for "bar"') 279 | .string('b') 280 | .argv; 281 | ``` 282 | 283 | __argly:__ 284 | 285 | ```javascript 286 | var result = require('argly') 287 | .createParser({ 288 | '--help': { type: 'string', description: 'Show this help message' }, 289 | '--foo -f': { type: 'string', description: 'Some helpful description for "foo"' }, 290 | '--bar -b': { type: 'string', description: 'Some helpful description for "bar"' } 291 | }) 292 | .parse(); 293 | ``` 294 | 295 | * [yargs](https://github.com/chevex/yargs) - A fork of `optimist` with documentation for those who speak Pirate. 296 | * [minimist](https://github.com/substack/minimist) - Very few features (by design). Not DRY. 297 | 298 | # TODO 299 | 300 | * Support equal separator: `--hello=world` 301 | * Support number arg: `-x256` 302 | 303 | # Additional Reading 304 | 305 | For module help, check out the test cases under the "test" directory. 306 | 307 | # License 308 | 309 | MIT 310 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "argly", 3 | "description": "A concise command line arguments parser with robust type handling", 4 | "keywords": [ 5 | "argument", 6 | "args", 7 | "option", 8 | "parser", 9 | "parsing", 10 | "cli", 11 | "command" 12 | ], 13 | "homepage": "https://github.com/patrick-steele-idem/argly", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/patrick-steele-idem/argly.git" 17 | }, 18 | "scripts": { 19 | "test": "node_modules/.bin/mocha --ui bdd --reporter spec ./test && node_modules/.bin/jshint src" 20 | }, 21 | "author": "Patrick Steele-Idem ", 22 | "contributors": [ 23 | "Patrick Steele-Idem ", 24 | "Phillip Gates-Idem " 25 | ], 26 | "maintainers": "Patrick Steele-Idem ", 27 | "dependencies": {}, 28 | "devDependencies": { 29 | "mocha": "~1.15.1", 30 | "chai": "~1.8.1", 31 | "jshint": "^2.4.4" 32 | }, 33 | "license": "MIT", 34 | "bin": {}, 35 | "main": "src/index.js", 36 | "publishConfig": { 37 | "registry": "https://registry.npmjs.org/" 38 | }, 39 | "version": "1.1.0" 40 | } 41 | -------------------------------------------------------------------------------- /src/Parser.js: -------------------------------------------------------------------------------- 1 | var optionRegExp = /^--?(.+)$/; 2 | var argNameRegExp = /^(--?)(no-)?([^\s]+)$/; 3 | var arrayTypeRegExp = /(.*)\[\]$/; 4 | 5 | function DEFAULT_ON_ERROR(err) { 6 | throw err; 7 | } 8 | 9 | function leftPad(str, len) { 10 | while (str.length < len) { 11 | str = ' ' + str; 12 | } 13 | return str; 14 | } 15 | 16 | function removeDashes(str) { 17 | return str.replace(/-([a-z])/g, function(match, lower) { 18 | return lower.toUpperCase(); 19 | }); 20 | } 21 | 22 | function getTargetPropertyForOption(option, arg) { 23 | var targetProperty = option.targetProperty; 24 | 25 | if (arg && targetProperty === '*') { 26 | targetProperty = arg; 27 | 28 | if (option.removeDashes !== false) { 29 | targetProperty = removeDashes(targetProperty); 30 | } 31 | } 32 | 33 | return targetProperty; 34 | } 35 | 36 | var strToBool = { 37 | '1': true, 38 | 'true': true, 39 | 'yes': true, 40 | 'y': true, 41 | '0': false, 42 | 'false': false, 43 | 'no': false, 44 | 'n': false 45 | }; 46 | 47 | var defaultTypeHandlers = { 48 | 'string': function(defaultVal) { 49 | if (typeof defaultVal === 'string') { 50 | return defaultVal; 51 | } 52 | }, 53 | 'boolean': function(defaultVal) { 54 | if (typeof defaultVal === 'boolean') { 55 | return defaultVal; 56 | } else if (typeof defaultVal === 'string') { 57 | defaultVal = defaultVal.toLowerCase(); 58 | 59 | var boolConversion = strToBool[defaultVal]; 60 | 61 | if (typeof boolConversion !== 'undefined') { 62 | return boolConversion; 63 | } 64 | } 65 | }, 66 | 'int': function(defaultVal) { 67 | if (typeof defaultVal === 'number' || typeof defaultVal === 'string') { 68 | return parseInt(defaultVal, 10); 69 | } 70 | }, 71 | 'integer': function(defaultVal) { 72 | if (typeof defaultVal === 'number' || typeof defaultVal === 'string') { 73 | return parseInt(defaultVal, 10); 74 | } 75 | }, 76 | 'number': function(defaultVal) { 77 | if (typeof defaultVal === 'number' || typeof defaultVal === 'string') { 78 | return parseFloat(defaultVal); 79 | } 80 | } 81 | }; 82 | 83 | function handleDefaults(parser, state, onError) { 84 | state.options.getOptions().forEach(function(option) { 85 | var targetProperty = option.targetProperty; 86 | var defaultValue = option.defaultValue; 87 | 88 | // Skip options that already have a value or do not have a default 89 | if (typeof state.result[targetProperty] !== 'undefined' || 90 | typeof defaultValue === 'undefined') { 91 | return; 92 | } 93 | 94 | if (typeof defaultValue === 'function') { 95 | defaultValue = defaultValue.call(parser); 96 | } 97 | 98 | var handler; 99 | // If the option has a type and it is not a complex type, we validate 100 | // that the default is the type we expect 101 | if (option.type && (handler = defaultTypeHandlers[option.type])) { 102 | var result = handler(defaultValue); 103 | 104 | if (typeof result === 'undefined') { 105 | return onError("Invalid default value '" + defaultValue + "' for target property '" + targetProperty + "'"); 106 | } 107 | 108 | state.result[option.targetProperty] = result; 109 | } else { 110 | state.result[option.targetProperty] = option.defaultValue; 111 | } 112 | }); 113 | } 114 | 115 | function Options(config) { 116 | this._lookup = {}; 117 | this._options = []; 118 | 119 | if (config) { 120 | this.add(config); 121 | } 122 | } 123 | 124 | Options.prototype = { 125 | add: function(config) { 126 | for (var optionsString in config) { 127 | if (config.hasOwnProperty(optionsString)) { 128 | var optionConfig = config[optionsString]; 129 | 130 | if (!optionConfig) { 131 | optionConfig = {}; 132 | } else if (typeof optionConfig === 'string') { 133 | optionConfig = { 134 | type: optionConfig 135 | }; 136 | } 137 | 138 | optionConfig.optionsString = optionsString; 139 | this._options.push(optionConfig); 140 | 141 | var aliases = optionsString.split(/\s+/); 142 | 143 | if (!optionConfig.targetProperty) { 144 | var optionNameMatches = optionRegExp.exec(aliases[0]); 145 | 146 | if (optionNameMatches) { 147 | optionConfig.targetProperty = optionNameMatches[1]; 148 | } else { 149 | optionConfig.targetProperty = aliases[0]; 150 | } 151 | 152 | if (optionConfig.removeDashes !== false) { 153 | optionConfig.targetProperty = removeDashes(optionConfig.targetProperty); 154 | } 155 | } 156 | 157 | if (optionConfig.type) { 158 | var arrayTypeMatches = arrayTypeRegExp.exec(optionConfig.type); 159 | if (arrayTypeMatches) { 160 | optionConfig.type = arrayTypeMatches[1] || null; 161 | optionConfig.isArray = true; 162 | } 163 | } 164 | 165 | if (optionConfig.options) { 166 | optionConfig.options = new Options(optionConfig.options); 167 | } 168 | 169 | for (var i = 0, len = aliases.length; i < len; i++) { 170 | var alias = aliases[i]; 171 | 172 | if (this._lookup[alias]) { 173 | throw new Error("Duplicate option provided '" + alias + "'"); 174 | } 175 | 176 | this._lookup[alias] = optionConfig; 177 | } 178 | } 179 | } 180 | }, 181 | 182 | getAllowedOptions: function() { 183 | return Object.keys(this._lookup); 184 | }, 185 | 186 | getProperties: function() { 187 | return this._options.map(function(optionConfig) { 188 | return optionConfig.targetProperty; 189 | }); 190 | }, 191 | 192 | getOptions: function() { 193 | return this._options; 194 | }, 195 | 196 | get: function(optionName) { 197 | return this._lookup[optionName]; 198 | } 199 | }; 200 | 201 | 202 | function Parser() { 203 | this._options = new Options(); 204 | this._usage = null; 205 | this._examples = []; 206 | this._validators = []; 207 | this._onError = null; 208 | } 209 | 210 | Parser.prototype = { 211 | options: function(config) { 212 | this._options.add(config); 213 | return this; 214 | }, 215 | 216 | forEachOption: function(fn) { 217 | this._options._options.forEach(fn); 218 | }, 219 | 220 | getProperties: function() { 221 | return this._options.getProperties(); 222 | }, 223 | 224 | usage: function(usage) { 225 | this._usage = usage; 226 | return this; 227 | }, 228 | 229 | example: function(label, command) { 230 | if (arguments.length === 1) { 231 | command = label; 232 | label = null; 233 | } 234 | 235 | this._examples.push({ 236 | label: label, 237 | command: command 238 | }); 239 | return this; 240 | }, 241 | 242 | validate: function(validateFunc) { 243 | this._validators.push(validateFunc); 244 | return this; 245 | }, 246 | 247 | onError: function(handlerFunc) { 248 | this._onError = handlerFunc; 249 | return this; 250 | }, 251 | 252 | getUsageString: function() { 253 | var lines = []; 254 | var i; 255 | var $0 = require('path').basename(process.argv[1]); 256 | var usage = (this._usage || 'Usage: $0 [OPTIONS]').replace(/\$0/g, $0); 257 | lines.push(usage); 258 | lines.push(''); 259 | 260 | if (this._examples.length) { 261 | lines.push('Examples:'); 262 | lines.push(''); 263 | for (i = 0; i < this._examples.length; i++) { 264 | var example = this._examples[i]; 265 | var label = example.label; 266 | var command = example.command.replace(/\$0/g, $0); 267 | if (label) { 268 | lines.push(' ' + label + ':'); 269 | lines.push(' ' + command); 270 | } else { 271 | lines.push(' ' + command); 272 | } 273 | lines.push(''); 274 | } 275 | } 276 | 277 | var optionsString; 278 | 279 | var optionConfigs = this._options._options; 280 | if (optionConfigs.length) { 281 | // Figure out the width for the left column 282 | var maxOptionLength = -1; 283 | for (i = 0; i < optionConfigs.length; i++) { 284 | optionsString = optionConfigs[i].optionsString; 285 | if (maxOptionLength === -1 || optionsString.length > maxOptionLength) { 286 | maxOptionLength = optionsString.length; 287 | } 288 | } 289 | 290 | lines.push('Options:'); 291 | lines.push(''); 292 | for (i = 0; i < optionConfigs.length; i++) { 293 | var option = optionConfigs[i]; 294 | optionsString = option.optionsString; 295 | 296 | var leftColumn = leftPad(optionsString, maxOptionLength); 297 | var indent = leftPad('', maxOptionLength + 1); 298 | var description = option.description || ''; 299 | if (option.type) { 300 | description += (description ? ' ' : '') + '[' + option.type + ']'; 301 | } 302 | 303 | var rightColumn = description.replace(/\n/g, '\n' + indent); 304 | 305 | lines.push(leftColumn + ' ' + rightColumn); 306 | lines.push(''); 307 | } 308 | } 309 | 310 | return lines.join('\n'); 311 | }, 312 | 313 | printUsage: function() { 314 | console.log(this.getUsageString()); 315 | }, 316 | 317 | parse: function(args) { 318 | if (args === undefined) { 319 | args = process.argv.slice(2); 320 | } 321 | 322 | var state = { 323 | option: null, 324 | targetProperty: null, 325 | result: {}, 326 | options: this._options 327 | }; 328 | 329 | var stack = [state]; 330 | 331 | function finishLastOption() { 332 | if (state.option) { 333 | 334 | var targetProperty = state.targetProperty; 335 | 336 | // Finish off the previous option... 337 | if (state.result[targetProperty] == null) { 338 | state.result[targetProperty] = true; 339 | } 340 | } 341 | 342 | state.option = null; 343 | } 344 | 345 | var onError = this._onError ? this._onError.bind(this) : DEFAULT_ON_ERROR; 346 | 347 | function addValue(arg) { 348 | var targetProperty = state.targetProperty; 349 | 350 | var option = state.option; 351 | if (!option) { 352 | option = state.option = state.options.get('*'); 353 | } 354 | 355 | if (!option) { 356 | return onError('Illegal argument: "' + arg + '"'); 357 | } 358 | 359 | if (!targetProperty) { 360 | targetProperty = state.targetProperty = getTargetPropertyForOption(option); 361 | } 362 | 363 | 364 | if (option.isArray || targetProperty === '*') { 365 | if (state.result[targetProperty] == null) { 366 | state.result[targetProperty] = []; 367 | } 368 | } 369 | 370 | var existingValue = state.result[targetProperty]; 371 | 372 | if (arg === 'true') { 373 | arg = true; 374 | } else if (arg === 'false') { 375 | arg = false; 376 | } else if (option.type === 'number') { 377 | arg = parseFloat(arg); 378 | } else if (option.type === 'int' || option.type === 'integer') { 379 | arg = parseInt(arg, 10); 380 | } else if (option.type === 'boolean') { 381 | arg = arg.toLowerCase(); 382 | arg = arg === '1' || arg === 'true' || arg === 'yes' || arg === 'y'; 383 | } 384 | 385 | if (existingValue == null) { 386 | state.result[targetProperty] = arg; 387 | } else { 388 | if (Array.isArray(existingValue)) { 389 | existingValue.push(arg); 390 | } else { 391 | state.result[targetProperty] = [existingValue, arg]; 392 | } 393 | } 394 | } 395 | 396 | for (var i = 0, len = args.length; i < len; i++) { 397 | var arg = args[i]; 398 | var argNameMatches; 399 | var option; 400 | 401 | if (arg === '[') { 402 | 403 | var nestedOptions = state.option.options; 404 | if (!nestedOptions) { 405 | nestedOptions = new Options({ 406 | '*': null 407 | }); 408 | } 409 | 410 | state = { 411 | option: null, 412 | targetProperty: null, 413 | result: {}, 414 | options: nestedOptions 415 | }; 416 | 417 | stack.push(state); 418 | } else if (arg === ']') { 419 | finishLastOption(); 420 | 421 | var complexResult = stack[stack.length - 1].result; 422 | 423 | // Restore parser state 424 | stack.pop(); 425 | state = stack[stack.length - 1]; 426 | 427 | addValue(complexResult); 428 | } else if( (argNameMatches = argNameRegExp.exec(arg)) ) { 429 | // We hit either a '--' or '-' arg 430 | finishLastOption(); 431 | 432 | var prefix = argNameMatches[1]; 433 | var no = argNameMatches[2]; 434 | var argName = argNameMatches[3]; 435 | 436 | if (no) { 437 | if (state.options.get(arg)) { 438 | no = false; 439 | } else { 440 | no = true; 441 | arg = prefix + argName; 442 | } 443 | } 444 | 445 | var splitArgs = null; 446 | 447 | 448 | if (prefix === '-') { 449 | /* jshint loopfunc:true */ 450 | splitArgs = argName.split('').map(function(shortArg) { 451 | return { 452 | argName: shortArg, 453 | arg: '-' + shortArg 454 | }; 455 | }); 456 | } else { 457 | splitArgs = [{ 458 | argName: argName, 459 | arg: arg 460 | }]; 461 | } 462 | 463 | for (var j = 0, len2 = splitArgs.length; j < len2; j++) { 464 | arg = splitArgs[j].arg; 465 | argName = splitArgs[j].argName; 466 | option = state.options.get(arg) || state.options.get('--*') || state.options.get('-*'); 467 | 468 | if (!option) { 469 | return onError('Illegal argument: "' + arg + '" (allowed: ' + state.options.getAllowedOptions().join(', ') + ')'); 470 | } 471 | 472 | state.option = option; 473 | 474 | state.targetProperty = getTargetPropertyForOption(option, argName); 475 | 476 | if (splitArgs.length > 1) { 477 | state.result[state.targetProperty] = true; 478 | } else if (no) { 479 | // Prefixed with 'no-' 480 | state.result[state.targetProperty] = false; 481 | } 482 | } 483 | } else { 484 | // This is an argument that is not prefixed with '--' or '-' 485 | addValue(arg); 486 | } 487 | } 488 | 489 | finishLastOption(); 490 | handleDefaults(this, state, onError); 491 | 492 | // Run the validators 493 | try { 494 | for (i = 0, len = this._validators.length; i < len; i++) { 495 | this._validators[i].call(this, state.result, this); 496 | } 497 | } catch (e) { 498 | onError(e); 499 | } 500 | 501 | return state.result; 502 | } 503 | }; 504 | 505 | module.exports = Parser; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var Parser = require('./Parser'); 2 | 3 | exports.createParser = function(options) { 4 | var parser = new Parser(); 5 | if (options) { 6 | parser.options(options); 7 | } 8 | return parser; 9 | }; -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "jasmine", 4 | "spyOn", 5 | "it", 6 | "xit", 7 | "console", 8 | "describe", 9 | "xdescribe", 10 | "beforeEach", 11 | "before", 12 | "after", 13 | "waits", 14 | "waitsFor", 15 | "runs", 16 | "raptor", 17 | "$rset", 18 | "$radd", 19 | "$rget", 20 | "$renv", 21 | "$rwidgets", 22 | "$", 23 | "dust", 24 | "__rhinoHelpers", 25 | "Packages", 26 | "JavaAdapter", 27 | "unescape" 28 | ], 29 | 30 | "globals": { 31 | "define": true, 32 | "require": true 33 | }, 34 | 35 | "node" : true, 36 | "es5" : false, 37 | "browser" : true, 38 | "boss" : false, 39 | "curly": false, 40 | "debug": false, 41 | "devel": false, 42 | "eqeqeq": true, 43 | "evil": true, 44 | "forin": false, 45 | "immed": true, 46 | "laxbreak": false, 47 | "newcap": true, 48 | "noarg": true, 49 | "noempty": false, 50 | "nonew": true, 51 | "nomen": false, 52 | "onevar": false, 53 | "plusplus": false, 54 | "regexp": false, 55 | "undef": true, 56 | "sub": false, 57 | "white": false, 58 | "eqeqeq": false, 59 | "latedef": true, 60 | "unused": "vars", 61 | 62 | /* Relaxing options: */ 63 | "eqnull": true 64 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chai = require('chai'); 3 | chai.Assertion.includeStack = true; 4 | require('chai').should(); 5 | var expect = require('chai').expect; 6 | 7 | 8 | // var parser = require('../') 9 | // .createParser() 10 | // .usage('Usage: $0 [dependency1, dependency2, ...] [OPTIONS]') 11 | // .example('CSS minification', '$0 style.less --transform optimizer-minify-css') 12 | // .example('CSS and JavaScript minification', '$0 hello.js style.less --transforms optimizer-minify-css optimizer-minify-js') 13 | // .example('Plugin with config', '$0 hello.js style.less --plugins [ my-plugin -x 2 -y true ]') 14 | // .options({ 15 | // '--dependencies --dependency -d *': 'string[]', 16 | // '--transforms --transform -t': 'string[]', 17 | // '--plugins --plugin -p': { 18 | // type: '[]', 19 | // options: { 20 | // '--module -m *': null, 21 | // '-*': null 22 | // } 23 | // } 24 | // }); 25 | 26 | describe('raptor-args' , function() { 27 | 28 | beforeEach(function(done) { 29 | done(); 30 | }); 31 | 32 | it('should allow for combination of default and boolean', function() { 33 | var parser = require('../') 34 | .createParser({ 35 | '--dependencies --dependency -d *': 'string[]', 36 | '--minify -m': 'boolean' 37 | }); 38 | 39 | // console.log('PARSER: ', require('util').inspect(parser, {depth: 100})); 40 | 41 | 42 | var parsed = parser.parse('foo bar --minify -d hello world'.split(/\s/)); 43 | 44 | expect(parsed.dependencies).to.deep.equal([ 45 | 'foo', 'bar', 'hello', 'world' 46 | ]); 47 | expect(parsed.minify).to.equal(true); 48 | }); 49 | 50 | it('should allow for boolean', function() { 51 | var parser = require('../') 52 | .createParser({ 53 | '--dependencies --dependency -d *': 'string[]', 54 | '--minify -m': 'boolean' 55 | }); 56 | 57 | var parsed = parser.parse('--minify'.split(/\s/)); 58 | 59 | expect(parsed.dependencies).to.equal(undefined); 60 | expect(parsed.minify).to.equal(true); 61 | }); 62 | 63 | it('should allow for boolean with explicit value', function() { 64 | var parser = require('../') 65 | .createParser({ 66 | '--dependencies --dependency -d *': 'string[]', 67 | '--minify -m': 'boolean' 68 | }); 69 | 70 | var parsed = parser.parse('--minify false'.split(/\s/)); 71 | 72 | expect(parsed.dependencies).to.equal(undefined); 73 | expect(parsed.minify).to.equal(false); 74 | }); 75 | 76 | it('should allow for negated boolean', function() { 77 | var parser = require('../') 78 | .createParser({ 79 | '--dependencies --dependency -d *': 'string[]', 80 | '--minify -m': 'boolean' 81 | }); 82 | 83 | var parsed = parser.parse('--no-minify'.split(/\s/)); 84 | 85 | expect(parsed.dependencies).to.equal(undefined); 86 | expect(parsed.minify).to.equal(false); 87 | }); 88 | 89 | it('should allow for dynamic args', function() { 90 | var parser = require('../') 91 | .createParser({ 92 | '*': 'string', 93 | '-*': 'string', 94 | '--minify -m': 'boolean' 95 | }); 96 | 97 | var parsed = parser.parse('foo --minify -f --hello world --no-bar --array 1 2'.split(/\s/)); 98 | 99 | expect(parsed['*']).to.deep.equal(['foo']); 100 | expect(parsed.minify).to.equal(true); 101 | expect(parsed.f).to.equal(true); 102 | expect(parsed.hello).to.equal('world'); 103 | expect(parsed.bar).to.equal(false); 104 | expect(parsed.array).to.deep.equal(['1', '2']); 105 | }); 106 | 107 | it('should allow for combined short args', function() { 108 | var parser = require('../') 109 | .createParser({ 110 | 111 | '--foo -f': 'boolean', 112 | '--bar -b': 'boolean', 113 | '--baz -z': 'boolean', 114 | '--hello -h': 'boolean' 115 | }); 116 | 117 | var parsed = parser.parse('-fbz'.split(/\s/)); 118 | 119 | // console.log('parsed: ', require('util').inspect(parsed, {depth: 100})); 120 | 121 | expect(parsed).to.deep.equal({ 122 | foo: true, 123 | bar: true, 124 | baz: true 125 | }); 126 | }); 127 | 128 | it('should allow for complex args', function() { 129 | var parser = require('../') 130 | .createParser({ 131 | '--minify -m': 'boolean', 132 | '--hello': 'string', 133 | '--plugins': { 134 | options: { 135 | '--module -m *': 'string', 136 | '-*': null 137 | } 138 | } 139 | }); 140 | 141 | var parsed = parser.parse('--minify --plugins [ foo -x -y ] [ bar -z hello ] --hello world'.split(/\s/)); 142 | 143 | // console.log('parsed: ', require('util').inspect(parsed, {depth: 100})); 144 | 145 | expect(parsed.minify).to.equal(true); 146 | expect(parsed.hello).to.equal('world'); 147 | 148 | expect(parsed.plugins).to.deep.equal([ 149 | { 150 | module: 'foo', 151 | x: true, 152 | y: true 153 | }, 154 | { 155 | module: 'bar', 156 | z: 'hello' 157 | } 158 | ]); 159 | }); 160 | 161 | it('should allow for empty string', function() { 162 | var parser = require('../') 163 | .createParser({ 164 | '--hello -h': 'string' 165 | }); 166 | 167 | var parsed = parser.parse(['--hello', '']); 168 | 169 | // console.log('parsed: ', require('util').inspect(parsed, {depth: 100})); 170 | 171 | expect(parsed).to.deep.equal({ 172 | hello: '' 173 | }); 174 | }); 175 | 176 | it('should remove dashes', function() { 177 | var parser = require('../') 178 | .createParser({ 179 | '--hello-world': 'string' 180 | }); 181 | 182 | var parsed = parser.parse(['--hello-world', 'foo']); 183 | 184 | // console.log('parsed: ', require('util').inspect(parsed, {depth: 100})); 185 | 186 | expect(parsed).to.deep.equal({ 187 | helloWorld: 'foo' 188 | }); 189 | }); 190 | 191 | it('should allow argument definition to start with "no-" prefix', function() { 192 | var parser = require('../') 193 | .createParser({ 194 | '--no-conflict': 'string' 195 | }); 196 | 197 | var parsed = parser.parse('--no-conflict myapp'.split(/\s/)); 198 | 199 | expect(parsed.noConflict).to.equal('myapp'); 200 | }); 201 | 202 | it('should allow default values', function() { 203 | var parser = require('../') 204 | .createParser({ 205 | '--foo -f': { 206 | type: 'string', 207 | defaultValue: 'bar' 208 | }, 209 | '--bar -b': { 210 | type: 'int', 211 | defaultValue: 5 212 | }, 213 | '--hello -h': { 214 | type: 'string', 215 | defaultValue: 'hello world' 216 | } 217 | }); 218 | 219 | var parsed = parser.parse('--foo bor'.split(/\s/)); 220 | 221 | expect(parsed).to.deep.equal({ 222 | foo: 'bor', 223 | bar: 5, 224 | hello: 'hello world' 225 | }); 226 | }); 227 | 228 | it('should allow default values as functions', function() { 229 | var parser = require('../') 230 | .createParser({ 231 | '--foo -f': { 232 | type: 'string', 233 | defaultValue: function() { 234 | return 'bar'; 235 | } 236 | }, 237 | '--bar -b': { 238 | type: 'string' 239 | } 240 | }); 241 | 242 | var parsed = parser.parse('--bar test'.split(/\s/)); 243 | 244 | expect(parsed).to.deep.equal({ 245 | foo: 'bar', 246 | bar: 'test' 247 | }); 248 | }); 249 | 250 | it('should call defaultValue function with Parser context', function() { 251 | var usage = 'Usage: test'; 252 | var parser = require('../') 253 | .createParser({ 254 | '--foo -f': { 255 | type: 'string', 256 | defaultValue: function() { 257 | expect(this.getUsageString().indexOf(usage)).to.not.equal(-1); 258 | return 'bar'; 259 | } 260 | }, 261 | '--bar -b': { 262 | type: 'string' 263 | } 264 | }) 265 | .usage(usage); 266 | 267 | var parsed = parser.parse('--bar test'.split(/\s/)); 268 | 269 | expect(parsed).to.deep.equal({ 270 | foo: 'bar', 271 | bar: 'test' 272 | }); 273 | }); 274 | 275 | it('should allow default values use pseudo defaults', function() { 276 | var parser = require('../') 277 | .createParser({ 278 | '--foo -f': { 279 | type: 'string' 280 | }, 281 | '--test -t': { 282 | type: 'boolean', 283 | defaultValue: 'y' 284 | } 285 | }); 286 | 287 | var parsed = parser.parse('--foo bar'.split(/\s/)); 288 | 289 | expect(parsed).to.deep.equal({ 290 | foo: 'bar', 291 | test: true 292 | }); 293 | }); 294 | 295 | it('should receive error when default value is incorrect type', function() { 296 | var parser = require('../') 297 | .createParser({ 298 | '--foo -f': { 299 | type: 'string', 300 | }, 301 | '--bar -b': { 302 | type: 'int', 303 | defaultValue: true 304 | } 305 | }); 306 | 307 | expect(function() { 308 | parser.parse('--foo bor'.split(/\s/)); 309 | }).to.throw(/Invalid default value \'true\' for target property \'bar\'/); 310 | }); 311 | 312 | it('should allow default value using complex types', function() { 313 | var parser = require('../') 314 | .createParser({ 315 | '--minify -m': 'boolean', 316 | '--hello': 'string', 317 | '--plugins': { 318 | options: { 319 | '--module -m *': 'string', 320 | '-*': null 321 | }, 322 | defaultValue: [ 323 | { 324 | module: 'foo', 325 | x: true, 326 | y: true 327 | }, 328 | { 329 | module: 'bar', 330 | z: 'hello' 331 | } 332 | ] 333 | } 334 | }); 335 | 336 | var parsed = parser.parse('--minify --plugins [ foo -x -y ] [ bar -z hello ] --hello world'.split(/\s/)); 337 | 338 | expect(parsed).to.deep.equal({ 339 | minify: true, 340 | hello: 'world', 341 | plugins: [ 342 | { 343 | module: 'foo', 344 | x: true, 345 | y: true 346 | }, 347 | { 348 | module: 'bar', 349 | z: 'hello' 350 | } 351 | ] 352 | }); 353 | }); 354 | 355 | it('should throw error if repeated option provided in parser', function() { 356 | expect(function() { 357 | require('../') 358 | .createParser({ 359 | '--minify -m': 'boolean', 360 | '--minify -t': 'boolean' 361 | }); 362 | }).to.throw(/Duplicate option provided \'--minify\'/); 363 | }); 364 | 365 | it('should throw error if repeated alias provided in parser', function() { 366 | expect(function() { 367 | require('../') 368 | .createParser({ 369 | '--minify -m': 'boolean', 370 | '--test -m': 'boolean' 371 | }); 372 | }).to.throw(/Duplicate option provided \'-m\'/); 373 | }); 374 | }); 375 | --------------------------------------------------------------------------------