usage.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. // this file handles outputting usage instructions,
  2. // failures, etc. keeps logging in one place.
  3. const stringWidth = require('string-width')
  4. const objFilter = require('./obj-filter')
  5. const setBlocking = require('set-blocking')
  6. const YError = require('./yerror')
  7. module.exports = function (yargs, y18n) {
  8. const __ = y18n.__
  9. const self = {}
  10. // methods for ouputting/building failure message.
  11. var fails = []
  12. self.failFn = function (f) {
  13. fails.push(f)
  14. }
  15. var failMessage = null
  16. var showHelpOnFail = true
  17. self.showHelpOnFail = function (enabled, message) {
  18. if (typeof enabled === 'string') {
  19. message = enabled
  20. enabled = true
  21. } else if (typeof enabled === 'undefined') {
  22. enabled = true
  23. }
  24. failMessage = message
  25. showHelpOnFail = enabled
  26. return self
  27. }
  28. var failureOutput = false
  29. self.fail = function (msg, err) {
  30. const logger = yargs._getLoggerInstance()
  31. if (fails.length) {
  32. for (var i = fails.length - 1; i >= 0; --i) {
  33. fails[i](msg, err, self)
  34. }
  35. } else {
  36. if (yargs.getExitProcess()) setBlocking(true)
  37. // don't output failure message more than once
  38. if (!failureOutput) {
  39. failureOutput = true
  40. if (showHelpOnFail) yargs.showHelp('error')
  41. if (msg) logger.error(msg)
  42. if (failMessage) {
  43. if (msg) logger.error('')
  44. logger.error(failMessage)
  45. }
  46. }
  47. err = err || new YError(msg)
  48. if (yargs.getExitProcess()) {
  49. return yargs.exit(1)
  50. } else if (yargs._hasParseCallback()) {
  51. return yargs.exit(1, err)
  52. } else {
  53. throw err
  54. }
  55. }
  56. }
  57. // methods for ouputting/building help (usage) message.
  58. var usage
  59. self.usage = function (msg) {
  60. usage = msg
  61. }
  62. self.getUsage = function () {
  63. return usage
  64. }
  65. var examples = []
  66. self.example = function (cmd, description) {
  67. examples.push([cmd, description || ''])
  68. }
  69. var commands = []
  70. self.command = function (cmd, description, isDefault, aliases) {
  71. // the last default wins, so cancel out any previously set default
  72. if (isDefault) {
  73. commands = commands.map(function (cmdArray) {
  74. cmdArray[2] = false
  75. return cmdArray
  76. })
  77. }
  78. commands.push([cmd, description || '', isDefault, aliases])
  79. }
  80. self.getCommands = function () {
  81. return commands
  82. }
  83. var descriptions = {}
  84. self.describe = function (key, desc) {
  85. if (typeof key === 'object') {
  86. Object.keys(key).forEach(function (k) {
  87. self.describe(k, key[k])
  88. })
  89. } else {
  90. descriptions[key] = desc
  91. }
  92. }
  93. self.getDescriptions = function () {
  94. return descriptions
  95. }
  96. var epilog
  97. self.epilog = function (msg) {
  98. epilog = msg
  99. }
  100. var wrapSet = false
  101. var wrap
  102. self.wrap = function (cols) {
  103. wrapSet = true
  104. wrap = cols
  105. }
  106. function getWrap () {
  107. if (!wrapSet) {
  108. wrap = windowWidth()
  109. wrapSet = true
  110. }
  111. return wrap
  112. }
  113. var deferY18nLookupPrefix = '__yargsString__:'
  114. self.deferY18nLookup = function (str) {
  115. return deferY18nLookupPrefix + str
  116. }
  117. var defaultGroup = 'Options:'
  118. self.help = function () {
  119. normalizeAliases()
  120. // handle old demanded API
  121. var demandedOptions = yargs.getDemandedOptions()
  122. var demandedCommands = yargs.getDemandedCommands()
  123. var groups = yargs.getGroups()
  124. var options = yargs.getOptions()
  125. var keys = Object.keys(
  126. Object.keys(descriptions)
  127. .concat(Object.keys(demandedOptions))
  128. .concat(Object.keys(demandedCommands))
  129. .concat(Object.keys(options.default))
  130. .reduce(function (acc, key) {
  131. if (key !== '_') acc[key] = true
  132. return acc
  133. }, {})
  134. )
  135. var theWrap = getWrap()
  136. var ui = require('cliui')({
  137. width: theWrap,
  138. wrap: !!theWrap
  139. })
  140. // the usage string.
  141. if (usage) {
  142. var u = usage.replace(/\$0/g, yargs.$0)
  143. ui.div(u + '\n')
  144. }
  145. // your application's commands, i.e., non-option
  146. // arguments populated in '_'.
  147. if (commands.length) {
  148. ui.div(__('Commands:'))
  149. commands.forEach(function (command) {
  150. ui.span(
  151. {text: command[0], padding: [0, 2, 0, 2], width: maxWidth(commands, theWrap) + 4},
  152. {text: command[1]}
  153. )
  154. var hints = []
  155. if (command[2]) hints.push('[' + __('default:').slice(0, -1) + ']') // TODO hacking around i18n here
  156. if (command[3] && command[3].length) {
  157. hints.push('[' + __('aliases:') + ' ' + command[3].join(', ') + ']')
  158. }
  159. if (hints.length) {
  160. ui.div({text: hints.join(' '), padding: [0, 0, 0, 2], align: 'right'})
  161. } else {
  162. ui.div()
  163. }
  164. })
  165. ui.div()
  166. }
  167. // perform some cleanup on the keys array, making it
  168. // only include top-level keys not their aliases.
  169. var aliasKeys = (Object.keys(options.alias) || [])
  170. .concat(Object.keys(yargs.parsed.newAliases) || [])
  171. keys = keys.filter(function (key) {
  172. return !yargs.parsed.newAliases[key] && aliasKeys.every(function (alias) {
  173. return (options.alias[alias] || []).indexOf(key) === -1
  174. })
  175. })
  176. // populate 'Options:' group with any keys that have not
  177. // explicitly had a group set.
  178. if (!groups[defaultGroup]) groups[defaultGroup] = []
  179. addUngroupedKeys(keys, options.alias, groups)
  180. // display 'Options:' table along with any custom tables:
  181. Object.keys(groups).forEach(function (groupName) {
  182. if (!groups[groupName].length) return
  183. ui.div(__(groupName))
  184. // if we've grouped the key 'f', but 'f' aliases 'foobar',
  185. // normalizedKeys should contain only 'foobar'.
  186. var normalizedKeys = groups[groupName].map(function (key) {
  187. if (~aliasKeys.indexOf(key)) return key
  188. for (var i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) {
  189. if (~(options.alias[aliasKey] || []).indexOf(key)) return aliasKey
  190. }
  191. return key
  192. })
  193. // actually generate the switches string --foo, -f, --bar.
  194. var switches = normalizedKeys.reduce(function (acc, key) {
  195. acc[key] = [ key ].concat(options.alias[key] || [])
  196. .map(function (sw) {
  197. return (sw.length > 1 ? '--' : '-') + sw
  198. })
  199. .join(', ')
  200. return acc
  201. }, {})
  202. normalizedKeys.forEach(function (key) {
  203. var kswitch = switches[key]
  204. var desc = descriptions[key] || ''
  205. var type = null
  206. if (~desc.lastIndexOf(deferY18nLookupPrefix)) desc = __(desc.substring(deferY18nLookupPrefix.length))
  207. if (~options.boolean.indexOf(key)) type = '[' + __('boolean') + ']'
  208. if (~options.count.indexOf(key)) type = '[' + __('count') + ']'
  209. if (~options.string.indexOf(key)) type = '[' + __('string') + ']'
  210. if (~options.normalize.indexOf(key)) type = '[' + __('string') + ']'
  211. if (~options.array.indexOf(key)) type = '[' + __('array') + ']'
  212. if (~options.number.indexOf(key)) type = '[' + __('number') + ']'
  213. var extra = [
  214. type,
  215. (key in demandedOptions) ? '[' + __('required') + ']' : null,
  216. options.choices && options.choices[key] ? '[' + __('choices:') + ' ' +
  217. self.stringifiedValues(options.choices[key]) + ']' : null,
  218. defaultString(options.default[key], options.defaultDescription[key])
  219. ].filter(Boolean).join(' ')
  220. ui.span(
  221. {text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches, theWrap) + 4},
  222. desc
  223. )
  224. if (extra) ui.div({text: extra, padding: [0, 0, 0, 2], align: 'right'})
  225. else ui.div()
  226. })
  227. ui.div()
  228. })
  229. // describe some common use-cases for your application.
  230. if (examples.length) {
  231. ui.div(__('Examples:'))
  232. examples.forEach(function (example) {
  233. example[0] = example[0].replace(/\$0/g, yargs.$0)
  234. })
  235. examples.forEach(function (example) {
  236. if (example[1] === '') {
  237. ui.div(
  238. {
  239. text: example[0],
  240. padding: [0, 2, 0, 2]
  241. }
  242. )
  243. } else {
  244. ui.div(
  245. {
  246. text: example[0],
  247. padding: [0, 2, 0, 2],
  248. width: maxWidth(examples, theWrap) + 4
  249. }, {
  250. text: example[1]
  251. }
  252. )
  253. }
  254. })
  255. ui.div()
  256. }
  257. // the usage string.
  258. if (epilog) {
  259. var e = epilog.replace(/\$0/g, yargs.$0)
  260. ui.div(e + '\n')
  261. }
  262. return ui.toString()
  263. }
  264. // return the maximum width of a string
  265. // in the left-hand column of a table.
  266. function maxWidth (table, theWrap) {
  267. var width = 0
  268. // table might be of the form [leftColumn],
  269. // or {key: leftColumn}
  270. if (!Array.isArray(table)) {
  271. table = Object.keys(table).map(function (key) {
  272. return [table[key]]
  273. })
  274. }
  275. table.forEach(function (v) {
  276. width = Math.max(stringWidth(v[0]), width)
  277. })
  278. // if we've enabled 'wrap' we should limit
  279. // the max-width of the left-column.
  280. if (theWrap) width = Math.min(width, parseInt(theWrap * 0.5, 10))
  281. return width
  282. }
  283. // make sure any options set for aliases,
  284. // are copied to the keys being aliased.
  285. function normalizeAliases () {
  286. // handle old demanded API
  287. var demandedOptions = yargs.getDemandedOptions()
  288. var options = yargs.getOptions()
  289. ;(Object.keys(options.alias) || []).forEach(function (key) {
  290. options.alias[key].forEach(function (alias) {
  291. // copy descriptions.
  292. if (descriptions[alias]) self.describe(key, descriptions[alias])
  293. // copy demanded.
  294. if (alias in demandedOptions) yargs.demandOption(key, demandedOptions[alias])
  295. // type messages.
  296. if (~options.boolean.indexOf(alias)) yargs.boolean(key)
  297. if (~options.count.indexOf(alias)) yargs.count(key)
  298. if (~options.string.indexOf(alias)) yargs.string(key)
  299. if (~options.normalize.indexOf(alias)) yargs.normalize(key)
  300. if (~options.array.indexOf(alias)) yargs.array(key)
  301. if (~options.number.indexOf(alias)) yargs.number(key)
  302. })
  303. })
  304. }
  305. // given a set of keys, place any keys that are
  306. // ungrouped under the 'Options:' grouping.
  307. function addUngroupedKeys (keys, aliases, groups) {
  308. var groupedKeys = []
  309. var toCheck = null
  310. Object.keys(groups).forEach(function (group) {
  311. groupedKeys = groupedKeys.concat(groups[group])
  312. })
  313. keys.forEach(function (key) {
  314. toCheck = [key].concat(aliases[key])
  315. if (!toCheck.some(function (k) {
  316. return groupedKeys.indexOf(k) !== -1
  317. })) {
  318. groups[defaultGroup].push(key)
  319. }
  320. })
  321. return groupedKeys
  322. }
  323. self.showHelp = function (level) {
  324. const logger = yargs._getLoggerInstance()
  325. if (!level) level = 'error'
  326. var emit = typeof level === 'function' ? level : logger[level]
  327. emit(self.help())
  328. }
  329. self.functionDescription = function (fn) {
  330. var description = fn.name ? require('decamelize')(fn.name, '-') : __('generated-value')
  331. return ['(', description, ')'].join('')
  332. }
  333. self.stringifiedValues = function (values, separator) {
  334. var string = ''
  335. var sep = separator || ', '
  336. var array = [].concat(values)
  337. if (!values || !array.length) return string
  338. array.forEach(function (value) {
  339. if (string.length) string += sep
  340. string += JSON.stringify(value)
  341. })
  342. return string
  343. }
  344. // format the default-value-string displayed in
  345. // the right-hand column.
  346. function defaultString (value, defaultDescription) {
  347. var string = '[' + __('default:') + ' '
  348. if (value === undefined && !defaultDescription) return null
  349. if (defaultDescription) {
  350. string += defaultDescription
  351. } else {
  352. switch (typeof value) {
  353. case 'string':
  354. string += JSON.stringify(value)
  355. break
  356. case 'object':
  357. string += JSON.stringify(value)
  358. break
  359. default:
  360. string += value
  361. }
  362. }
  363. return string + ']'
  364. }
  365. // guess the width of the console window, max-width 80.
  366. function windowWidth () {
  367. var maxWidth = 80
  368. if (typeof process === 'object' && process.stdout && process.stdout.columns) {
  369. return Math.min(maxWidth, process.stdout.columns)
  370. } else {
  371. return maxWidth
  372. }
  373. }
  374. // logic for displaying application version.
  375. var version = null
  376. self.version = function (ver) {
  377. version = ver
  378. }
  379. self.showVersion = function () {
  380. const logger = yargs._getLoggerInstance()
  381. if (typeof version === 'function') logger.log(version())
  382. else logger.log(version)
  383. }
  384. self.reset = function (localLookup) {
  385. // do not reset wrap here
  386. // do not reset fails here
  387. failMessage = null
  388. failureOutput = false
  389. usage = undefined
  390. epilog = undefined
  391. examples = []
  392. commands = []
  393. descriptions = objFilter(descriptions, function (k, v) {
  394. return !localLookup[k]
  395. })
  396. return self
  397. }
  398. var frozen
  399. self.freeze = function () {
  400. frozen = {}
  401. frozen.failMessage = failMessage
  402. frozen.failureOutput = failureOutput
  403. frozen.usage = usage
  404. frozen.epilog = epilog
  405. frozen.examples = examples
  406. frozen.commands = commands
  407. frozen.descriptions = descriptions
  408. }
  409. self.unfreeze = function () {
  410. failMessage = frozen.failMessage
  411. failureOutput = frozen.failureOutput
  412. usage = frozen.usage
  413. epilog = frozen.epilog
  414. examples = frozen.examples
  415. commands = frozen.commands
  416. descriptions = frozen.descriptions
  417. frozen = undefined
  418. }
  419. return self
  420. }