master
Raw Download raw file
  1// info about each config option.
  2
  3var debug = process.env.DEBUG_NOPT || process.env.NOPT_DEBUG
  4  ? function () { console.error.apply(console, arguments) }
  5  : function () {}
  6
  7var url = require("url")
  8  , path = require("path")
  9  , Stream = require("stream").Stream
 10  , abbrev = require("abbrev")
 11
 12module.exports = exports = nopt
 13exports.clean = clean
 14
 15exports.typeDefs =
 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    }
 23  }
 24
 25function nopt (types, shorthands, args, slice) {
 26  args = args || process.argv
 27  types = types || {}
 28  shorthands = shorthands || {}
 29  if (typeof slice !== "number") slice = 2
 30
 31  debug(types, shorthands, args, slice)
 32
 33  args = args.slice(slice)
 34  var data = {}
 35    , key
 36    , remain = []
 37    , cooked = args
 38    , original = args.slice(0)
 39
 40  parse(args, data, remain, types, shorthands)
 41  // now data is full
 42  clean(data, types, exports.typeDefs)
 43  data.argv = {remain:remain,cooked:cooked,original:original}
 44  Object.defineProperty(data.argv, 'toString', { value: function () {
 45    return this.original.map(JSON.stringify).join(" ")
 46  }, enumerable: false })
 47  return data
 48}
 49
 50function clean (data, types, typeDefs) {
 51  typeDefs = typeDefs || exports.typeDefs
 52  var remove = {}
 53    , typeDefault = [false, true, null, String, Number, Array]
 54
 55  Object.keys(data).forEach(function (k) {
 56    if (k === "argv") return
 57    var val = data[k]
 58      , isArray = Array.isArray(val)
 59      , type = types[k]
 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]
 64
 65    debug("val=%j", val)
 66    debug("types=", 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)
 71        val = val.trim()
 72        if ((val === "null" && ~type.indexOf(null))
 73            || (val === "true" &&
 74               (~type.indexOf(true) || ~type.indexOf(Boolean)))
 75            || (val === "false" &&
 76               (~type.indexOf(false) || ~type.indexOf(Boolean)))) {
 77          val = JSON.parse(val)
 78          debug("jsonable %j", val)
 79        } else if (~type.indexOf(Number) && !isNaN(val)) {
 80          debug("convert to number", val)
 81          val = +val
 82        } else if (~type.indexOf(Date) && !isNaN(Date.parse(val))) {
 83          debug("convert to date", val)
 84          val = new Date(val)
 85        }
 86      }
 87
 88      if (!types.hasOwnProperty(k)) {
 89        return val
 90      }
 91
 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))) {
 95        val = null
 96      }
 97
 98      var d = {}
 99      d[k] = val
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])
106        }
107        return remove
108      }
109      debug("validated val", d, val, types[k])
110      return d[k]
111    }).filter(function (val) { return val !== remove })
112
113    if (!val.length) delete data[k]
114    else if (isArray) {
115      debug(isArray, data[k], val)
116      data[k] = val
117    } else data[k] = val[0]
118
119    debug("k=%s val=%j", k, val, data[k])
120  })
121}
122
123function validateString (data, k, val) {
124  data[k] = String(val)
125}
126
127function validatePath (data, k, val) {
128  data[k] = path.resolve(String(val))
129  return true
130}
131
132function validateNumber (data, k, val) {
133  debug("validate Number %j %j %j", k, val, isNaN(val))
134  if (isNaN(val)) return false
135  data[k] = +val
136}
137
138function 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)
143}
144
145function 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
150    else val = true
151  } else val = !!val
152  data[k] = val
153}
154
155function validateUrl (data, k, val) {
156  val = url.parse(String(val))
157  if (!val.host) return false
158  data[k] = val.href
159}
160
161function validateStream (data, k, val) {
162  if (!(val instanceof Stream)) return false
163  data[k] = val
164}
165
166function 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
172    }
173    delete data[k]
174    return false
175  }
176
177  // an array of anything?
178  if (type === Array) return true
179
180  // NaN is poisonous.  Means that something is not allowed.
181  if (type !== type) {
182    debug("Poison NaN", k, val, type)
183    delete data[k]
184    return false
185  }
186
187  // explicit list of values
188  if (val === type) {
189    debug("Explicitly allowed %j", val)
190    // if (isArray) (data[k] = data[k] || []).push(val)
191    // else data[k] = val
192    data[k] = val
193    return true
194  }
195
196  // now go through the list of typeDefs, validate against each one.
197  var ok = false
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) {
203      var d = {}
204      ok = false !== t.validate(d, k, val)
205      val = d[k]
206      if (ok) {
207        // if (isArray) (data[k] = data[k] || []).push(val)
208        // else data[k] = val
209        data[k] = val
210        break
211      }
212    }
213  }
214  debug("OK? %j (%j %j %j)", ok, k, val, types[i])
215
216  if (!ok) delete data[k]
217  return ok
218}
219
220function parse (args, data, remain, types, shorthands) {
221  debug("parse", args, data, remain)
222
223  var key = null
224    , abbrevs = abbrev(Object.keys(types))
225    , shortAbbr = abbrev(Object.keys(shorthands))
226
227  for (var i = 0; i < args.length; i ++) {
228    var arg = args[i]
229    debug("arg", arg)
230
231    if (arg.match(/^-{2,}$/)) {
232      // done with keys.
233      // the rest are args.
234      remain.push.apply(remain, args.slice(i + 1))
235      args[i] = "--"
236      break
237    }
238    var hadEq = false
239    if (arg.charAt(0) === "-" && arg.length > 1) {
240      if (arg.indexOf("=") !== -1) {
241        hadEq = true
242        var v = arg.split("=")
243        arg = v.shift()
244        v = v.join("=")
245        args.splice.apply(args, [i, 1].concat([arg, v]))
246      }
247
248      // see if it's a shorthand
249      // if so, splice and back up to re-parse it.
250      var shRes = resolveShort(arg, shorthands, shortAbbr, abbrevs)
251      debug("arg=%j shRes=%j", arg, shRes)
252      if (shRes) {
253        debug(arg, shRes)
254        args.splice.apply(args, [i, 1].concat(shRes))
255        if (arg !== shRes[0]) {
256          i --
257          continue
258        }
259      }
260      arg = arg.replace(/^-+/, "")
261      var no = null
262      while (arg.toLowerCase().indexOf("no-") === 0) {
263        no = !no
264        arg = arg.substr(3)
265      }
266
267      if (abbrevs[arg]) arg = abbrevs[arg]
268
269      var isArray = types[arg] === Array ||
270        Array.isArray(types[arg]) && types[arg].indexOf(Array) !== -1
271
272      // allow unknown things to be arrays if specified multiple times.
273      if (!types.hasOwnProperty(arg) && data.hasOwnProperty(arg)) {
274        if (!Array.isArray(data[arg]))
275          data[arg] = [data[arg]]
276        isArray = true
277      }
278
279      var val
280        , la = args[i + 1]
281
282      var isBool = typeof no === 'boolean' ||
283        types[arg] === Boolean ||
284        Array.isArray(types[arg]) && types[arg].indexOf(Boolean) !== -1 ||
285        (typeof types[arg] === 'undefined' && !hadEq) ||
286        (la === "false" &&
287         (types[arg] === null ||
288          Array.isArray(types[arg]) && ~types[arg].indexOf(null)))
289
290      if (isBool) {
291        // just set and move along
292        val = !no
293        // however, also support --bool true or --bool false
294        if (la === "true" || la === "false") {
295          val = JSON.parse(la)
296          la = null
297          if (no) val = !val
298          i ++
299        }
300
301        // also support "foo":[Boolean, "bar"] and "--foo bar"
302        if (Array.isArray(types[arg]) && la) {
303          if (~types[arg].indexOf(la)) {
304            // an explicit type
305            val = la
306            i ++
307          } else if ( la === "null" && ~types[arg].indexOf(null) ) {
308            // null allowed
309            val = null
310            i ++
311          } else if ( !la.match(/^-{2,}[^-]/) &&
312                      !isNaN(la) &&
313                      ~types[arg].indexOf(Number) ) {
314            // number
315            val = +la
316            i ++
317          } else if ( !la.match(/^-[^-]/) && ~types[arg].indexOf(String) ) {
318            // string
319            val = la
320            i ++
321          }
322        }
323
324        if (isArray) (data[arg] = data[arg] || []).push(val)
325        else data[arg] = val
326
327        continue
328      }
329
330      if (la && la.match(/^-{2,}$/)) {
331        la = undefined
332        i --
333      }
334
335      val = la === undefined ? true : la
336      if (isArray) (data[arg] = data[arg] || []).push(val)
337      else data[arg] = val
338
339      i ++
340      continue
341    }
342    remain.push(arg)
343  }
344}
345
346function resolveShort (arg, shorthands, shortAbbr, abbrevs) {
347  // handle single-char shorthands glommed together, like
348  // npm ls -glp, but only if there is one dash, and only if
349  // all of the chars are single-char shorthands, and it's
350  // not a match to some other abbrev.
351  arg = arg.replace(/^-+/, '')
352
353  // if it's an exact known option, then don't go any further
354  if (abbrevs[arg] === arg)
355    return null
356
357  // if it's an exact known shortopt, same deal
358  if (shorthands[arg]) {
359    // make it an array, if it's a list of words
360    if (shorthands[arg] && !Array.isArray(shorthands[arg]))
361      shorthands[arg] = shorthands[arg].split(/\s+/)
362
363    return shorthands[arg]
364  }
365
366  // first check to see if this arg is a set of single-char shorthands
367  var singles = shorthands.___singles
368  if (!singles) {
369    singles = Object.keys(shorthands).filter(function (s) {
370      return s.length === 1
371    }).reduce(function (l,r) {
372      l[r] = true
373      return l
374    }, {})
375    shorthands.___singles = singles
376    debug('shorthand singles', singles)
377  }
378
379  var chrs = arg.split("").filter(function (c) {
380    return singles[c]
381  })
382
383  if (chrs.join("") === arg) return chrs.map(function (c) {
384    return shorthands[c]
385  }).reduce(function (l, r) {
386    return l.concat(r)
387  }, [])
388
389
390  // if it's an arg abbrev, and not a literal shorthand, then prefer the arg
391  if (abbrevs[arg] && !shorthands[arg])
392    return null
393
394  // if it's an abbr for a shorthand, then use that
395  if (shortAbbr[arg])
396    arg = shortAbbr[arg]
397
398  // make it an array, if it's a list of words
399  if (shorthands[arg] && !Array.isArray(shorthands[arg]))
400    shorthands[arg] = shorthands[arg].split(/\s+/)
401
402  return shorthands[arg]
403}
404
405if (module === require.main) {
406var assert = require("assert")
407  , util = require("util")
408
409  , shorthands =
410    { s : ["--loglevel", "silent"]
411    , d : ["--loglevel", "info"]
412    , dd : ["--loglevel", "verbose"]
413    , ddd : ["--loglevel", "silly"]
414    , noreg : ["--no-registry"]
415    , reg : ["--registry"]
416    , "no-reg" : ["--no-registry"]
417    , silent : ["--loglevel", "silent"]
418    , verbose : ["--loglevel", "verbose"]
419    , h : ["--usage"]
420    , H : ["--usage"]
421    , "?" : ["--usage"]
422    , help : ["--usage"]
423    , v : ["--version"]
424    , f : ["--force"]
425    , desc : ["--description"]
426    , "no-desc" : ["--no-description"]
427    , "local" : ["--no-global"]
428    , l : ["--long"]
429    , p : ["--parseable"]
430    , porcelain : ["--parseable"]
431    , g : ["--global"]
432    }
433
434  , types =
435    { aoa: Array
436    , nullstream: [null, Stream]
437    , date: Date
438    , str: String
439    , browser : String
440    , cache : path
441    , color : ["always", Boolean]
442    , depth : Number
443    , description : Boolean
444    , dev : Boolean
445    , editor : path
446    , force : Boolean
447    , global : Boolean
448    , globalconfig : path
449    , group : [String, Number]
450    , gzipbin : String
451    , logfd : [Number, Stream]
452    , loglevel : ["silent","win","error","warn","info","verbose","silly"]
453    , long : Boolean
454    , "node-version" : [false, String]
455    , npaturl : url
456    , npat : Boolean
457    , "onload-script" : [false, String]
458    , outfd : [Number, Stream]
459    , parseable : Boolean
460    , pre: Boolean
461    , prefix: path
462    , proxy : url
463    , "rebuild-bundle" : Boolean
464    , registry : url
465    , searchopts : String
466    , searchexclude: [null, String]
467    , shell : path
468    , t: [Array, String]
469    , tag : String
470    , tar : String
471    , tmp : path
472    , "unsafe-perm" : Boolean
473    , usage : Boolean
474    , user : String
475    , username : String
476    , userconfig : path
477    , version : Boolean
478    , viewer: path
479    , _exit : Boolean
480    }
481
482; [["-v", {version:true}, []]
483  ,["---v", {version:true}, []]
484  ,["ls -s --no-reg connect -d",
485    {loglevel:"info",registry:null},["ls","connect"]]
486  ,["ls ---s foo",{loglevel:"silent"},["ls","foo"]]
487  ,["ls --registry blargle", {}, ["ls"]]
488  ,["--no-registry", {registry:null}, []]
489  ,["--no-color true", {color:false}, []]
490  ,["--no-color false", {color:true}, []]
491  ,["--no-color", {color:false}, []]
492  ,["--color false", {color:false}, []]
493  ,["--color --logfd 7", {logfd:7,color:true}, []]
494  ,["--color=true", {color:true}, []]
495  ,["--logfd=10", {logfd:10}, []]
496  ,["--tmp=/tmp -tar=gtar",{tmp:"/tmp",tar:"gtar"},[]]
497  ,["--tmp=tmp -tar=gtar",
498    {tmp:path.resolve(process.cwd(), "tmp"),tar:"gtar"},[]]
499  ,["--logfd x", {}, []]
500  ,["a -true -- -no-false", {true:true},["a","-no-false"]]
501  ,["a -no-false", {false:false},["a"]]
502  ,["a -no-no-true", {true:true}, ["a"]]
503  ,["a -no-no-no-false", {false:false}, ["a"]]
504  ,["---NO-no-No-no-no-no-nO-no-no"+
505    "-No-no-no-no-no-no-no-no-no"+
506    "-no-no-no-no-NO-NO-no-no-no-no-no-no"+
507    "-no-body-can-do-the-boogaloo-like-I-do"
508   ,{"body-can-do-the-boogaloo-like-I-do":false}, []]
509  ,["we are -no-strangers-to-love "+
510    "--you-know=the-rules --and=so-do-i "+
511    "---im-thinking-of=a-full-commitment "+
512    "--no-you-would-get-this-from-any-other-guy "+
513    "--no-gonna-give-you-up "+
514    "-no-gonna-let-you-down=true "+
515    "--no-no-gonna-run-around false "+
516    "--desert-you=false "+
517    "--make-you-cry false "+
518    "--no-tell-a-lie "+
519    "--no-no-and-hurt-you false"
520   ,{"strangers-to-love":false
521    ,"you-know":"the-rules"
522    ,"and":"so-do-i"
523    ,"you-would-get-this-from-any-other-guy":false
524    ,"gonna-give-you-up":false
525    ,"gonna-let-you-down":false
526    ,"gonna-run-around":false
527    ,"desert-you":false
528    ,"make-you-cry":false
529    ,"tell-a-lie":false
530    ,"and-hurt-you":false
531    },["we", "are"]]
532  ,["-t one -t two -t three"
533   ,{t: ["one", "two", "three"]}
534   ,[]]
535  ,["-t one -t null -t three four five null"
536   ,{t: ["one", "null", "three"]}
537   ,["four", "five", "null"]]
538  ,["-t foo"
539   ,{t:["foo"]}
540   ,[]]
541  ,["--no-t"
542   ,{t:["false"]}
543   ,[]]
544  ,["-no-no-t"
545   ,{t:["true"]}
546   ,[]]
547  ,["-aoa one -aoa null -aoa 100"
548   ,{aoa:["one", null, 100]}
549   ,[]]
550  ,["-str 100"
551   ,{str:"100"}
552   ,[]]
553  ,["--color always"
554   ,{color:"always"}
555   ,[]]
556  ,["--no-nullstream"
557   ,{nullstream:null}
558   ,[]]
559  ,["--nullstream false"
560   ,{nullstream:null}
561   ,[]]
562  ,["--notadate=2011-01-25"
563   ,{notadate: "2011-01-25"}
564   ,[]]
565  ,["--date 2011-01-25"
566   ,{date: new Date("2011-01-25")}
567   ,[]]
568  ,["-cl 1"
569   ,{config: true, length: 1}
570   ,[]
571   ,{config: Boolean, length: Number, clear: Boolean}
572   ,{c: "--config", l: "--length"}]
573  ,["--acount bla"
574   ,{"acount":true}
575   ,["bla"]
576   ,{account: Boolean, credentials: Boolean, options: String}
577   ,{a:"--account", c:"--credentials",o:"--options"}]
578  ,["--clear"
579   ,{clear:true}
580   ,[]
581   ,{clear:Boolean,con:Boolean,len:Boolean,exp:Boolean,add:Boolean,rep:Boolean}
582   ,{c:"--con",l:"--len",e:"--exp",a:"--add",r:"--rep"}]
583  ,["--file -"
584   ,{"file":"-"}
585   ,[]
586   ,{file:String}
587   ,{}]
588  ,["--file -"
589   ,{"file":true}
590   ,["-"]
591   ,{file:Boolean}
592   ,{}]
593  ].forEach(function (test) {
594    var argv = test[0].split(/\s+/)
595      , opts = test[1]
596      , rem = test[2]
597      , actual = nopt(test[3] || types, test[4] || shorthands, argv, 0)
598      , parsed = actual.argv
599    delete actual.argv
600    console.log(util.inspect(actual, false, 2, true), parsed.remain)
601    for (var i in opts) {
602      var e = JSON.stringify(opts[i])
603        , a = JSON.stringify(actual[i] === undefined ? null : actual[i])
604      if (e && typeof e === "object") {
605        assert.deepEqual(e, a)
606      } else {
607        assert.equal(e, a)
608      }
609    }
610    assert.deepEqual(rem, parsed.remain)
611  })
612}