1 // info about each config option.
3 var debug = process.env.DEBUG_NOPT || process.env.NOPT_DEBUG
4 ? function () { console.error.apply(console, arguments) }
7 var url = require("url")
8 , path = require("path")
9 , Stream = require("stream").Stream
10 , abbrev = require("./abbrev")
12 module.exports = exports = nopt
16 { String : { type: String, validate: validateString }
17 , Boolean : { type: Boolean, validate: validateBoolean }
18 , url : { type: url, validate: validateUrl }
19 , Number : { type: Number, validate: validateNumber }
20 , path : { type: path, validate: validatePath }
21 , Stream : { type: Stream, validate: validateStream }
22 , Date : { type: Date, validate: validateDate }
25 function nopt (types, shorthands, args, slice) {
26 args = args || process.argv
28 shorthands = shorthands || {}
29 if (typeof slice !== "number") slice = 2
31 debug(types, shorthands, args, slice)
33 args = args.slice(slice)
38 , original = args.slice(0)
40 parse(args, data, remain, types, shorthands)
42 clean(data, types, exports.typeDefs)
43 data.argv = {remain:remain,cooked:cooked,original:original}
44 data.argv.toString = function () {
45 return this.original.map(JSON.stringify).join(" ")
50 function clean (data, types, typeDefs) {
51 typeDefs = typeDefs || exports.typeDefs
53 , typeDefault = [false, true, null, String, Number]
55 Object.keys(data).forEach(function (k) {
56 if (k === "argv") return
58 , isArray = Array.isArray(val)
60 if (!isArray) val = [val]
61 if (!type) type = typeDefault
62 if (type === Array) type = typeDefault.concat(Array)
63 if (!Array.isArray(type)) type = [type]
67 val = val.map(function (val) {
68 // if it's an unknown value, then parse false/true/null/numbers/dates
69 if (typeof val === "string") {
70 debug("string %j", val)
72 if ((val === "null" && ~type.indexOf(null))
74 (~type.indexOf(true) || ~type.indexOf(Boolean)))
75 || (val === "false" &&
76 (~type.indexOf(false) || ~type.indexOf(Boolean)))) {
78 debug("jsonable %j", val)
79 } else if (~type.indexOf(Number) && !isNaN(val)) {
80 debug("convert to number", val)
82 } else if (~type.indexOf(Date) && !isNaN(Date.parse(val))) {
83 debug("convert to date", val)
88 if (!types.hasOwnProperty(k)) {
92 // allow `--no-blah` to set 'blah' to null if null is allowed
93 if (val === false && ~type.indexOf(null) &&
94 !(~type.indexOf(false) || ~type.indexOf(Boolean))) {
100 debug("prevalidated val", d, val, types[k])
101 if (!validate(d, k, val, types[k], typeDefs)) {
102 if (exports.invalidHandler) {
103 exports.invalidHandler(k, val, types[k], data)
104 } else if (exports.invalidHandler !== false) {
105 debug("invalid: "+k+"="+val, types[k])
109 debug("validated val", d, val, types[k])
111 }).filter(function (val) { return val !== remove })
113 if (!val.length) delete data[k]
115 debug(isArray, data[k], val)
117 } else data[k] = val[0]
119 debug("k=%s val=%j", k, val, data[k])
123 function validateString (data, k, val) {
124 data[k] = String(val)
127 function validatePath (data, k, val) {
128 data[k] = path.resolve(String(val))
132 function validateNumber (data, k, val) {
133 debug("validate Number %j %j %j", k, val, isNaN(val))
134 if (isNaN(val)) return false
138 function validateDate (data, k, val) {
139 debug("validate Date %j %j %j", k, val, Date.parse(val))
140 var s = Date.parse(val)
141 if (isNaN(s)) return false
142 data[k] = new Date(val)
145 function validateBoolean (data, k, val) {
146 if (val instanceof Boolean) val = val.valueOf()
147 else if (typeof val === "string") {
148 if (!isNaN(val)) val = !!(+val)
149 else if (val === "null" || val === "false") val = false
155 function validateUrl (data, k, val) {
156 val = url.parse(String(val))
157 if (!val.host) return false
161 function validateStream (data, k, val) {
162 if (!(val instanceof Stream)) return false
166 function validate (data, k, val, type, typeDefs) {
167 // arrays are lists of types.
168 if (Array.isArray(type)) {
169 for (var i = 0, l = type.length; i < l; i ++) {
170 if (type[i] === Array) continue
171 if (validate(data, k, val, type[i], typeDefs)) return true
177 // an array of anything?
178 if (type === Array) return true
180 // NaN is poisonous. Means that something is not allowed.
182 debug("Poison NaN", k, val, type)
187 // explicit list of values
189 debug("Explicitly allowed %j", val)
190 // if (isArray) (data[k] = data[k] || []).push(val)
191 // else data[k] = val
196 // now go through the list of typeDefs, validate against each one.
198 , types = Object.keys(typeDefs)
199 for (var i = 0, l = types.length; i < l; i ++) {
200 debug("test type %j %j %j", k, val, types[i])
201 var t = typeDefs[types[i]]
202 if (t && type === t.type) {
204 ok = false !== t.validate(d, k, val)
207 // if (isArray) (data[k] = data[k] || []).push(val)
208 // else data[k] = val
214 debug("OK? %j (%j %j %j)", ok, k, val, types[i])
216 if (!ok) delete data[k]
220 function parse (args, data, remain, types, shorthands) {
221 debug("parse", args, data, remain)
224 , abbrevs = abbrev(Object.keys(types))
225 , shortAbbr = abbrev(Object.keys(shorthands))
227 for (var i = 0; i < args.length; i ++) {
231 if (arg.match(/^-{2,}$/)) {
233 // the rest are args.
234 remain.push.apply(remain, args.slice(i + 1))
238 if (arg.charAt(0) === "-") {
239 if (arg.indexOf("=") !== -1) {
240 var v = arg.split("=")
243 args.splice.apply(args, [i, 1].concat([arg, v]))
245 // see if it's a shorthand
246 // if so, splice and back up to re-parse it.
247 var shRes = resolveShort(arg, shorthands, shortAbbr, abbrevs)
248 debug("arg=%j shRes=%j", arg, shRes)
251 args.splice.apply(args, [i, 1].concat(shRes))
252 if (arg !== shRes[0]) {
257 arg = arg.replace(/^-+/, "")
259 while (arg.toLowerCase().indexOf("no-") === 0) {
264 if (abbrevs[arg]) arg = abbrevs[arg]
266 var isArray = types[arg] === Array ||
267 Array.isArray(types[arg]) && types[arg].indexOf(Array) !== -1
273 types[arg] === Boolean ||
274 Array.isArray(types[arg]) && types[arg].indexOf(Boolean) !== -1 ||
276 (types[arg] === null ||
277 Array.isArray(types[arg]) && ~types[arg].indexOf(null)))
280 // just set and move along
282 // however, also support --bool true or --bool false
283 if (la === "true" || la === "false") {
290 // also support "foo":[Boolean, "bar"] and "--foo bar"
291 if (Array.isArray(types[arg]) && la) {
292 if (~types[arg].indexOf(la)) {
296 } else if ( la === "null" && ~types[arg].indexOf(null) ) {
300 } else if ( !la.match(/^-{2,}[^-]/) &&
302 ~types[arg].indexOf(Number) ) {
306 } else if ( !la.match(/^-[^-]/) && ~types[arg].indexOf(String) ) {
313 if (isArray) (data[arg] = data[arg] || []).push(val)
319 if (la && la.match(/^-{2,}$/)) {
324 val = la === undefined ? true : la
325 if (isArray) (data[arg] = data[arg] || []).push(val)
335 function resolveShort (arg, shorthands, shortAbbr, abbrevs) {
336 // handle single-char shorthands glommed together, like
337 // npm ls -glp, but only if there is one dash, and only if
338 // all of the chars are single-char shorthands, and it's
339 // not a match to some other abbrev.
340 arg = arg.replace(/^-+/, '')
341 if (abbrevs[arg] && !shorthands[arg]) {
344 if (shortAbbr[arg]) {
347 var singles = shorthands.___singles
349 singles = Object.keys(shorthands).filter(function (s) {
350 return s.length === 1
351 }).reduce(function (l,r) { l[r] = true ; return l }, {})
352 shorthands.___singles = singles
354 var chrs = arg.split("").filter(function (c) {
357 if (chrs.join("") === arg) return chrs.map(function (c) {
359 }).reduce(function (l, r) {
364 if (shorthands[arg] && !Array.isArray(shorthands[arg])) {
365 shorthands[arg] = shorthands[arg].split(/\s+/)
367 return shorthands[arg]
370 if (module === require.main) {
371 var assert = require("assert")
372 , util = require("util")
375 { s : ["--loglevel", "silent"]
376 , d : ["--loglevel", "info"]
377 , dd : ["--loglevel", "verbose"]
378 , ddd : ["--loglevel", "silly"]
379 , noreg : ["--no-registry"]
380 , reg : ["--registry"]
381 , "no-reg" : ["--no-registry"]
382 , silent : ["--loglevel", "silent"]
383 , verbose : ["--loglevel", "verbose"]
390 , desc : ["--description"]
391 , "no-desc" : ["--no-description"]
392 , "local" : ["--no-global"]
394 , p : ["--parseable"]
395 , porcelain : ["--parseable"]
401 , nullstream: [null, Stream]
406 , color : ["always", Boolean]
408 , description : Boolean
413 , globalconfig : path
414 , group : [String, Number]
416 , logfd : [Number, Stream]
417 , loglevel : ["silent","win","error","warn","info","verbose","silly"]
419 , "node-version" : [false, String]
422 , "onload-script" : [false, String]
423 , outfd : [Number, Stream]
424 , parseable : Boolean
428 , "rebuild-bundle" : Boolean
430 , searchopts : String
431 , searchexclude: [null, String]
437 , "unsafe-perm" : Boolean
447 ; [["-v", {version:true}, []]
448 ,["---v", {version:true}, []]
449 ,["ls -s --no-reg connect -d",
450 {loglevel:"info",registry:null},["ls","connect"]]
451 ,["ls ---s foo",{loglevel:"silent"},["ls","foo"]]
452 ,["ls --registry blargle", {}, ["ls"]]
453 ,["--no-registry", {registry:null}, []]
454 ,["--no-color true", {color:false}, []]
455 ,["--no-color false", {color:true}, []]
456 ,["--no-color", {color:false}, []]
457 ,["--color false", {color:false}, []]
458 ,["--color --logfd 7", {logfd:7,color:true}, []]
459 ,["--color=true", {color:true}, []]
460 ,["--logfd=10", {logfd:10}, []]
461 ,["--tmp=/tmp -tar=gtar",{tmp:"/tmp",tar:"gtar"},[]]
462 ,["--tmp=tmp -tar=gtar",
463 {tmp:path.resolve(process.cwd(), "tmp"),tar:"gtar"},[]]
464 ,["--logfd x", {}, []]
465 ,["a -true -- -no-false", {true:true},["a","-no-false"]]
466 ,["a -no-false", {false:false},["a"]]
467 ,["a -no-no-true", {true:true}, ["a"]]
468 ,["a -no-no-no-false", {false:false}, ["a"]]
469 ,["---NO-no-No-no-no-no-nO-no-no"+
470 "-No-no-no-no-no-no-no-no-no"+
471 "-no-no-no-no-NO-NO-no-no-no-no-no-no"+
472 "-no-body-can-do-the-boogaloo-like-I-do"
473 ,{"body-can-do-the-boogaloo-like-I-do":false}, []]
474 ,["we are -no-strangers-to-love "+
475 "--you-know the-rules --and so-do-i "+
476 "---im-thinking-of=a-full-commitment "+
477 "--no-you-would-get-this-from-any-other-guy "+
478 "--no-gonna-give-you-up "+
479 "-no-gonna-let-you-down=true "+
480 "--no-no-gonna-run-around false "+
481 "--desert-you=false "+
482 "--make-you-cry false "+
484 "--no-no-and-hurt-you false"
485 ,{"strangers-to-love":false
486 ,"you-know":"the-rules"
488 ,"you-would-get-this-from-any-other-guy":false
489 ,"gonna-give-you-up":false
490 ,"gonna-let-you-down":false
491 ,"gonna-run-around":false
493 ,"make-you-cry":false
495 ,"and-hurt-you":false
497 ,["-t one -t two -t three"
498 ,{t: ["one", "two", "three"]}
500 ,["-t one -t null -t three four five null"
501 ,{t: ["one", "null", "three"]}
502 ,["four", "five", "null"]]
512 ,["-aoa one -aoa null -aoa 100"
513 ,{aoa:["one", null, 100]}
524 ,["--nullstream false"
527 ,["--notadate 2011-01-25"
528 ,{notadate: "2011-01-25"}
530 ,["--date 2011-01-25"
531 ,{date: new Date("2011-01-25")}
533 ].forEach(function (test) {
534 var argv = test[0].split(/\s+/)
537 , actual = nopt(types, shorthands, argv, 0)
538 , parsed = actual.argv
540 console.log(util.inspect(actual, false, 2, true), parsed.remain)
541 for (var i in opts) {
542 var e = JSON.stringify(opts[i])
543 , a = JSON.stringify(actual[i] === undefined ? null : actual[i])
544 if (e && typeof e === "object") {
545 assert.deepEqual(e, a)
550 assert.deepEqual(rem, parsed.remain)