index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. var assert = require('assert')
  2. var Completion = require('./lib/completion')
  3. var Parser = require('./lib/parser')
  4. var path = require('path')
  5. var tokenizeArgString = require('./lib/tokenize-arg-string')
  6. var Usage = require('./lib/usage')
  7. var Validation = require('./lib/validation')
  8. var Y18n = require('y18n')
  9. Argv(process.argv.slice(2))
  10. var exports = module.exports = Argv
  11. function Argv (processArgs, cwd) {
  12. processArgs = processArgs || [] // handle calling yargs().
  13. var self = {}
  14. var completion = null
  15. var usage = null
  16. var validation = null
  17. var y18n = Y18n({
  18. directory: path.resolve(__dirname, './locales'),
  19. updateFiles: false
  20. })
  21. if (!cwd) cwd = process.cwd()
  22. self.$0 = process.argv
  23. .slice(0, 2)
  24. .map(function (x, i) {
  25. // ignore the node bin, specify this in your
  26. // bin file with #!/usr/bin/env node
  27. if (i === 0 && /\b(node|iojs)$/.test(x)) return
  28. var b = rebase(cwd, x)
  29. return x.match(/^\//) && b.length < x.length ? b : x
  30. })
  31. .join(' ').trim()
  32. if (process.env._ !== undefined && process.argv[1] === process.env._) {
  33. self.$0 = process.env._.replace(
  34. path.dirname(process.execPath) + '/', ''
  35. )
  36. }
  37. var options
  38. self.resetOptions = self.reset = function () {
  39. // put yargs back into its initial
  40. // state, this is useful for creating a
  41. // nested CLI.
  42. options = {
  43. array: [],
  44. boolean: [],
  45. string: [],
  46. narg: {},
  47. key: {},
  48. alias: {},
  49. default: {},
  50. defaultDescription: {},
  51. choices: {},
  52. requiresArg: [],
  53. count: [],
  54. normalize: [],
  55. config: {},
  56. envPrefix: undefined
  57. }
  58. usage = Usage(self, y18n) // handle usage output.
  59. validation = Validation(self, usage, y18n) // handle arg validation.
  60. completion = Completion(self, usage)
  61. demanded = {}
  62. groups = {}
  63. exitProcess = true
  64. strict = false
  65. helpOpt = null
  66. versionOpt = null
  67. commandHandlers = {}
  68. self.parsed = false
  69. return self
  70. }
  71. self.resetOptions()
  72. self.boolean = function (bools) {
  73. options.boolean.push.apply(options.boolean, [].concat(bools))
  74. return self
  75. }
  76. self.array = function (arrays) {
  77. options.array.push.apply(options.array, [].concat(arrays))
  78. return self
  79. }
  80. self.nargs = function (key, n) {
  81. if (typeof key === 'object') {
  82. Object.keys(key).forEach(function (k) {
  83. self.nargs(k, key[k])
  84. })
  85. } else {
  86. options.narg[key] = n
  87. }
  88. return self
  89. }
  90. self.choices = function (key, values) {
  91. if (typeof key === 'object') {
  92. Object.keys(key).forEach(function (k) {
  93. self.choices(k, key[k])
  94. })
  95. } else {
  96. options.choices[key] = (options.choices[key] || []).concat(values)
  97. }
  98. return self
  99. }
  100. self.normalize = function (strings) {
  101. options.normalize.push.apply(options.normalize, [].concat(strings))
  102. return self
  103. }
  104. self.config = function (key, msg, parseFn) {
  105. if (typeof msg === 'function') {
  106. parseFn = msg
  107. msg = null
  108. }
  109. self.describe(key, msg || usage.deferY18nLookup('Path to JSON config file'))
  110. ;(Array.isArray(key) ? key : [key]).forEach(function (k) {
  111. options.config[k] = parseFn || true
  112. })
  113. return self
  114. }
  115. self.example = function (cmd, description) {
  116. usage.example(cmd, description)
  117. return self
  118. }
  119. self.command = function (cmd, description, fn) {
  120. if (description !== false) {
  121. usage.command(cmd, description)
  122. }
  123. if (fn) commandHandlers[cmd] = fn
  124. return self
  125. }
  126. var commandHandlers = {}
  127. self.getCommandHandlers = function () {
  128. return commandHandlers
  129. }
  130. self.string = function (strings) {
  131. options.string.push.apply(options.string, [].concat(strings))
  132. return self
  133. }
  134. self.default = function (key, value, defaultDescription) {
  135. if (typeof key === 'object') {
  136. Object.keys(key).forEach(function (k) {
  137. self.default(k, key[k])
  138. })
  139. } else {
  140. if (defaultDescription) options.defaultDescription[key] = defaultDescription
  141. if (typeof value === 'function') {
  142. if (!options.defaultDescription[key]) options.defaultDescription[key] = usage.functionDescription(value)
  143. value = value.call()
  144. }
  145. options.default[key] = value
  146. }
  147. return self
  148. }
  149. self.alias = function (x, y) {
  150. if (typeof x === 'object') {
  151. Object.keys(x).forEach(function (key) {
  152. self.alias(key, x[key])
  153. })
  154. } else {
  155. // perhaps 'x' is already an alias in another list?
  156. // if so we should append to x's list.
  157. var aliases = null
  158. Object.keys(options.alias).forEach(function (key) {
  159. if (~options.alias[key].indexOf(x)) aliases = options.alias[key]
  160. })
  161. if (aliases) { // x was an alias itself.
  162. aliases.push(y)
  163. } else { // x is a new alias key.
  164. options.alias[x] = (options.alias[x] || []).concat(y)
  165. }
  166. // wait! perhaps we've created two lists of aliases
  167. // that reference each other?
  168. if (options.alias[y]) {
  169. Array.prototype.push.apply((options.alias[x] || aliases), options.alias[y])
  170. delete options.alias[y]
  171. }
  172. }
  173. return self
  174. }
  175. self.count = function (counts) {
  176. options.count.push.apply(options.count, [].concat(counts))
  177. return self
  178. }
  179. var demanded = {}
  180. self.demand = self.required = self.require = function (keys, max, msg) {
  181. // you can optionally provide a 'max' key,
  182. // which will raise an exception if too many '_'
  183. // options are provided.
  184. if (typeof max !== 'number') {
  185. msg = max
  186. max = Infinity
  187. }
  188. if (typeof keys === 'number') {
  189. if (!demanded._) demanded._ = { count: 0, msg: null, max: max }
  190. demanded._.count = keys
  191. demanded._.msg = msg
  192. } else if (Array.isArray(keys)) {
  193. keys.forEach(function (key) {
  194. self.demand(key, msg)
  195. })
  196. } else {
  197. if (typeof msg === 'string') {
  198. demanded[keys] = { msg: msg }
  199. } else if (msg === true || typeof msg === 'undefined') {
  200. demanded[keys] = { msg: undefined }
  201. }
  202. }
  203. return self
  204. }
  205. self.getDemanded = function () {
  206. return demanded
  207. }
  208. self.requiresArg = function (requiresArgs) {
  209. options.requiresArg.push.apply(options.requiresArg, [].concat(requiresArgs))
  210. return self
  211. }
  212. self.implies = function (key, value) {
  213. validation.implies(key, value)
  214. return self
  215. }
  216. self.usage = function (msg, opts) {
  217. if (!opts && typeof msg === 'object') {
  218. opts = msg
  219. msg = null
  220. }
  221. usage.usage(msg)
  222. if (opts) self.options(opts)
  223. return self
  224. }
  225. self.epilogue = self.epilog = function (msg) {
  226. usage.epilog(msg)
  227. return self
  228. }
  229. self.fail = function (f) {
  230. usage.failFn(f)
  231. return self
  232. }
  233. self.check = function (f) {
  234. validation.check(f)
  235. return self
  236. }
  237. self.defaults = self.default
  238. self.describe = function (key, desc) {
  239. options.key[key] = true
  240. usage.describe(key, desc)
  241. return self
  242. }
  243. self.parse = function (args) {
  244. return parseArgs(args)
  245. }
  246. self.option = self.options = function (key, opt) {
  247. if (typeof key === 'object') {
  248. Object.keys(key).forEach(function (k) {
  249. self.options(k, key[k])
  250. })
  251. } else {
  252. assert(typeof opt === 'object', 'second argument to option must be an object')
  253. options.key[key] = true // track manually set keys.
  254. if (opt.alias) self.alias(key, opt.alias)
  255. var demand = opt.demand || opt.required || opt.require
  256. if (demand) {
  257. self.demand(key, demand)
  258. } if ('config' in opt) {
  259. self.config(key, opt.configParser)
  260. } if ('default' in opt) {
  261. self.default(key, opt.default)
  262. } if ('nargs' in opt) {
  263. self.nargs(key, opt.nargs)
  264. } if ('choices' in opt) {
  265. self.choices(key, opt.choices)
  266. } if ('group' in opt) {
  267. self.group(key, opt.group)
  268. } if (opt.boolean || opt.type === 'boolean') {
  269. self.boolean(key)
  270. if (opt.alias) self.boolean(opt.alias)
  271. } if (opt.array || opt.type === 'array') {
  272. self.array(key)
  273. if (opt.alias) self.array(opt.alias)
  274. } if (opt.string || opt.type === 'string') {
  275. self.string(key)
  276. if (opt.alias) self.string(opt.alias)
  277. } if (opt.count || opt.type === 'count') {
  278. self.count(key)
  279. } if (opt.defaultDescription) {
  280. options.defaultDescription[key] = opt.defaultDescription
  281. }
  282. var desc = opt.describe || opt.description || opt.desc
  283. if (desc) {
  284. self.describe(key, desc)
  285. }
  286. if (opt.requiresArg) {
  287. self.requiresArg(key)
  288. }
  289. }
  290. return self
  291. }
  292. self.getOptions = function () {
  293. return options
  294. }
  295. var groups = {}
  296. self.group = function (opts, groupName) {
  297. var seen = {}
  298. groups[groupName] = (groups[groupName] || []).concat(opts).filter(function (key) {
  299. if (seen[key]) return false
  300. return (seen[key] = true)
  301. })
  302. return self
  303. }
  304. self.getGroups = function () {
  305. return groups
  306. }
  307. // as long as options.envPrefix is not undefined,
  308. // parser will apply env vars matching prefix to argv
  309. self.env = function (prefix) {
  310. if (prefix === false) options.envPrefix = undefined
  311. else options.envPrefix = prefix || ''
  312. return self
  313. }
  314. self.wrap = function (cols) {
  315. usage.wrap(cols)
  316. return self
  317. }
  318. var strict = false
  319. self.strict = function () {
  320. strict = true
  321. return self
  322. }
  323. self.getStrict = function () {
  324. return strict
  325. }
  326. self.showHelp = function (level) {
  327. if (!self.parsed) parseArgs(processArgs) // run parser, if it has not already been executed.
  328. usage.showHelp(level)
  329. return self
  330. }
  331. var versionOpt = null
  332. self.version = function (ver, opt, msg) {
  333. versionOpt = opt || 'version'
  334. usage.version(ver)
  335. self.boolean(versionOpt)
  336. self.describe(versionOpt, msg || usage.deferY18nLookup('Show version number'))
  337. return self
  338. }
  339. var helpOpt = null
  340. self.addHelpOpt = function (opt, msg) {
  341. helpOpt = opt
  342. self.boolean(opt)
  343. self.describe(opt, msg || usage.deferY18nLookup('Show help'))
  344. return self
  345. }
  346. self.showHelpOnFail = function (enabled, message) {
  347. usage.showHelpOnFail(enabled, message)
  348. return self
  349. }
  350. var exitProcess = true
  351. self.exitProcess = function (enabled) {
  352. if (typeof enabled !== 'boolean') {
  353. enabled = true
  354. }
  355. exitProcess = enabled
  356. return self
  357. }
  358. self.getExitProcess = function () {
  359. return exitProcess
  360. }
  361. self.help = function () {
  362. if (arguments.length > 0) return self.addHelpOpt.apply(self, arguments)
  363. if (!self.parsed) parseArgs(processArgs) // run parser, if it has not already been executed.
  364. return usage.help()
  365. }
  366. var completionCommand = null
  367. self.completion = function (cmd, desc, fn) {
  368. // a function to execute when generating
  369. // completions can be provided as the second
  370. // or third argument to completion.
  371. if (typeof desc === 'function') {
  372. fn = desc
  373. desc = null
  374. }
  375. // register the completion command.
  376. completionCommand = cmd || 'completion'
  377. if (!desc && desc !== false) {
  378. desc = 'generate bash completion script'
  379. }
  380. self.command(completionCommand, desc)
  381. // a function can be provided
  382. if (fn) completion.registerFunction(fn)
  383. return self
  384. }
  385. self.showCompletionScript = function ($0) {
  386. $0 = $0 || self.$0
  387. console.log(completion.generateCompletionScript($0))
  388. return self
  389. }
  390. self.locale = function (locale) {
  391. if (arguments.length === 0) {
  392. guessLocale()
  393. return y18n.getLocale()
  394. }
  395. detectLocale = false
  396. y18n.setLocale(locale)
  397. return self
  398. }
  399. self.updateStrings = self.updateLocale = function (obj) {
  400. detectLocale = false
  401. y18n.updateLocale(obj)
  402. return self
  403. }
  404. var detectLocale = true
  405. self.detectLocale = function (detect) {
  406. detectLocale = detect
  407. return self
  408. }
  409. self.getDetectLocale = function () {
  410. return detectLocale
  411. }
  412. self.getUsageInstance = function () {
  413. return usage
  414. }
  415. self.getValidationInstance = function () {
  416. return validation
  417. }
  418. self.terminalWidth = function () {
  419. return require('window-size').width
  420. }
  421. Object.defineProperty(self, 'argv', {
  422. get: function () {
  423. var args = null
  424. try {
  425. args = parseArgs(processArgs)
  426. } catch (err) {
  427. usage.fail(err.message)
  428. }
  429. return args
  430. },
  431. enumerable: true
  432. })
  433. function parseArgs (args) {
  434. args = normalizeArgs(args)
  435. var parsed = Parser(args, options, y18n)
  436. var argv = parsed.argv
  437. var aliases = parsed.aliases
  438. argv.$0 = self.$0
  439. self.parsed = parsed
  440. guessLocale() // guess locale lazily, so that it can be turned off in chain.
  441. // while building up the argv object, there
  442. // are two passes through the parser. If completion
  443. // is being performed short-circuit on the first pass.
  444. if (completionCommand &&
  445. (process.argv.join(' ')).indexOf(completion.completionKey) !== -1 &&
  446. !argv[completion.completionKey]) {
  447. return argv
  448. }
  449. // if there's a handler associated with a
  450. // command defer processing to it.
  451. var handlerKeys = Object.keys(self.getCommandHandlers())
  452. for (var i = 0, command; (command = handlerKeys[i]) !== undefined; i++) {
  453. if (~argv._.indexOf(command)) {
  454. runCommand(command, self, argv)
  455. return self.argv
  456. }
  457. }
  458. // generate a completion script for adding to ~/.bashrc.
  459. if (completionCommand && ~argv._.indexOf(completionCommand) && !argv[completion.completionKey]) {
  460. self.showCompletionScript()
  461. if (exitProcess) {
  462. process.exit(0)
  463. }
  464. }
  465. // we must run completions first, a user might
  466. // want to complete the --help or --version option.
  467. if (completion.completionKey in argv) {
  468. // we allow for asynchronous completions,
  469. // e.g., loading in a list of commands from an API.
  470. completion.getCompletion(function (completions) {
  471. ;(completions || []).forEach(function (completion) {
  472. console.log(completion)
  473. })
  474. if (exitProcess) {
  475. process.exit(0)
  476. }
  477. })
  478. return
  479. }
  480. var helpOrVersion = false
  481. Object.keys(argv).forEach(function (key) {
  482. if (key === helpOpt && argv[key]) {
  483. helpOrVersion = true
  484. self.showHelp('log')
  485. if (exitProcess) {
  486. process.exit(0)
  487. }
  488. } else if (key === versionOpt && argv[key]) {
  489. helpOrVersion = true
  490. usage.showVersion()
  491. if (exitProcess) {
  492. process.exit(0)
  493. }
  494. }
  495. })
  496. // If the help or version options where used and exitProcess is false,
  497. // we won't run validations
  498. if (!helpOrVersion) {
  499. if (parsed.error) throw parsed.error
  500. // if we're executed via bash completion, don't
  501. // bother with validation.
  502. if (!argv[completion.completionKey]) {
  503. validation.nonOptionCount(argv)
  504. validation.missingArgumentValue(argv)
  505. validation.requiredArguments(argv)
  506. if (strict) validation.unknownArguments(argv, aliases)
  507. validation.customChecks(argv, aliases)
  508. validation.limitedChoices(argv)
  509. validation.implications(argv)
  510. }
  511. }
  512. setPlaceholderKeys(argv)
  513. return argv
  514. }
  515. function guessLocale () {
  516. if (!detectLocale) return
  517. try {
  518. var osLocale = require('os-locale')
  519. self.locale(osLocale.sync({ spawn: false }))
  520. } catch (err) {
  521. // if we explode looking up locale just noop
  522. // we'll keep using the default language 'en'.
  523. }
  524. }
  525. function runCommand (command, yargs, argv) {
  526. setPlaceholderKeys(argv)
  527. yargs.getCommandHandlers()[command](yargs.reset(), argv)
  528. }
  529. function setPlaceholderKeys (argv) {
  530. Object.keys(options.key).forEach(function (key) {
  531. // don't set placeholder keys for dot
  532. // notation options 'foo.bar'.
  533. if (~key.indexOf('.')) return
  534. if (typeof argv[key] === 'undefined') argv[key] = undefined
  535. })
  536. }
  537. function normalizeArgs (args) {
  538. if (typeof args === 'string') {
  539. return tokenizeArgString(args)
  540. }
  541. return args
  542. }
  543. singletonify(self)
  544. return self
  545. }
  546. // rebase an absolute path to a relative one with respect to a base directory
  547. // exported for tests
  548. exports.rebase = rebase
  549. function rebase (base, dir) {
  550. return path.relative(base, dir)
  551. }
  552. /* Hack an instance of Argv with process.argv into Argv
  553. so people can do
  554. require('yargs')(['--beeble=1','-z','zizzle']).argv
  555. to parse a list of args and
  556. require('yargs').argv
  557. to get a parsed version of process.argv.
  558. */
  559. function singletonify (inst) {
  560. Object.keys(inst).forEach(function (key) {
  561. if (key === 'argv') {
  562. Argv.__defineGetter__(key, inst.__lookupGetter__(key))
  563. } else {
  564. Argv[key] = typeof inst[key] === 'function' ? inst[key].bind(inst) : inst[key]
  565. }
  566. })
  567. }