usage.js 13 KB

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