nopt.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. // info about each config option.
  2. var debug = process.env.DEBUG_NOPT || process.env.NOPT_DEBUG
  3. ? function () { console.error.apply(console, arguments) }
  4. : function () {}
  5. var url = require("url")
  6. , path = require("path")
  7. , Stream = require("stream").Stream
  8. , abbrev = require("abbrev")
  9. module.exports = exports = nopt
  10. exports.clean = clean
  11. exports.typeDefs =
  12. { String : { type: String, validate: validateString }
  13. , Boolean : { type: Boolean, validate: validateBoolean }
  14. , url : { type: url, validate: validateUrl }
  15. , Number : { type: Number, validate: validateNumber }
  16. , path : { type: path, validate: validatePath }
  17. , Stream : { type: Stream, validate: validateStream }
  18. , Date : { type: Date, validate: validateDate }
  19. }
  20. function nopt (types, shorthands, args, slice) {
  21. args = args || process.argv
  22. types = types || {}
  23. shorthands = shorthands || {}
  24. if (typeof slice !== "number") slice = 2
  25. debug(types, shorthands, args, slice)
  26. args = args.slice(slice)
  27. var data = {}
  28. , key
  29. , remain = []
  30. , cooked = args
  31. , original = args.slice(0)
  32. parse(args, data, remain, types, shorthands)
  33. // now data is full
  34. clean(data, types, exports.typeDefs)
  35. data.argv = {remain:remain,cooked:cooked,original:original}
  36. data.argv.toString = function () {
  37. return this.original.map(JSON.stringify).join(" ")
  38. }
  39. return data
  40. }
  41. function clean (data, types, typeDefs) {
  42. typeDefs = typeDefs || exports.typeDefs
  43. var remove = {}
  44. , typeDefault = [false, true, null, String, Number]
  45. Object.keys(data).forEach(function (k) {
  46. if (k === "argv") return
  47. var val = data[k]
  48. , isArray = Array.isArray(val)
  49. , type = types[k]
  50. if (!isArray) val = [val]
  51. if (!type) type = typeDefault
  52. if (type === Array) type = typeDefault.concat(Array)
  53. if (!Array.isArray(type)) type = [type]
  54. debug("val=%j", val)
  55. debug("types=", type)
  56. val = val.map(function (val) {
  57. // if it's an unknown value, then parse false/true/null/numbers/dates
  58. if (typeof val === "string") {
  59. debug("string %j", val)
  60. val = val.trim()
  61. if ((val === "null" && ~type.indexOf(null))
  62. || (val === "true" &&
  63. (~type.indexOf(true) || ~type.indexOf(Boolean)))
  64. || (val === "false" &&
  65. (~type.indexOf(false) || ~type.indexOf(Boolean)))) {
  66. val = JSON.parse(val)
  67. debug("jsonable %j", val)
  68. } else if (~type.indexOf(Number) && !isNaN(val)) {
  69. debug("convert to number", val)
  70. val = +val
  71. } else if (~type.indexOf(Date) && !isNaN(Date.parse(val))) {
  72. debug("convert to date", val)
  73. val = new Date(val)
  74. }
  75. }
  76. if (!types.hasOwnProperty(k)) {
  77. return val
  78. }
  79. // allow `--no-blah` to set 'blah' to null if null is allowed
  80. if (val === false && ~type.indexOf(null) &&
  81. !(~type.indexOf(false) || ~type.indexOf(Boolean))) {
  82. val = null
  83. }
  84. var d = {}
  85. d[k] = val
  86. debug("prevalidated val", d, val, types[k])
  87. if (!validate(d, k, val, types[k], typeDefs)) {
  88. if (exports.invalidHandler) {
  89. exports.invalidHandler(k, val, types[k], data)
  90. } else if (exports.invalidHandler !== false) {
  91. debug("invalid: "+k+"="+val, types[k])
  92. }
  93. return remove
  94. }
  95. debug("validated val", d, val, types[k])
  96. return d[k]
  97. }).filter(function (val) { return val !== remove })
  98. if (!val.length) delete data[k]
  99. else if (isArray) {
  100. debug(isArray, data[k], val)
  101. data[k] = val
  102. } else data[k] = val[0]
  103. debug("k=%s val=%j", k, val, data[k])
  104. })
  105. }
  106. function validateString (data, k, val) {
  107. data[k] = String(val)
  108. }
  109. function validatePath (data, k, val) {
  110. data[k] = path.resolve(String(val))
  111. return true
  112. }
  113. function validateNumber (data, k, val) {
  114. debug("validate Number %j %j %j", k, val, isNaN(val))
  115. if (isNaN(val)) return false
  116. data[k] = +val
  117. }
  118. function validateDate (data, k, val) {
  119. debug("validate Date %j %j %j", k, val, Date.parse(val))
  120. var s = Date.parse(val)
  121. if (isNaN(s)) return false
  122. data[k] = new Date(val)
  123. }
  124. function validateBoolean (data, k, val) {
  125. if (val instanceof Boolean) val = val.valueOf()
  126. else if (typeof val === "string") {
  127. if (!isNaN(val)) val = !!(+val)
  128. else if (val === "null" || val === "false") val = false
  129. else val = true
  130. } else val = !!val
  131. data[k] = val
  132. }
  133. function validateUrl (data, k, val) {
  134. val = url.parse(String(val))
  135. if (!val.host) return false
  136. data[k] = val.href
  137. }
  138. function validateStream (data, k, val) {
  139. if (!(val instanceof Stream)) return false
  140. data[k] = val
  141. }
  142. function validate (data, k, val, type, typeDefs) {
  143. // arrays are lists of types.
  144. if (Array.isArray(type)) {
  145. for (var i = 0, l = type.length; i < l; i ++) {
  146. if (type[i] === Array) continue
  147. if (validate(data, k, val, type[i], typeDefs)) return true
  148. }
  149. delete data[k]
  150. return false
  151. }
  152. // an array of anything?
  153. if (type === Array) return true
  154. // NaN is poisonous. Means that something is not allowed.
  155. if (type !== type) {
  156. debug("Poison NaN", k, val, type)
  157. delete data[k]
  158. return false
  159. }
  160. // explicit list of values
  161. if (val === type) {
  162. debug("Explicitly allowed %j", val)
  163. // if (isArray) (data[k] = data[k] || []).push(val)
  164. // else data[k] = val
  165. data[k] = val
  166. return true
  167. }
  168. // now go through the list of typeDefs, validate against each one.
  169. var ok = false
  170. , types = Object.keys(typeDefs)
  171. for (var i = 0, l = types.length; i < l; i ++) {
  172. debug("test type %j %j %j", k, val, types[i])
  173. var t = typeDefs[types[i]]
  174. if (t && type === t.type) {
  175. var d = {}
  176. ok = false !== t.validate(d, k, val)
  177. val = d[k]
  178. if (ok) {
  179. // if (isArray) (data[k] = data[k] || []).push(val)
  180. // else data[k] = val
  181. data[k] = val
  182. break
  183. }
  184. }
  185. }
  186. debug("OK? %j (%j %j %j)", ok, k, val, types[i])
  187. if (!ok) delete data[k]
  188. return ok
  189. }
  190. function parse (args, data, remain, types, shorthands) {
  191. debug("parse", args, data, remain)
  192. var key = null
  193. , abbrevs = abbrev(Object.keys(types))
  194. , shortAbbr = abbrev(Object.keys(shorthands))
  195. for (var i = 0; i < args.length; i ++) {
  196. var arg = args[i]
  197. debug("arg", arg)
  198. if (arg.match(/^-{2,}$/)) {
  199. // done with keys.
  200. // the rest are args.
  201. remain.push.apply(remain, args.slice(i + 1))
  202. args[i] = "--"
  203. break
  204. }
  205. if (arg.charAt(0) === "-") {
  206. if (arg.indexOf("=") !== -1) {
  207. var v = arg.split("=")
  208. arg = v.shift()
  209. v = v.join("=")
  210. args.splice.apply(args, [i, 1].concat([arg, v]))
  211. }
  212. // see if it's a shorthand
  213. // if so, splice and back up to re-parse it.
  214. var shRes = resolveShort(arg, shorthands, shortAbbr, abbrevs)
  215. debug("arg=%j shRes=%j", arg, shRes)
  216. if (shRes) {
  217. debug(arg, shRes)
  218. args.splice.apply(args, [i, 1].concat(shRes))
  219. if (arg !== shRes[0]) {
  220. i --
  221. continue
  222. }
  223. }
  224. arg = arg.replace(/^-+/, "")
  225. var no = false
  226. while (arg.toLowerCase().indexOf("no-") === 0) {
  227. no = !no
  228. arg = arg.substr(3)
  229. }
  230. if (abbrevs[arg]) arg = abbrevs[arg]
  231. var isArray = types[arg] === Array ||
  232. Array.isArray(types[arg]) && types[arg].indexOf(Array) !== -1
  233. var val
  234. , la = args[i + 1]
  235. var isBool = no ||
  236. types[arg] === Boolean ||
  237. Array.isArray(types[arg]) && types[arg].indexOf(Boolean) !== -1 ||
  238. (la === "false" &&
  239. (types[arg] === null ||
  240. Array.isArray(types[arg]) && ~types[arg].indexOf(null)))
  241. if (isBool) {
  242. // just set and move along
  243. val = !no
  244. // however, also support --bool true or --bool false
  245. if (la === "true" || la === "false") {
  246. val = JSON.parse(la)
  247. la = null
  248. if (no) val = !val
  249. i ++
  250. }
  251. // also support "foo":[Boolean, "bar"] and "--foo bar"
  252. if (Array.isArray(types[arg]) && la) {
  253. if (~types[arg].indexOf(la)) {
  254. // an explicit type
  255. val = la
  256. i ++
  257. } else if ( la === "null" && ~types[arg].indexOf(null) ) {
  258. // null allowed
  259. val = null
  260. i ++
  261. } else if ( !la.match(/^-{2,}[^-]/) &&
  262. !isNaN(la) &&
  263. ~types[arg].indexOf(Number) ) {
  264. // number
  265. val = +la
  266. i ++
  267. } else if ( !la.match(/^-[^-]/) && ~types[arg].indexOf(String) ) {
  268. // string
  269. val = la
  270. i ++
  271. }
  272. }
  273. if (isArray) (data[arg] = data[arg] || []).push(val)
  274. else data[arg] = val
  275. continue
  276. }
  277. if (la && la.match(/^-{2,}$/)) {
  278. la = undefined
  279. i --
  280. }
  281. val = la === undefined ? true : la
  282. if (isArray) (data[arg] = data[arg] || []).push(val)
  283. else data[arg] = val
  284. i ++
  285. continue
  286. }
  287. remain.push(arg)
  288. }
  289. }
  290. function resolveShort (arg, shorthands, shortAbbr, abbrevs) {
  291. // handle single-char shorthands glommed together, like
  292. // npm ls -glp, but only if there is one dash, and only if
  293. // all of the chars are single-char shorthands, and it's
  294. // not a match to some other abbrev.
  295. arg = arg.replace(/^-+/, '')
  296. if (abbrevs[arg] && !shorthands[arg]) {
  297. return null
  298. }
  299. if (shortAbbr[arg]) {
  300. arg = shortAbbr[arg]
  301. } else {
  302. var singles = shorthands.___singles
  303. if (!singles) {
  304. singles = Object.keys(shorthands).filter(function (s) {
  305. return s.length === 1
  306. }).reduce(function (l,r) { l[r] = true ; return l }, {})
  307. shorthands.___singles = singles
  308. }
  309. var chrs = arg.split("").filter(function (c) {
  310. return singles[c]
  311. })
  312. if (chrs.join("") === arg) return chrs.map(function (c) {
  313. return shorthands[c]
  314. }).reduce(function (l, r) {
  315. return l.concat(r)
  316. }, [])
  317. }
  318. if (shorthands[arg] && !Array.isArray(shorthands[arg])) {
  319. shorthands[arg] = shorthands[arg].split(/\s+/)
  320. }
  321. return shorthands[arg]
  322. }
  323. if (module === require.main) {
  324. var assert = require("assert")
  325. , util = require("util")
  326. , shorthands =
  327. { s : ["--loglevel", "silent"]
  328. , d : ["--loglevel", "info"]
  329. , dd : ["--loglevel", "verbose"]
  330. , ddd : ["--loglevel", "silly"]
  331. , noreg : ["--no-registry"]
  332. , reg : ["--registry"]
  333. , "no-reg" : ["--no-registry"]
  334. , silent : ["--loglevel", "silent"]
  335. , verbose : ["--loglevel", "verbose"]
  336. , h : ["--usage"]
  337. , H : ["--usage"]
  338. , "?" : ["--usage"]
  339. , help : ["--usage"]
  340. , v : ["--version"]
  341. , f : ["--force"]
  342. , desc : ["--description"]
  343. , "no-desc" : ["--no-description"]
  344. , "local" : ["--no-global"]
  345. , l : ["--long"]
  346. , p : ["--parseable"]
  347. , porcelain : ["--parseable"]
  348. , g : ["--global"]
  349. }
  350. , types =
  351. { aoa: Array
  352. , nullstream: [null, Stream]
  353. , date: Date
  354. , str: String
  355. , browser : String
  356. , cache : path
  357. , color : ["always", Boolean]
  358. , depth : Number
  359. , description : Boolean
  360. , dev : Boolean
  361. , editor : path
  362. , force : Boolean
  363. , global : Boolean
  364. , globalconfig : path
  365. , group : [String, Number]
  366. , gzipbin : String
  367. , logfd : [Number, Stream]
  368. , loglevel : ["silent","win","error","warn","info","verbose","silly"]
  369. , long : Boolean
  370. , "node-version" : [false, String]
  371. , npaturl : url
  372. , npat : Boolean
  373. , "onload-script" : [false, String]
  374. , outfd : [Number, Stream]
  375. , parseable : Boolean
  376. , pre: Boolean
  377. , prefix: path
  378. , proxy : url
  379. , "rebuild-bundle" : Boolean
  380. , registry : url
  381. , searchopts : String
  382. , searchexclude: [null, String]
  383. , shell : path
  384. , t: [Array, String]
  385. , tag : String
  386. , tar : String
  387. , tmp : path
  388. , "unsafe-perm" : Boolean
  389. , usage : Boolean
  390. , user : String
  391. , username : String
  392. , userconfig : path
  393. , version : Boolean
  394. , viewer: path
  395. , _exit : Boolean
  396. }
  397. ; [["-v", {version:true}, []]
  398. ,["---v", {version:true}, []]
  399. ,["ls -s --no-reg connect -d",
  400. {loglevel:"info",registry:null},["ls","connect"]]
  401. ,["ls ---s foo",{loglevel:"silent"},["ls","foo"]]
  402. ,["ls --registry blargle", {}, ["ls"]]
  403. ,["--no-registry", {registry:null}, []]
  404. ,["--no-color true", {color:false}, []]
  405. ,["--no-color false", {color:true}, []]
  406. ,["--no-color", {color:false}, []]
  407. ,["--color false", {color:false}, []]
  408. ,["--color --logfd 7", {logfd:7,color:true}, []]
  409. ,["--color=true", {color:true}, []]
  410. ,["--logfd=10", {logfd:10}, []]
  411. ,["--tmp=/tmp -tar=gtar",{tmp:"/tmp",tar:"gtar"},[]]
  412. ,["--tmp=tmp -tar=gtar",
  413. {tmp:path.resolve(process.cwd(), "tmp"),tar:"gtar"},[]]
  414. ,["--logfd x", {}, []]
  415. ,["a -true -- -no-false", {true:true},["a","-no-false"]]
  416. ,["a -no-false", {false:false},["a"]]
  417. ,["a -no-no-true", {true:true}, ["a"]]
  418. ,["a -no-no-no-false", {false:false}, ["a"]]
  419. ,["---NO-no-No-no-no-no-nO-no-no"+
  420. "-No-no-no-no-no-no-no-no-no"+
  421. "-no-no-no-no-NO-NO-no-no-no-no-no-no"+
  422. "-no-body-can-do-the-boogaloo-like-I-do"
  423. ,{"body-can-do-the-boogaloo-like-I-do":false}, []]
  424. ,["we are -no-strangers-to-love "+
  425. "--you-know the-rules --and so-do-i "+
  426. "---im-thinking-of=a-full-commitment "+
  427. "--no-you-would-get-this-from-any-other-guy "+
  428. "--no-gonna-give-you-up "+
  429. "-no-gonna-let-you-down=true "+
  430. "--no-no-gonna-run-around false "+
  431. "--desert-you=false "+
  432. "--make-you-cry false "+
  433. "--no-tell-a-lie "+
  434. "--no-no-and-hurt-you false"
  435. ,{"strangers-to-love":false
  436. ,"you-know":"the-rules"
  437. ,"and":"so-do-i"
  438. ,"you-would-get-this-from-any-other-guy":false
  439. ,"gonna-give-you-up":false
  440. ,"gonna-let-you-down":false
  441. ,"gonna-run-around":false
  442. ,"desert-you":false
  443. ,"make-you-cry":false
  444. ,"tell-a-lie":false
  445. ,"and-hurt-you":false
  446. },["we", "are"]]
  447. ,["-t one -t two -t three"
  448. ,{t: ["one", "two", "three"]}
  449. ,[]]
  450. ,["-t one -t null -t three four five null"
  451. ,{t: ["one", "null", "three"]}
  452. ,["four", "five", "null"]]
  453. ,["-t foo"
  454. ,{t:["foo"]}
  455. ,[]]
  456. ,["--no-t"
  457. ,{t:["false"]}
  458. ,[]]
  459. ,["-no-no-t"
  460. ,{t:["true"]}
  461. ,[]]
  462. ,["-aoa one -aoa null -aoa 100"
  463. ,{aoa:["one", null, 100]}
  464. ,[]]
  465. ,["-str 100"
  466. ,{str:"100"}
  467. ,[]]
  468. ,["--color always"
  469. ,{color:"always"}
  470. ,[]]
  471. ,["--no-nullstream"
  472. ,{nullstream:null}
  473. ,[]]
  474. ,["--nullstream false"
  475. ,{nullstream:null}
  476. ,[]]
  477. ,["--notadate 2011-01-25"
  478. ,{notadate: "2011-01-25"}
  479. ,[]]
  480. ,["--date 2011-01-25"
  481. ,{date: new Date("2011-01-25")}
  482. ,[]]
  483. ].forEach(function (test) {
  484. var argv = test[0].split(/\s+/)
  485. , opts = test[1]
  486. , rem = test[2]
  487. , actual = nopt(types, shorthands, argv, 0)
  488. , parsed = actual.argv
  489. delete actual.argv
  490. console.log(util.inspect(actual, false, 2, true), parsed.remain)
  491. for (var i in opts) {
  492. var e = JSON.stringify(opts[i])
  493. , a = JSON.stringify(actual[i] === undefined ? null : actual[i])
  494. if (e && typeof e === "object") {
  495. assert.deepEqual(e, a)
  496. } else {
  497. assert.equal(e, a)
  498. }
  499. }
  500. assert.deepEqual(rem, parsed.remain)
  501. })
  502. }