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