• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  , osenv = require("osenv")
12
13module.exports = exports = nopt
14exports.clean = clean
15
16exports.typeDefs =
17  { String  : { type: String,  validate: validateString  }
18  , Boolean : { type: Boolean, validate: validateBoolean }
19  , url     : { type: url,     validate: validateUrl     }
20  , Number  : { type: Number,  validate: validateNumber  }
21  , path    : { type: path,    validate: validatePath    }
22  , Stream  : { type: Stream,  validate: validateStream  }
23  , Date    : { type: Date,    validate: validateDate    }
24  }
25
26function nopt (types, shorthands, args, slice) {
27  args = args || process.argv
28  types = types || {}
29  shorthands = shorthands || {}
30  if (typeof slice !== "number") slice = 2
31
32  debug(types, shorthands, args, slice)
33
34  args = args.slice(slice)
35  var data = {}
36    , key
37    , argv = {
38        remain: [],
39        cooked: args,
40        original: args.slice(0)
41      }
42
43  parse(args, data, argv.remain, types, shorthands)
44  // now data is full
45  clean(data, types, exports.typeDefs)
46  data.argv = argv
47  Object.defineProperty(data.argv, 'toString', { value: function () {
48    return this.original.map(JSON.stringify).join(" ")
49  }, enumerable: false })
50  return data
51}
52
53function clean (data, types, typeDefs) {
54  typeDefs = typeDefs || exports.typeDefs
55  var remove = {}
56    , typeDefault = [false, true, null, String, Array]
57
58  Object.keys(data).forEach(function (k) {
59    if (k === "argv") return
60    var val = data[k]
61      , isArray = Array.isArray(val)
62      , type = types[k]
63    if (!isArray) val = [val]
64    if (!type) type = typeDefault
65    if (type === Array) type = typeDefault.concat(Array)
66    if (!Array.isArray(type)) type = [type]
67
68    debug("val=%j", val)
69    debug("types=", type)
70    val = val.map(function (val) {
71      // if it's an unknown value, then parse false/true/null/numbers/dates
72      if (typeof val === "string") {
73        debug("string %j", val)
74        val = val.trim()
75        if ((val === "null" && ~type.indexOf(null))
76            || (val === "true" &&
77               (~type.indexOf(true) || ~type.indexOf(Boolean)))
78            || (val === "false" &&
79               (~type.indexOf(false) || ~type.indexOf(Boolean)))) {
80          val = JSON.parse(val)
81          debug("jsonable %j", val)
82        } else if (~type.indexOf(Number) && !isNaN(val)) {
83          debug("convert to number", val)
84          val = +val
85        } else if (~type.indexOf(Date) && !isNaN(Date.parse(val))) {
86          debug("convert to date", val)
87          val = new Date(val)
88        }
89      }
90
91      if (!types.hasOwnProperty(k)) {
92        return val
93      }
94
95      // allow `--no-blah` to set 'blah' to null if null is allowed
96      if (val === false && ~type.indexOf(null) &&
97          !(~type.indexOf(false) || ~type.indexOf(Boolean))) {
98        val = null
99      }
100
101      var d = {}
102      d[k] = val
103      debug("prevalidated val", d, val, types[k])
104      if (!validate(d, k, val, types[k], typeDefs)) {
105        if (exports.invalidHandler) {
106          exports.invalidHandler(k, val, types[k], data)
107        } else if (exports.invalidHandler !== false) {
108          debug("invalid: "+k+"="+val, types[k])
109        }
110        return remove
111      }
112      debug("validated val", d, val, types[k])
113      return d[k]
114    }).filter(function (val) { return val !== remove })
115
116    // if we allow Array specifically, then an empty array is how we
117    // express 'no value here', not null.  Allow it.
118    if (!val.length && type.indexOf(Array) === -1) {
119      debug('VAL HAS NO LENGTH, DELETE IT', val, k, type.indexOf(Array))
120      delete data[k]
121    }
122    else if (isArray) {
123      debug(isArray, data[k], val)
124      data[k] = val
125    } else data[k] = val[0]
126
127    debug("k=%s val=%j", k, val, data[k])
128  })
129}
130
131function validateString (data, k, val) {
132  data[k] = String(val)
133}
134
135function validatePath (data, k, val) {
136  if (val === true) return false
137  if (val === null) return true
138
139  val = String(val)
140
141  var isWin       = process.platform === 'win32'
142    , homePattern = isWin ? /^~(\/|\\)/ : /^~\//
143    , home        = osenv.home()
144
145  if (home && val.match(homePattern)) {
146    data[k] = path.resolve(home, val.substr(2))
147  } else {
148    data[k] = path.resolve(val)
149  }
150  return true
151}
152
153function validateNumber (data, k, val) {
154  debug("validate Number %j %j %j", k, val, isNaN(val))
155  if (isNaN(val)) return false
156  data[k] = +val
157}
158
159function validateDate (data, k, val) {
160  var s = Date.parse(val)
161  debug("validate Date %j %j %j", k, val, s)
162  if (isNaN(s)) return false
163  data[k] = new Date(val)
164}
165
166function validateBoolean (data, k, val) {
167  if (val instanceof Boolean) val = val.valueOf()
168  else if (typeof val === "string") {
169    if (!isNaN(val)) val = !!(+val)
170    else if (val === "null" || val === "false") val = false
171    else val = true
172  } else val = !!val
173  data[k] = val
174}
175
176function validateUrl (data, k, val) {
177  val = url.parse(String(val))
178  if (!val.host) return false
179  data[k] = val.href
180}
181
182function validateStream (data, k, val) {
183  if (!(val instanceof Stream)) return false
184  data[k] = val
185}
186
187function validate (data, k, val, type, typeDefs) {
188  // arrays are lists of types.
189  if (Array.isArray(type)) {
190    for (var i = 0, l = type.length; i < l; i ++) {
191      if (type[i] === Array) continue
192      if (validate(data, k, val, type[i], typeDefs)) return true
193    }
194    delete data[k]
195    return false
196  }
197
198  // an array of anything?
199  if (type === Array) return true
200
201  // NaN is poisonous.  Means that something is not allowed.
202  if (type !== type) {
203    debug("Poison NaN", k, val, type)
204    delete data[k]
205    return false
206  }
207
208  // explicit list of values
209  if (val === type) {
210    debug("Explicitly allowed %j", val)
211    // if (isArray) (data[k] = data[k] || []).push(val)
212    // else data[k] = val
213    data[k] = val
214    return true
215  }
216
217  // now go through the list of typeDefs, validate against each one.
218  var ok = false
219    , types = Object.keys(typeDefs)
220  for (var i = 0, l = types.length; i < l; i ++) {
221    debug("test type %j %j %j", k, val, types[i])
222    var t = typeDefs[types[i]]
223    if (t &&
224      ((type && type.name && t.type && t.type.name) ? (type.name === t.type.name) : (type === t.type))) {
225      var d = {}
226      ok = false !== t.validate(d, k, val)
227      val = d[k]
228      if (ok) {
229        // if (isArray) (data[k] = data[k] || []).push(val)
230        // else data[k] = val
231        data[k] = val
232        break
233      }
234    }
235  }
236  debug("OK? %j (%j %j %j)", ok, k, val, types[i])
237
238  if (!ok) delete data[k]
239  return ok
240}
241
242function parse (args, data, remain, types, shorthands) {
243  debug("parse", args, data, remain)
244
245  var key = null
246    , abbrevs = abbrev(Object.keys(types))
247    , shortAbbr = abbrev(Object.keys(shorthands))
248
249  for (var i = 0; i < args.length; i ++) {
250    var arg = args[i]
251    debug("arg", arg)
252
253    if (arg.match(/^-{2,}$/)) {
254      // done with keys.
255      // the rest are args.
256      remain.push.apply(remain, args.slice(i + 1))
257      args[i] = "--"
258      break
259    }
260    var hadEq = false
261    if (arg.charAt(0) === "-" && arg.length > 1) {
262      var at = arg.indexOf('=')
263      if (at > -1) {
264        hadEq = true
265        var v = arg.substr(at + 1)
266        arg = arg.substr(0, at)
267        args.splice(i, 1, arg, v)
268      }
269
270      // see if it's a shorthand
271      // if so, splice and back up to re-parse it.
272      var shRes = resolveShort(arg, shorthands, shortAbbr, abbrevs)
273      debug("arg=%j shRes=%j", arg, shRes)
274      if (shRes) {
275        debug(arg, shRes)
276        args.splice.apply(args, [i, 1].concat(shRes))
277        if (arg !== shRes[0]) {
278          i --
279          continue
280        }
281      }
282      arg = arg.replace(/^-+/, "")
283      var no = null
284      while (arg.toLowerCase().indexOf("no-") === 0) {
285        no = !no
286        arg = arg.substr(3)
287      }
288
289      if (abbrevs[arg]) arg = abbrevs[arg]
290
291      var argType = types[arg]
292      var isTypeArray = Array.isArray(argType)
293      if (isTypeArray && argType.length === 1) {
294        isTypeArray = false
295        argType = argType[0]
296      }
297
298      var isArray = argType === Array ||
299        isTypeArray && argType.indexOf(Array) !== -1
300
301      // allow unknown things to be arrays if specified multiple times.
302      if (!types.hasOwnProperty(arg) && data.hasOwnProperty(arg)) {
303        if (!Array.isArray(data[arg]))
304          data[arg] = [data[arg]]
305        isArray = true
306      }
307
308      var val
309        , la = args[i + 1]
310
311      var isBool = typeof no === 'boolean' ||
312        argType === Boolean ||
313        isTypeArray && argType.indexOf(Boolean) !== -1 ||
314        (typeof argType === 'undefined' && !hadEq) ||
315        (la === "false" &&
316         (argType === null ||
317          isTypeArray && ~argType.indexOf(null)))
318
319      if (isBool) {
320        // just set and move along
321        val = !no
322        // however, also support --bool true or --bool false
323        if (la === "true" || la === "false") {
324          val = JSON.parse(la)
325          la = null
326          if (no) val = !val
327          i ++
328        }
329
330        // also support "foo":[Boolean, "bar"] and "--foo bar"
331        if (isTypeArray && la) {
332          if (~argType.indexOf(la)) {
333            // an explicit type
334            val = la
335            i ++
336          } else if ( la === "null" && ~argType.indexOf(null) ) {
337            // null allowed
338            val = null
339            i ++
340          } else if ( !la.match(/^-{2,}[^-]/) &&
341                      !isNaN(la) &&
342                      ~argType.indexOf(Number) ) {
343            // number
344            val = +la
345            i ++
346          } else if ( !la.match(/^-[^-]/) && ~argType.indexOf(String) ) {
347            // string
348            val = la
349            i ++
350          }
351        }
352
353        if (isArray) (data[arg] = data[arg] || []).push(val)
354        else data[arg] = val
355
356        continue
357      }
358
359      if (argType === String) {
360        if (la === undefined) {
361          la = ""
362        } else if (la.match(/^-{1,2}[^-]+/)) {
363          la = ""
364          i --
365        }
366      }
367
368      if (la && la.match(/^-{2,}$/)) {
369        la = undefined
370        i --
371      }
372
373      val = la === undefined ? true : la
374      if (isArray) (data[arg] = data[arg] || []).push(val)
375      else data[arg] = val
376
377      i ++
378      continue
379    }
380    remain.push(arg)
381  }
382}
383
384function resolveShort (arg, shorthands, shortAbbr, abbrevs) {
385  // handle single-char shorthands glommed together, like
386  // npm ls -glp, but only if there is one dash, and only if
387  // all of the chars are single-char shorthands, and it's
388  // not a match to some other abbrev.
389  arg = arg.replace(/^-+/, '')
390
391  // if it's an exact known option, then don't go any further
392  if (abbrevs[arg] === arg)
393    return null
394
395  // if it's an exact known shortopt, same deal
396  if (shorthands[arg]) {
397    // make it an array, if it's a list of words
398    if (shorthands[arg] && !Array.isArray(shorthands[arg]))
399      shorthands[arg] = shorthands[arg].split(/\s+/)
400
401    return shorthands[arg]
402  }
403
404  // first check to see if this arg is a set of single-char shorthands
405  var singles = shorthands.___singles
406  if (!singles) {
407    singles = Object.keys(shorthands).filter(function (s) {
408      return s.length === 1
409    }).reduce(function (l,r) {
410      l[r] = true
411      return l
412    }, {})
413    shorthands.___singles = singles
414    debug('shorthand singles', singles)
415  }
416
417  var chrs = arg.split("").filter(function (c) {
418    return singles[c]
419  })
420
421  if (chrs.join("") === arg) return chrs.map(function (c) {
422    return shorthands[c]
423  }).reduce(function (l, r) {
424    return l.concat(r)
425  }, [])
426
427
428  // if it's an arg abbrev, and not a literal shorthand, then prefer the arg
429  if (abbrevs[arg] && !shorthands[arg])
430    return null
431
432  // if it's an abbr for a shorthand, then use that
433  if (shortAbbr[arg])
434    arg = shortAbbr[arg]
435
436  // make it an array, if it's a list of words
437  if (shorthands[arg] && !Array.isArray(shorthands[arg]))
438    shorthands[arg] = shorthands[arg].split(/\s+/)
439
440  return shorthands[arg]
441}
442