2 // this file handles outputting usage instructions,
3 // failures, etc. keeps logging in one place.
4 const stringWidth = require('string-width')
5 const objFilter = require('./obj-filter')
6 const path = require('path')
7 const setBlocking = require('set-blocking')
8 const YError = require('./yerror')
10 module.exports = function usage (yargs, y18n) {
14 // methods for ouputting/building failure message.
16 self.failFn = function failFn (f) {
20 let failMessage = null
21 let showHelpOnFail = true
22 self.showHelpOnFail = function showHelpOnFailFn (enabled, message) {
23 if (typeof enabled === 'string') {
26 } else if (typeof enabled === 'undefined') {
30 showHelpOnFail = enabled
34 let failureOutput = false
35 self.fail = function fail (msg, err) {
36 const logger = yargs._getLoggerInstance()
39 for (let i = fails.length - 1; i >= 0; --i) {
40 fails[i](msg, err, self)
43 if (yargs.getExitProcess()) setBlocking(true)
45 // don't output failure message more than once
48 if (showHelpOnFail) yargs.showHelp('error')
49 if (msg || err) logger.error(msg || err)
51 if (msg || err) logger.error('')
52 logger.error(failMessage)
56 err = err || new YError(msg)
57 if (yargs.getExitProcess()) {
59 } else if (yargs._hasParseCallback()) {
60 return yargs.exit(1, err)
67 // methods for ouputting/building help (usage) message.
69 let usageDisabled = false
70 self.usage = (msg, description) => {
77 usages.push([msg, description || ''])
80 self.getUsage = () => {
83 self.getUsageDisabled = () => {
87 self.getPositionalGroupName = () => {
88 return __('Positionals:')
92 self.example = (cmd, description) => {
93 examples.push([cmd, description || ''])
97 self.command = function command (cmd, description, isDefault, aliases) {
98 // the last default wins, so cancel out any previously set default
100 commands = commands.map((cmdArray) => {
105 commands.push([cmd, description || '', isDefault, aliases])
107 self.getCommands = () => commands
109 let descriptions = {}
110 self.describe = function describe (key, desc) {
111 if (typeof key === 'object') {
112 Object.keys(key).forEach((k) => {
113 self.describe(k, key[k])
116 descriptions[key] = desc
119 self.getDescriptions = () => descriptions
122 self.epilog = (msg) => {
128 self.wrap = (cols) => {
133 function getWrap () {
142 const deferY18nLookupPrefix = '__yargsString__:'
143 self.deferY18nLookup = str => deferY18nLookupPrefix + str
145 const defaultGroup = 'Options:'
146 self.help = function help () {
149 // handle old demanded API
150 const base$0 = path.basename(yargs.$0)
151 const demandedOptions = yargs.getDemandedOptions()
152 const demandedCommands = yargs.getDemandedCommands()
153 const groups = yargs.getGroups()
154 const options = yargs.getOptions()
155 let keys = Object.keys(
156 Object.keys(descriptions)
157 .concat(Object.keys(demandedOptions))
158 .concat(Object.keys(demandedCommands))
159 .concat(Object.keys(options.default))
160 .reduce((acc, key) => {
161 if (key !== '_') acc[key] = true
166 const theWrap = getWrap()
167 const ui = require('cliui')({
173 if (!usageDisabled) {
175 // user-defined usage.
176 usages.forEach((usage) => {
177 ui.div(`${usage[0].replace(/\$0/g, base$0)}`)
179 ui.div({text: `${usage[1]}`, padding: [1, 0, 0, 0]})
183 } else if (commands.length) {
185 // demonstrate how commands are used.
186 if (demandedCommands._) {
187 u = `${base$0} <${__('command')}>\n`
189 u = `${base$0} [${__('command')}]\n`
195 // your application's commands, i.e., non-option
196 // arguments populated in '_'.
197 if (commands.length) {
198 ui.div(__('Commands:'))
200 const context = yargs.getContext()
201 const parentCommands = context.commands.length ? `${context.commands.join(' ')} ` : ''
203 commands.forEach((command) => {
204 const commandString = `${base$0} ${parentCommands}${command[0].replace(/^\$0 ?/, '')}` // drop $0 from default commands.
208 padding: [0, 2, 0, 2],
209 width: maxWidth(commands, theWrap, `${base$0}${parentCommands}`) + 4
214 if (command[2]) hints.push(`[${__('default:').slice(0, -1)}]`) // TODO hacking around i18n here
215 if (command[3] && command[3].length) {
216 hints.push(`[${__('aliases:')} ${command[3].join(', ')}]`)
219 ui.div({text: hints.join(' '), padding: [0, 0, 0, 2], align: 'right'})
228 // perform some cleanup on the keys array, making it
229 // only include top-level keys not their aliases.
230 const aliasKeys = (Object.keys(options.alias) || [])
231 .concat(Object.keys(yargs.parsed.newAliases) || [])
233 keys = keys.filter(key => !yargs.parsed.newAliases[key] && aliasKeys.every(alias => (options.alias[alias] || []).indexOf(key) === -1))
235 // populate 'Options:' group with any keys that have not
236 // explicitly had a group set.
237 if (!groups[defaultGroup]) groups[defaultGroup] = []
238 addUngroupedKeys(keys, options.alias, groups)
240 // display 'Options:' table along with any custom tables:
241 Object.keys(groups).forEach((groupName) => {
242 if (!groups[groupName].length) return
244 ui.div(__(groupName))
246 // if we've grouped the key 'f', but 'f' aliases 'foobar',
247 // normalizedKeys should contain only 'foobar'.
248 const normalizedKeys = groups[groupName].map((key) => {
249 if (~aliasKeys.indexOf(key)) return key
250 for (let i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) {
251 if (~(options.alias[aliasKey] || []).indexOf(key)) return aliasKey
256 // actually generate the switches string --foo, -f, --bar.
257 const switches = normalizedKeys.reduce((acc, key) => {
258 acc[key] = [ key ].concat(options.alias[key] || [])
260 // for the special positional group don't
261 // add '--' or '-' prefix.
262 if (groupName === self.getPositionalGroupName()) return sw
263 else return (sw.length > 1 ? '--' : '-') + sw
270 normalizedKeys.forEach((key) => {
271 const kswitch = switches[key]
272 let desc = descriptions[key] || ''
275 if (~desc.lastIndexOf(deferY18nLookupPrefix)) desc = __(desc.substring(deferY18nLookupPrefix.length))
277 if (~options.boolean.indexOf(key)) type = `[${__('boolean')}]`
278 if (~options.count.indexOf(key)) type = `[${__('count')}]`
279 if (~options.string.indexOf(key)) type = `[${__('string')}]`
280 if (~options.normalize.indexOf(key)) type = `[${__('string')}]`
281 if (~options.array.indexOf(key)) type = `[${__('array')}]`
282 if (~options.number.indexOf(key)) type = `[${__('number')}]`
286 (key in demandedOptions) ? `[${__('required')}]` : null,
287 options.choices && options.choices[key] ? `[${__('choices:')} ${
288 self.stringifiedValues(options.choices[key])}]` : null,
289 defaultString(options.default[key], options.defaultDescription[key])
290 ].filter(Boolean).join(' ')
293 {text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches, theWrap) + 4},
297 if (extra) ui.div({text: extra, padding: [0, 0, 0, 2], align: 'right'})
304 // describe some common use-cases for your application.
305 if (examples.length) {
306 ui.div(__('Examples:'))
308 examples.forEach((example) => {
309 example[0] = example[0].replace(/\$0/g, base$0)
312 examples.forEach((example) => {
313 if (example[1] === '') {
317 padding: [0, 2, 0, 2]
324 padding: [0, 2, 0, 2],
325 width: maxWidth(examples, theWrap) + 4
338 const e = epilog.replace(/\$0/g, base$0)
345 // return the maximum width of a string
346 // in the left-hand column of a table.
347 function maxWidth (table, theWrap, modifier) {
350 // table might be of the form [leftColumn],
351 // or {key: leftColumn}
352 if (!Array.isArray(table)) {
353 table = Object.keys(table).map(key => [table[key]])
356 table.forEach((v) => {
358 stringWidth(modifier ? `${modifier} ${v[0]}` : v[0]),
363 // if we've enabled 'wrap' we should limit
364 // the max-width of the left-column.
365 if (theWrap) width = Math.min(width, parseInt(theWrap * 0.5, 10))
370 // make sure any options set for aliases,
371 // are copied to the keys being aliased.
372 function normalizeAliases () {
373 // handle old demanded API
374 const demandedOptions = yargs.getDemandedOptions()
375 const options = yargs.getOptions()
377 ;(Object.keys(options.alias) || []).forEach((key) => {
378 options.alias[key].forEach((alias) => {
379 // copy descriptions.
380 if (descriptions[alias]) self.describe(key, descriptions[alias])
382 if (alias in demandedOptions) yargs.demandOption(key, demandedOptions[alias])
384 if (~options.boolean.indexOf(alias)) yargs.boolean(key)
385 if (~options.count.indexOf(alias)) yargs.count(key)
386 if (~options.string.indexOf(alias)) yargs.string(key)
387 if (~options.normalize.indexOf(alias)) yargs.normalize(key)
388 if (~options.array.indexOf(alias)) yargs.array(key)
389 if (~options.number.indexOf(alias)) yargs.number(key)
394 // given a set of keys, place any keys that are
395 // ungrouped under the 'Options:' grouping.
396 function addUngroupedKeys (keys, aliases, groups) {
399 Object.keys(groups).forEach((group) => {
400 groupedKeys = groupedKeys.concat(groups[group])
403 keys.forEach((key) => {
404 toCheck = [key].concat(aliases[key])
405 if (!toCheck.some(k => groupedKeys.indexOf(k) !== -1)) {
406 groups[defaultGroup].push(key)
412 self.showHelp = (level) => {
413 const logger = yargs._getLoggerInstance()
414 if (!level) level = 'error'
415 const emit = typeof level === 'function' ? level : logger[level]
419 self.functionDescription = (fn) => {
420 const description = fn.name ? require('decamelize')(fn.name, '-') : __('generated-value')
421 return ['(', description, ')'].join('')
424 self.stringifiedValues = function stringifiedValues (values, separator) {
426 const sep = separator || ', '
427 const array = [].concat(values)
429 if (!values || !array.length) return string
431 array.forEach((value) => {
432 if (string.length) string += sep
433 string += JSON.stringify(value)
439 // format the default-value-string displayed in
440 // the right-hand column.
441 function defaultString (value, defaultDescription) {
442 let string = `[${__('default:')} `
444 if (value === undefined && !defaultDescription) return null
446 if (defaultDescription) {
447 string += defaultDescription
449 switch (typeof value) {
451 string += `"${value}"`
454 string += JSON.stringify(value)
464 // guess the width of the console window, max-width 80.
465 function windowWidth () {
467 if (typeof process === 'object' && process.stdout && process.stdout.columns) {
468 return Math.min(maxWidth, process.stdout.columns)
474 // logic for displaying application version.
476 self.version = (ver) => {
480 self.showVersion = () => {
481 const logger = yargs._getLoggerInstance()
485 self.reset = function reset (localLookup) {
486 // do not reset wrap here
487 // do not reset fails here
489 failureOutput = false
491 usageDisabled = false
495 descriptions = objFilter(descriptions, (k, v) => !localLookup[k])
500 self.freeze = function freeze () {
502 frozen.failMessage = failMessage
503 frozen.failureOutput = failureOutput
504 frozen.usages = usages
505 frozen.usageDisabled = usageDisabled
506 frozen.epilog = epilog
507 frozen.examples = examples
508 frozen.commands = commands
509 frozen.descriptions = descriptions
511 self.unfreeze = function unfreeze () {
512 failMessage = frozen.failMessage
513 failureOutput = frozen.failureOutput
514 usages = frozen.usages
515 usageDisabled = frozen.usageDisabled
516 epilog = frozen.epilog
517 examples = frozen.examples
518 commands = frozen.commands
519 descriptions = frozen.descriptions