yargs.js 20 KB

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