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} 550