UNPKG

15.9 kBJavaScriptView Raw
1'use strict'
2// this file handles outputting usage instructions,
3// failures, etc. keeps logging in one place.
4const decamelize = require('decamelize')
5const stringWidth = require('string-width')
6const objFilter = require('./obj-filter')
7const path = require('path')
8const setBlocking = require('set-blocking')
9const YError = require('./yerror')
10
11module.exports = function usage (yargs, y18n) {
12 const __ = y18n.__
13 const self = {}
14
15 // methods for ouputting/building failure message.
16 const fails = []
17 self.failFn = function failFn (f) {
18 fails.push(f)
19 }
20
21 let failMessage = null
22 let showHelpOnFail = true
23 self.showHelpOnFail = function showHelpOnFailFn (enabled, message) {
24 if (typeof enabled === 'string') {
25 message = enabled
26 enabled = true
27 } else if (typeof enabled === 'undefined') {
28 enabled = true
29 }
30 failMessage = message
31 showHelpOnFail = enabled
32 return self
33 }
34
35 let failureOutput = false
36 self.fail = function fail (msg, err) {
37 const logger = yargs._getLoggerInstance()
38
39 if (fails.length) {
40 for (let i = fails.length - 1; i >= 0; --i) {
41 fails[i](msg, err, self)
42 }
43 } else {
44 if (yargs.getExitProcess()) setBlocking(true)
45
46 // don't output failure message more than once
47 if (!failureOutput) {
48 failureOutput = true
49 if (showHelpOnFail) {
50 yargs.showHelp('error')
51 logger.error()
52 }
53 if (msg || err) logger.error(msg || err)
54 if (failMessage) {
55 if (msg || err) logger.error('')
56 logger.error(failMessage)
57 }
58 }
59
60 err = err || new YError(msg)
61 if (yargs.getExitProcess()) {
62 return yargs.exit(1)
63 } else if (yargs._hasParseCallback()) {
64 return yargs.exit(1, err)
65 } else {
66 throw err
67 }
68 }
69 }
70
71 // methods for ouputting/building help (usage) message.
72 let usages = []
73 let usageDisabled = false
74 self.usage = (msg, description) => {
75 if (msg === null) {
76 usageDisabled = true
77 usages = []
78 return
79 }
80 usageDisabled = false
81 usages.push([msg, description || ''])
82 return self
83 }
84 self.getUsage = () => {
85 return usages
86 }
87 self.getUsageDisabled = () => {
88 return usageDisabled
89 }
90
91 self.getPositionalGroupName = () => {
92 return __('Positionals:')
93 }
94
95 let examples = []
96 self.example = (cmd, description) => {
97 examples.push([cmd, description || ''])
98 }
99
100 let commands = []
101 self.command = function command (cmd, description, isDefault, aliases) {
102 // the last default wins, so cancel out any previously set default
103 if (isDefault) {
104 commands = commands.map((cmdArray) => {
105 cmdArray[2] = false
106 return cmdArray
107 })
108 }
109 commands.push([cmd, description || '', isDefault, aliases])
110 }
111 self.getCommands = () => commands
112
113 let descriptions = {}
114 self.describe = function describe (key, desc) {
115 if (typeof key === 'object') {
116 Object.keys(key).forEach((k) => {
117 self.describe(k, key[k])
118 })
119 } else {
120 descriptions[key] = desc
121 }
122 }
123 self.getDescriptions = () => descriptions
124
125 let epilogs = []
126 self.epilog = (msg) => {
127 epilogs.push(msg)
128 }
129
130 let wrapSet = false
131 let wrap
132 self.wrap = (cols) => {
133 wrapSet = true
134 wrap = cols
135 }
136
137 function getWrap () {
138 if (!wrapSet) {
139 wrap = windowWidth()
140 wrapSet = true
141 }
142
143 return wrap
144 }
145
146 const deferY18nLookupPrefix = '__yargsString__:'
147 self.deferY18nLookup = str => deferY18nLookupPrefix + str
148
149 const defaultGroup = 'Options:'
150 self.help = function help () {
151 if (cachedHelpMessage) return cachedHelpMessage
152 normalizeAliases()
153
154 // handle old demanded API
155 const base$0 = yargs.customScriptName ? yargs.$0 : path.basename(yargs.$0)
156 const demandedOptions = yargs.getDemandedOptions()
157 const demandedCommands = yargs.getDemandedCommands()
158 const groups = yargs.getGroups()
159 const options = yargs.getOptions()
160
161 let keys = []
162 keys = keys.concat(Object.keys(descriptions))
163 keys = keys.concat(Object.keys(demandedOptions))
164 keys = keys.concat(Object.keys(demandedCommands))
165 keys = keys.concat(Object.keys(options.default))
166 keys = keys.filter(filterHiddenOptions)
167 keys = Object.keys(keys.reduce((acc, key) => {
168 if (key !== '_') acc[key] = true
169 return acc
170 }, {}))
171
172 const theWrap = getWrap()
173 const ui = require('cliui')({
174 width: theWrap,
175 wrap: !!theWrap
176 })
177
178 // the usage string.
179 if (!usageDisabled) {
180 if (usages.length) {
181 // user-defined usage.
182 usages.forEach((usage) => {
183 ui.div(`${usage[0].replace(/\$0/g, base$0)}`)
184 if (usage[1]) {
185 ui.div({ text: `${usage[1]}`, padding: [1, 0, 0, 0] })
186 }
187 })
188 ui.div()
189 } else if (commands.length) {
190 let u = null
191 // demonstrate how commands are used.
192 if (demandedCommands._) {
193 u = `${base$0} <${__('command')}>\n`
194 } else {
195 u = `${base$0} [${__('command')}]\n`
196 }
197 ui.div(`${u}`)
198 }
199 }
200
201 // your application's commands, i.e., non-option
202 // arguments populated in '_'.
203 if (commands.length) {
204 ui.div(__('Commands:'))
205
206 const context = yargs.getContext()
207 const parentCommands = context.commands.length ? `${context.commands.join(' ')} ` : ''
208
209 if (yargs.getParserConfiguration()['sort-commands'] === true) {
210 commands = commands.sort((a, b) => a[0].localeCompare(b[0]))
211 }
212
213 commands.forEach((command) => {
214 const commandString = `${base$0} ${parentCommands}${command[0].replace(/^\$0 ?/, '')}` // drop $0 from default commands.
215 ui.span(
216 {
217 text: commandString,
218 padding: [0, 2, 0, 2],
219 width: maxWidth(commands, theWrap, `${base$0}${parentCommands}`) + 4
220 },
221 { text: command[1] }
222 )
223 const hints = []
224 if (command[2]) hints.push(`[${__('default:').slice(0, -1)}]`) // TODO hacking around i18n here
225 if (command[3] && command[3].length) {
226 hints.push(`[${__('aliases:')} ${command[3].join(', ')}]`)
227 }
228 if (hints.length) {
229 ui.div({ text: hints.join(' '), padding: [0, 0, 0, 2], align: 'right' })
230 } else {
231 ui.div()
232 }
233 })
234
235 ui.div()
236 }
237
238 // perform some cleanup on the keys array, making it
239 // only include top-level keys not their aliases.
240 const aliasKeys = (Object.keys(options.alias) || [])
241 .concat(Object.keys(yargs.parsed.newAliases) || [])
242
243 keys = keys.filter(key => !yargs.parsed.newAliases[key] && aliasKeys.every(alias => (options.alias[alias] || []).indexOf(key) === -1))
244
245 // populate 'Options:' group with any keys that have not
246 // explicitly had a group set.
247 if (!groups[defaultGroup]) groups[defaultGroup] = []
248 addUngroupedKeys(keys, options.alias, groups)
249
250 // display 'Options:' table along with any custom tables:
251 Object.keys(groups).forEach((groupName) => {
252 if (!groups[groupName].length) return
253
254 // if we've grouped the key 'f', but 'f' aliases 'foobar',
255 // normalizedKeys should contain only 'foobar'.
256 const normalizedKeys = groups[groupName].filter(filterHiddenOptions).map((key) => {
257 if (~aliasKeys.indexOf(key)) return key
258 for (let i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) {
259 if (~(options.alias[aliasKey] || []).indexOf(key)) return aliasKey
260 }
261 return key
262 })
263
264 if (normalizedKeys.length < 1) return
265
266 ui.div(__(groupName))
267
268 // actually generate the switches string --foo, -f, --bar.
269 const switches = normalizedKeys.reduce((acc, key) => {
270 acc[key] = [ key ].concat(options.alias[key] || [])
271 .map(sw => {
272 // for the special positional group don't
273 // add '--' or '-' prefix.
274 if (groupName === self.getPositionalGroupName()) return sw
275 else return (sw.length > 1 ? '--' : '-') + sw
276 })
277 .join(', ')
278
279 return acc
280 }, {})
281
282 normalizedKeys.forEach((key) => {
283 const kswitch = switches[key]
284 let desc = descriptions[key] || ''
285 let type = null
286
287 if (~desc.lastIndexOf(deferY18nLookupPrefix)) desc = __(desc.substring(deferY18nLookupPrefix.length))
288
289 if (~options.boolean.indexOf(key)) type = `[${__('boolean')}]`
290 if (~options.count.indexOf(key)) type = `[${__('count')}]`
291 if (~options.string.indexOf(key)) type = `[${__('string')}]`
292 if (~options.normalize.indexOf(key)) type = `[${__('string')}]`
293 if (~options.array.indexOf(key)) type = `[${__('array')}]`
294 if (~options.number.indexOf(key)) type = `[${__('number')}]`
295
296 const extra = [
297 type,
298 (key in demandedOptions) ? `[${__('required')}]` : null,
299 options.choices && options.choices[key] ? `[${__('choices:')} ${
300 self.stringifiedValues(options.choices[key])}]` : null,
301 defaultString(options.default[key], options.defaultDescription[key])
302 ].filter(Boolean).join(' ')
303
304 ui.span(
305 { text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches, theWrap) + 4 },
306 desc
307 )
308
309 if (extra) ui.div({ text: extra, padding: [0, 0, 0, 2], align: 'right' })
310 else ui.div()
311 })
312
313 ui.div()
314 })
315
316 // describe some common use-cases for your application.
317 if (examples.length) {
318 ui.div(__('Examples:'))
319
320 examples.forEach((example) => {
321 example[0] = example[0].replace(/\$0/g, base$0)
322 })
323
324 examples.forEach((example) => {
325 if (example[1] === '') {
326 ui.div(
327 {
328 text: example[0],
329 padding: [0, 2, 0, 2]
330 }
331 )
332 } else {
333 ui.div(
334 {
335 text: example[0],
336 padding: [0, 2, 0, 2],
337 width: maxWidth(examples, theWrap) + 4
338 }, {
339 text: example[1]
340 }
341 )
342 }
343 })
344
345 ui.div()
346 }
347
348 // the usage string.
349 if (epilogs.length > 0) {
350 const e = epilogs.map(epilog => epilog.replace(/\$0/g, base$0)).join('\n')
351 ui.div(`${e}\n`)
352 }
353
354 // Remove the trailing white spaces
355 return ui.toString().replace(/\s*$/, '')
356 }
357
358 // return the maximum width of a string
359 // in the left-hand column of a table.
360 function maxWidth (table, theWrap, modifier) {
361 let width = 0
362
363 // table might be of the form [leftColumn],
364 // or {key: leftColumn}
365 if (!Array.isArray(table)) {
366 table = Object.keys(table).map(key => [table[key]])
367 }
368
369 table.forEach((v) => {
370 width = Math.max(
371 stringWidth(modifier ? `${modifier} ${v[0]}` : v[0]),
372 width
373 )
374 })
375
376 // if we've enabled 'wrap' we should limit
377 // the max-width of the left-column.
378 if (theWrap) width = Math.min(width, parseInt(theWrap * 0.5, 10))
379
380 return width
381 }
382
383 // make sure any options set for aliases,
384 // are copied to the keys being aliased.
385 function normalizeAliases () {
386 // handle old demanded API
387 const demandedOptions = yargs.getDemandedOptions()
388 const options = yargs.getOptions()
389
390 ;(Object.keys(options.alias) || []).forEach((key) => {
391 options.alias[key].forEach((alias) => {
392 // copy descriptions.
393 if (descriptions[alias]) self.describe(key, descriptions[alias])
394 // copy demanded.
395 if (alias in demandedOptions) yargs.demandOption(key, demandedOptions[alias])
396 // type messages.
397 if (~options.boolean.indexOf(alias)) yargs.boolean(key)
398 if (~options.count.indexOf(alias)) yargs.count(key)
399 if (~options.string.indexOf(alias)) yargs.string(key)
400 if (~options.normalize.indexOf(alias)) yargs.normalize(key)
401 if (~options.array.indexOf(alias)) yargs.array(key)
402 if (~options.number.indexOf(alias)) yargs.number(key)
403 })
404 })
405 }
406
407 // if yargs is executing an async handler, we take a snapshot of the
408 // help message to display on failure:
409 let cachedHelpMessage
410 self.cacheHelpMessage = function () {
411 cachedHelpMessage = this.help()
412 }
413
414 // given a set of keys, place any keys that are
415 // ungrouped under the 'Options:' grouping.
416 function addUngroupedKeys (keys, aliases, groups) {
417 let groupedKeys = []
418 let toCheck = null
419 Object.keys(groups).forEach((group) => {
420 groupedKeys = groupedKeys.concat(groups[group])
421 })
422
423 keys.forEach((key) => {
424 toCheck = [key].concat(aliases[key])
425 if (!toCheck.some(k => groupedKeys.indexOf(k) !== -1)) {
426 groups[defaultGroup].push(key)
427 }
428 })
429 return groupedKeys
430 }
431
432 function filterHiddenOptions (key) {
433 return yargs.getOptions().hiddenOptions.indexOf(key) < 0 || yargs.parsed.argv[yargs.getOptions().showHiddenOpt]
434 }
435
436 self.showHelp = (level) => {
437 const logger = yargs._getLoggerInstance()
438 if (!level) level = 'error'
439 const emit = typeof level === 'function' ? level : logger[level]
440 emit(self.help())
441 }
442
443 self.functionDescription = (fn) => {
444 const description = fn.name ? decamelize(fn.name, '-') : __('generated-value')
445 return ['(', description, ')'].join('')
446 }
447
448 self.stringifiedValues = function stringifiedValues (values, separator) {
449 let string = ''
450 const sep = separator || ', '
451 const array = [].concat(values)
452
453 if (!values || !array.length) return string
454
455 array.forEach((value) => {
456 if (string.length) string += sep
457 string += JSON.stringify(value)
458 })
459
460 return string
461 }
462
463 // format the default-value-string displayed in
464 // the right-hand column.
465 function defaultString (value, defaultDescription) {
466 let string = `[${__('default:')} `
467
468 if (value === undefined && !defaultDescription) return null
469
470 if (defaultDescription) {
471 string += defaultDescription
472 } else {
473 switch (typeof value) {
474 case 'string':
475 string += `"${value}"`
476 break
477 case 'object':
478 string += JSON.stringify(value)
479 break
480 default:
481 string += value
482 }
483 }
484
485 return `${string}]`
486 }
487
488 // guess the width of the console window, max-width 80.
489 function windowWidth () {
490 const maxWidth = 80
491 if (typeof process === 'object' && process.stdout && process.stdout.columns) {
492 return Math.min(maxWidth, process.stdout.columns)
493 } else {
494 return maxWidth
495 }
496 }
497
498 // logic for displaying application version.
499 let version = null
500 self.version = (ver) => {
501 version = ver
502 }
503
504 self.showVersion = () => {
505 const logger = yargs._getLoggerInstance()
506 logger.log(version)
507 }
508
509 self.reset = function reset (localLookup) {
510 // do not reset wrap here
511 // do not reset fails here
512 failMessage = null
513 failureOutput = false
514 usages = []
515 usageDisabled = false
516 epilogs = []
517 examples = []
518 commands = []
519 descriptions = objFilter(descriptions, (k, v) => !localLookup[k])
520 return self
521 }
522
523 let frozens = []
524 self.freeze = function freeze () {
525 let frozen = {}
526 frozens.push(frozen)
527 frozen.failMessage = failMessage
528 frozen.failureOutput = failureOutput
529 frozen.usages = usages
530 frozen.usageDisabled = usageDisabled
531 frozen.epilogs = epilogs
532 frozen.examples = examples
533 frozen.commands = commands
534 frozen.descriptions = descriptions
535 }
536 self.unfreeze = function unfreeze () {
537 let frozen = frozens.pop()
538 failMessage = frozen.failMessage
539 failureOutput = frozen.failureOutput
540 usages = frozen.usages
541 usageDisabled = frozen.usageDisabled
542 epilogs = frozen.epilogs
543 examples = frozen.examples
544 commands = frozen.commands
545 descriptions = frozen.descriptions
546 }
547
548 return self
549}