1 | 'use strict'
|
---|
2 | // this file handles outputting usage instructions,
|
---|
3 | // failures, etc. keeps logging in one place.
|
---|
4 | const decamelize = require('./decamelize')
|
---|
5 | const stringWidth = require('string-width')
|
---|
6 | const objFilter = require('./obj-filter')
|
---|
7 | const path = require('path')
|
---|
8 | const setBlocking = require('set-blocking')
|
---|
9 | const YError = require('./yerror')
|
---|
10 |
|
---|
11 | module.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 epilog
|
---|
126 | self.epilog = (msg) => {
|
---|
127 | epilog = 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 | normalizeAliases()
|
---|
152 |
|
---|
153 | // handle old demanded API
|
---|
154 | const base$0 = path.basename(yargs.$0)
|
---|
155 | const demandedOptions = yargs.getDemandedOptions()
|
---|
156 | const demandedCommands = yargs.getDemandedCommands()
|
---|
157 | const groups = yargs.getGroups()
|
---|
158 | const options = yargs.getOptions()
|
---|
159 |
|
---|
160 | let keys = []
|
---|
161 | keys = keys.concat(Object.keys(descriptions))
|
---|
162 | keys = keys.concat(Object.keys(demandedOptions))
|
---|
163 | keys = keys.concat(Object.keys(demandedCommands))
|
---|
164 | keys = keys.concat(Object.keys(options.default))
|
---|
165 | keys = keys.filter(filterHiddenOptions)
|
---|
166 | keys = Object.keys(keys.reduce((acc, key) => {
|
---|
167 | if (key !== '_') acc[key] = true
|
---|
168 | return acc
|
---|
169 | }, {}))
|
---|
170 |
|
---|
171 | const theWrap = getWrap()
|
---|
172 | const ui = require('cliui')({
|
---|
173 | width: theWrap,
|
---|
174 | wrap: !!theWrap
|
---|
175 | })
|
---|
176 |
|
---|
177 | // the usage string.
|
---|
178 | if (!usageDisabled) {
|
---|
179 | if (usages.length) {
|
---|
180 | // user-defined usage.
|
---|
181 | usages.forEach((usage) => {
|
---|
182 | ui.div(`${usage[0].replace(/\$0/g, base$0)}`)
|
---|
183 | if (usage[1]) {
|
---|
184 | ui.div({ text: `${usage[1]}`, padding: [1, 0, 0, 0] })
|
---|
185 | }
|
---|
186 | })
|
---|
187 | ui.div()
|
---|
188 | } else if (commands.length) {
|
---|
189 | let u = null
|
---|
190 | // demonstrate how commands are used.
|
---|
191 | if (demandedCommands._) {
|
---|
192 | u = `${base$0} <${__('command')}>\n`
|
---|
193 | } else {
|
---|
194 | u = `${base$0} [${__('command')}]\n`
|
---|
195 | }
|
---|
196 | ui.div(`${u}`)
|
---|
197 | }
|
---|
198 | }
|
---|
199 |
|
---|
200 | // your application's commands, i.e., non-option
|
---|
201 | // arguments populated in '_'.
|
---|
202 | if (commands.length) {
|
---|
203 | ui.div(__('Commands:'))
|
---|
204 |
|
---|
205 | const context = yargs.getContext()
|
---|
206 | const parentCommands = context.commands.length ? `${context.commands.join(' ')} ` : ''
|
---|
207 |
|
---|
208 | if (yargs.getParserConfiguration()['sort-commands'] === true) {
|
---|
209 | commands = commands.sort((a, b) => a[0].localeCompare(b[0]))
|
---|
210 | }
|
---|
211 |
|
---|
212 | commands.forEach((command) => {
|
---|
213 | const commandString = `${base$0} ${parentCommands}${command[0].replace(/^\$0 ?/, '')}` // drop $0 from default commands.
|
---|
214 | ui.span(
|
---|
215 | {
|
---|
216 | text: commandString,
|
---|
217 | padding: [0, 2, 0, 2],
|
---|
218 | width: maxWidth(commands, theWrap, `${base$0}${parentCommands}`) + 4
|
---|
219 | },
|
---|
220 | { text: command[1] }
|
---|
221 | )
|
---|
222 | const hints = []
|
---|
223 | if (command[2]) hints.push(`[${__('default:').slice(0, -1)}]`) // TODO hacking around i18n here
|
---|
224 | if (command[3] && command[3].length) {
|
---|
225 | hints.push(`[${__('aliases:')} ${command[3].join(', ')}]`)
|
---|
226 | }
|
---|
227 | if (hints.length) {
|
---|
228 | ui.div({ text: hints.join(' '), padding: [0, 0, 0, 2], align: 'right' })
|
---|
229 | } else {
|
---|
230 | ui.div()
|
---|
231 | }
|
---|
232 | })
|
---|
233 |
|
---|
234 | ui.div()
|
---|
235 | }
|
---|
236 |
|
---|
237 | // perform some cleanup on the keys array, making it
|
---|
238 | // only include top-level keys not their aliases.
|
---|
239 | const aliasKeys = (Object.keys(options.alias) || [])
|
---|
240 | .concat(Object.keys(yargs.parsed.newAliases) || [])
|
---|
241 |
|
---|
242 | keys = keys.filter(key => !yargs.parsed.newAliases[key] && aliasKeys.every(alias => (options.alias[alias] || []).indexOf(key) === -1))
|
---|
243 |
|
---|
244 | // populate 'Options:' group with any keys that have not
|
---|
245 | // explicitly had a group set.
|
---|
246 | if (!groups[defaultGroup]) groups[defaultGroup] = []
|
---|
247 | addUngroupedKeys(keys, options.alias, groups)
|
---|
248 |
|
---|
249 | // display 'Options:' table along with any custom tables:
|
---|
250 | Object.keys(groups).forEach((groupName) => {
|
---|
251 | if (!groups[groupName].length) return
|
---|
252 |
|
---|
253 | // if we've grouped the key 'f', but 'f' aliases 'foobar',
|
---|
254 | // normalizedKeys should contain only 'foobar'.
|
---|
255 | const normalizedKeys = groups[groupName].filter(filterHiddenOptions).map((key) => {
|
---|
256 | if (~aliasKeys.indexOf(key)) return key
|
---|
257 | for (let i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) {
|
---|
258 | if (~(options.alias[aliasKey] || []).indexOf(key)) return aliasKey
|
---|
259 | }
|
---|
260 | return key
|
---|
261 | })
|
---|
262 |
|
---|
263 | if (normalizedKeys.length < 1) return
|
---|
264 |
|
---|
265 | ui.div(__(groupName))
|
---|
266 |
|
---|
267 | // actually generate the switches string --foo, -f, --bar.
|
---|
268 | const switches = normalizedKeys.reduce((acc, key) => {
|
---|
269 | acc[key] = [ key ].concat(options.alias[key] || [])
|
---|
270 | .map(sw => {
|
---|
271 | // for the special positional group don't
|
---|
272 | // add '--' or '-' prefix.
|
---|
273 | if (groupName === self.getPositionalGroupName()) return sw
|
---|
274 | else return (sw.length > 1 ? '--' : '-') + sw
|
---|
275 | })
|
---|
276 | .join(', ')
|
---|
277 |
|
---|
278 | return acc
|
---|
279 | }, {})
|
---|
280 |
|
---|
281 | normalizedKeys.forEach((key) => {
|
---|
282 | const kswitch = switches[key]
|
---|
283 | let desc = descriptions[key] || ''
|
---|
284 | let type = null
|
---|
285 |
|
---|
286 | if (~desc.lastIndexOf(deferY18nLookupPrefix)) desc = __(desc.substring(deferY18nLookupPrefix.length))
|
---|
287 |
|
---|
288 | if (~options.boolean.indexOf(key)) type = `[${__('boolean')}]`
|
---|
289 | if (~options.count.indexOf(key)) type = `[${__('count')}]`
|
---|
290 | if (~options.string.indexOf(key)) type = `[${__('string')}]`
|
---|
291 | if (~options.normalize.indexOf(key)) type = `[${__('string')}]`
|
---|
292 | if (~options.array.indexOf(key)) type = `[${__('array')}]`
|
---|
293 | if (~options.number.indexOf(key)) type = `[${__('number')}]`
|
---|
294 |
|
---|
295 | const extra = [
|
---|
296 | type,
|
---|
297 | (key in demandedOptions) ? `[${__('required')}]` : null,
|
---|
298 | options.choices && options.choices[key] ? `[${__('choices:')} ${
|
---|
299 | self.stringifiedValues(options.choices[key])}]` : null,
|
---|
300 | defaultString(options.default[key], options.defaultDescription[key])
|
---|
301 | ].filter(Boolean).join(' ')
|
---|
302 |
|
---|
303 | ui.span(
|
---|
304 | { text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches, theWrap) + 4 },
|
---|
305 | desc
|
---|
306 | )
|
---|
307 |
|
---|
308 | if (extra) ui.div({ text: extra, padding: [0, 0, 0, 2], align: 'right' })
|
---|
309 | else ui.div()
|
---|
310 | })
|
---|
311 |
|
---|
312 | ui.div()
|
---|
313 | })
|
---|
314 |
|
---|
315 | // describe some common use-cases for your application.
|
---|
316 | if (examples.length) {
|
---|
317 | ui.div(__('Examples:'))
|
---|
318 |
|
---|
319 | examples.forEach((example) => {
|
---|
320 | example[0] = example[0].replace(/\$0/g, base$0)
|
---|
321 | })
|
---|
322 |
|
---|
323 | examples.forEach((example) => {
|
---|
324 | if (example[1] === '') {
|
---|
325 | ui.div(
|
---|
326 | {
|
---|
327 | text: example[0],
|
---|
328 | padding: [0, 2, 0, 2]
|
---|
329 | }
|
---|
330 | )
|
---|
331 | } else {
|
---|
332 | ui.div(
|
---|
333 | {
|
---|
334 | text: example[0],
|
---|
335 | padding: [0, 2, 0, 2],
|
---|
336 | width: maxWidth(examples, theWrap) + 4
|
---|
337 | }, {
|
---|
338 | text: example[1]
|
---|
339 | }
|
---|
340 | )
|
---|
341 | }
|
---|
342 | })
|
---|
343 |
|
---|
344 | ui.div()
|
---|
345 | }
|
---|
346 |
|
---|
347 | // the usage string.
|
---|
348 | if (epilog) {
|
---|
349 | const e = epilog.replace(/\$0/g, base$0)
|
---|
350 | ui.div(`${e}\n`)
|
---|
351 | }
|
---|
352 |
|
---|
353 | // Remove the trailing white spaces
|
---|
354 | return ui.toString().replace(/\s*$/, '')
|
---|
355 | }
|
---|
356 |
|
---|
357 | // return the maximum width of a string
|
---|
358 | // in the left-hand column of a table.
|
---|
359 | function maxWidth (table, theWrap, modifier) {
|
---|
360 | let width = 0
|
---|
361 |
|
---|
362 | // table might be of the form [leftColumn],
|
---|
363 | // or {key: leftColumn}
|
---|
364 | if (!Array.isArray(table)) {
|
---|
365 | table = Object.keys(table).map(key => [table[key]])
|
---|
366 | }
|
---|
367 |
|
---|
368 | table.forEach((v) => {
|
---|
369 | width = Math.max(
|
---|
370 | stringWidth(modifier ? `${modifier} ${v[0]}` : v[0]),
|
---|
371 | width
|
---|
372 | )
|
---|
373 | })
|
---|
374 |
|
---|
375 | // if we've enabled 'wrap' we should limit
|
---|
376 | // the max-width of the left-column.
|
---|
377 | if (theWrap) width = Math.min(width, parseInt(theWrap * 0.5, 10))
|
---|
378 |
|
---|
379 | return width
|
---|
380 | }
|
---|
381 |
|
---|
382 | // make sure any options set for aliases,
|
---|
383 | // are copied to the keys being aliased.
|
---|
384 | function normalizeAliases () {
|
---|
385 | // handle old demanded API
|
---|
386 | const demandedOptions = yargs.getDemandedOptions()
|
---|
387 | const options = yargs.getOptions()
|
---|
388 |
|
---|
389 | ;(Object.keys(options.alias) || []).forEach((key) => {
|
---|
390 | options.alias[key].forEach((alias) => {
|
---|
391 | // copy descriptions.
|
---|
392 | if (descriptions[alias]) self.describe(key, descriptions[alias])
|
---|
393 | // copy demanded.
|
---|
394 | if (alias in demandedOptions) yargs.demandOption(key, demandedOptions[alias])
|
---|
395 | // type messages.
|
---|
396 | if (~options.boolean.indexOf(alias)) yargs.boolean(key)
|
---|
397 | if (~options.count.indexOf(alias)) yargs.count(key)
|
---|
398 | if (~options.string.indexOf(alias)) yargs.string(key)
|
---|
399 | if (~options.normalize.indexOf(alias)) yargs.normalize(key)
|
---|
400 | if (~options.array.indexOf(alias)) yargs.array(key)
|
---|
401 | if (~options.number.indexOf(alias)) yargs.number(key)
|
---|
402 | })
|
---|
403 | })
|
---|
404 | }
|
---|
405 |
|
---|
406 | // given a set of keys, place any keys that are
|
---|
407 | // ungrouped under the 'Options:' grouping.
|
---|
408 | function addUngroupedKeys (keys, aliases, groups) {
|
---|
409 | let groupedKeys = []
|
---|
410 | let toCheck = null
|
---|
411 | Object.keys(groups).forEach((group) => {
|
---|
412 | groupedKeys = groupedKeys.concat(groups[group])
|
---|
413 | })
|
---|
414 |
|
---|
415 | keys.forEach((key) => {
|
---|
416 | toCheck = [key].concat(aliases[key])
|
---|
417 | if (!toCheck.some(k => groupedKeys.indexOf(k) !== -1)) {
|
---|
418 | groups[defaultGroup].push(key)
|
---|
419 | }
|
---|
420 | })
|
---|
421 | return groupedKeys
|
---|
422 | }
|
---|
423 |
|
---|
424 | function filterHiddenOptions (key) {
|
---|
425 | return yargs.getOptions().hiddenOptions.indexOf(key) < 0 || yargs.parsed.argv[yargs.getOptions().showHiddenOpt]
|
---|
426 | }
|
---|
427 |
|
---|
428 | self.showHelp = (level) => {
|
---|
429 | const logger = yargs._getLoggerInstance()
|
---|
430 | if (!level) level = 'error'
|
---|
431 | const emit = typeof level === 'function' ? level : logger[level]
|
---|
432 | emit(self.help())
|
---|
433 | }
|
---|
434 |
|
---|
435 | self.functionDescription = (fn) => {
|
---|
436 | const description = fn.name ? decamelize(fn.name, '-') : __('generated-value')
|
---|
437 | return ['(', description, ')'].join('')
|
---|
438 | }
|
---|
439 |
|
---|
440 | self.stringifiedValues = function stringifiedValues (values, separator) {
|
---|
441 | let string = ''
|
---|
442 | const sep = separator || ', '
|
---|
443 | const array = [].concat(values)
|
---|
444 |
|
---|
445 | if (!values || !array.length) return string
|
---|
446 |
|
---|
447 | array.forEach((value) => {
|
---|
448 | if (string.length) string += sep
|
---|
449 | string += JSON.stringify(value)
|
---|
450 | })
|
---|
451 |
|
---|
452 | return string
|
---|
453 | }
|
---|
454 |
|
---|
455 | // format the default-value-string displayed in
|
---|
456 | // the right-hand column.
|
---|
457 | function defaultString (value, defaultDescription) {
|
---|
458 | let string = `[${__('default:')} `
|
---|
459 |
|
---|
460 | if (value === undefined && !defaultDescription) return null
|
---|
461 |
|
---|
462 | if (defaultDescription) {
|
---|
463 | string += defaultDescription
|
---|
464 | } else {
|
---|
465 | switch (typeof value) {
|
---|
466 | case 'string':
|
---|
467 | string += `"${value}"`
|
---|
468 | break
|
---|
469 | case 'object':
|
---|
470 | string += JSON.stringify(value)
|
---|
471 | break
|
---|
472 | default:
|
---|
473 | string += value
|
---|
474 | }
|
---|
475 | }
|
---|
476 |
|
---|
477 | return `${string}]`
|
---|
478 | }
|
---|
479 |
|
---|
480 | // guess the width of the console window, max-width 80.
|
---|
481 | function windowWidth () {
|
---|
482 | const maxWidth = 80
|
---|
483 | if (typeof process === 'object' && process.stdout && process.stdout.columns) {
|
---|
484 | return Math.min(maxWidth, process.stdout.columns)
|
---|
485 | } else {
|
---|
486 | return maxWidth
|
---|
487 | }
|
---|
488 | }
|
---|
489 |
|
---|
490 | // logic for displaying application version.
|
---|
491 | let version = null
|
---|
492 | self.version = (ver) => {
|
---|
493 | version = ver
|
---|
494 | }
|
---|
495 |
|
---|
496 | self.showVersion = () => {
|
---|
497 | const logger = yargs._getLoggerInstance()
|
---|
498 | logger.log(version)
|
---|
499 | }
|
---|
500 |
|
---|
501 | self.reset = function reset (localLookup) {
|
---|
502 | // do not reset wrap here
|
---|
503 | // do not reset fails here
|
---|
504 | failMessage = null
|
---|
505 | failureOutput = false
|
---|
506 | usages = []
|
---|
507 | usageDisabled = false
|
---|
508 | epilog = undefined
|
---|
509 | examples = []
|
---|
510 | commands = []
|
---|
511 | descriptions = objFilter(descriptions, (k, v) => !localLookup[k])
|
---|
512 | return self
|
---|
513 | }
|
---|
514 |
|
---|
515 | let frozen
|
---|
516 | self.freeze = function freeze () {
|
---|
517 | frozen = {}
|
---|
518 | frozen.failMessage = failMessage
|
---|
519 | frozen.failureOutput = failureOutput
|
---|
520 | frozen.usages = usages
|
---|
521 | frozen.usageDisabled = usageDisabled
|
---|
522 | frozen.epilog = epilog
|
---|
523 | frozen.examples = examples
|
---|
524 | frozen.commands = commands
|
---|
525 | frozen.descriptions = descriptions
|
---|
526 | }
|
---|
527 | self.unfreeze = function unfreeze () {
|
---|
528 | failMessage = frozen.failMessage
|
---|
529 | failureOutput = frozen.failureOutput
|
---|
530 | usages = frozen.usages
|
---|
531 | usageDisabled = frozen.usageDisabled
|
---|
532 | epilog = frozen.epilog
|
---|
533 | examples = frozen.examples
|
---|
534 | commands = frozen.commands
|
---|
535 | descriptions = frozen.descriptions
|
---|
536 | frozen = undefined
|
---|
537 | }
|
---|
538 |
|
---|
539 | return self
|
---|
540 | }
|
---|