1 // this file handles outputting usage instructions,
2 // failures, etc. keeps logging in one place.
3 const cliui = require('cliui')
4 const decamelize = require('decamelize')
5 const stringWidth = require('string-width')
6 const wsize = require('window-size')
7 const objFilter = require('./obj-filter')
8 const setBlocking = require('set-blocking')
10 module.exports = function (yargs, y18n) {
14 // methods for ouputting/building failure message.
16 self.failFn = function (f) {
20 var failMessage = null
21 var showHelpOnFail = true
22 self.showHelpOnFail = function (enabled, message) {
23 if (typeof enabled === 'string') {
26 } else if (typeof enabled === 'undefined') {
30 showHelpOnFail = enabled
34 var failureOutput = false
35 self.fail = function (msg, err) {
37 fails.forEach(function (f) {
41 if (yargs.getExitProcess()) setBlocking(true)
43 // don't output failure message more than once
46 if (showHelpOnFail) yargs.showHelp('error')
47 if (msg) console.error(msg)
49 if (msg) console.error('')
50 console.error(failMessage)
53 if (yargs.getExitProcess()) {
56 throw err || new Error(msg)
61 // methods for ouputting/building help (usage) message.
63 self.usage = function (msg) {
66 self.getUsage = function () {
71 self.example = function (cmd, description) {
72 examples.push([cmd, description || ''])
76 self.command = function (cmd, description) {
77 commands.push([cmd, description || ''])
79 self.getCommands = function () {
84 self.describe = function (key, desc) {
85 if (typeof key === 'object') {
86 Object.keys(key).forEach(function (k) {
87 self.describe(k, key[k])
90 descriptions[key] = desc
93 self.getDescriptions = function () {
98 self.epilog = function (msg) {
102 var wrap = windowWidth()
103 self.wrap = function (cols) {
107 var deferY18nLookupPrefix = '__yargsString__:'
108 self.deferY18nLookup = function (str) {
109 return deferY18nLookupPrefix + str
112 var defaultGroup = 'Options:'
113 self.help = function () {
116 var demanded = yargs.getDemanded()
117 var groups = yargs.getGroups()
118 var options = yargs.getOptions()
119 var keys = Object.keys(
120 Object.keys(descriptions)
121 .concat(Object.keys(demanded))
122 .concat(Object.keys(options.default))
123 .reduce(function (acc, key) {
124 if (key !== '_') acc[key] = true
135 var u = usage.replace(/\$0/g, yargs.$0)
139 // your application's commands, i.e., non-option
140 // arguments populated in '_'.
141 if (commands.length) {
142 ui.div(__('Commands:'))
144 commands.forEach(function (command) {
146 {text: command[0], padding: [0, 2, 0, 2], width: maxWidth(commands) + 4},
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) || [])
159 keys = keys.filter(function (key) {
160 return !yargs.parsed.newAliases[key] && aliasKeys.every(function (alias) {
161 return (options.alias[alias] || []).indexOf(key) === -1
165 // populate 'Options:' group with any keys that have not
166 // explicitly had a group set.
167 if (!groups[defaultGroup]) groups[defaultGroup] = []
168 addUngroupedKeys(keys, options.alias, groups)
170 // display 'Options:' table along with any custom tables:
171 Object.keys(groups).forEach(function (groupName) {
172 if (!groups[groupName].length) return
174 ui.div(__(groupName))
176 // if we've grouped the key 'f', but 'f' aliases 'foobar',
177 // normalizedKeys should contain only 'foobar'.
178 var normalizedKeys = groups[groupName].map(function (key) {
179 if (~aliasKeys.indexOf(key)) return key
180 for (var i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) {
181 if (~(options.alias[aliasKey] || []).indexOf(key)) return aliasKey
186 // actually generate the switches string --foo, -f, --bar.
187 var switches = normalizedKeys.reduce(function (acc, key) {
188 acc[key] = [ key ].concat(options.alias[key] || [])
190 return (sw.length > 1 ? '--' : '-') + sw
197 normalizedKeys.forEach(function (key) {
198 var kswitch = switches[key]
199 var desc = descriptions[key] || ''
202 if (~desc.lastIndexOf(deferY18nLookupPrefix)) desc = __(desc.substring(deferY18nLookupPrefix.length))
204 if (~options.boolean.indexOf(key)) type = '[' + __('boolean') + ']'
205 if (~options.count.indexOf(key)) type = '[' + __('count') + ']'
206 if (~options.string.indexOf(key)) type = '[' + __('string') + ']'
207 if (~options.normalize.indexOf(key)) type = '[' + __('string') + ']'
208 if (~options.array.indexOf(key)) type = '[' + __('array') + ']'
209 if (~options.number.indexOf(key)) type = '[' + __('number') + ']'
213 demanded[key] ? '[' + __('required') + ']' : null,
214 options.choices && options.choices[key] ? '[' + __('choices:') + ' ' +
215 self.stringifiedValues(options.choices[key]) + ']' : null,
216 defaultString(options.default[key], options.defaultDescription[key])
217 ].filter(Boolean).join(' ')
220 {text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches) + 4},
224 if (extra) ui.div({text: extra, padding: [0, 0, 0, 2], align: 'right'})
231 // describe some common use-cases for your application.
232 if (examples.length) {
233 ui.div(__('Examples:'))
235 examples.forEach(function (example) {
236 example[0] = example[0].replace(/\$0/g, yargs.$0)
239 examples.forEach(function (example) {
241 {text: example[0], padding: [0, 2, 0, 2], width: maxWidth(examples) + 4},
251 var e = epilog.replace(/\$0/g, yargs.$0)
258 // return the maximum width of a string
259 // in the left-hand column of a table.
260 function maxWidth (table) {
263 // table might be of the form [leftColumn],
264 // or {key: leftColumn}}
265 if (!Array.isArray(table)) {
266 table = Object.keys(table).map(function (key) {
271 table.forEach(function (v) {
272 width = Math.max(stringWidth(v[0]), width)
275 // if we've enabled 'wrap' we should limit
276 // the max-width of the left-column.
277 if (wrap) width = Math.min(width, parseInt(wrap * 0.5, 10))
282 // make sure any options set for aliases,
283 // are copied to the keys being aliased.
284 function normalizeAliases () {
285 var demanded = yargs.getDemanded()
286 var options = yargs.getOptions()
288 ;(Object.keys(options.alias) || []).forEach(function (key) {
289 options.alias[key].forEach(function (alias) {
290 // copy descriptions.
291 if (descriptions[alias]) self.describe(key, descriptions[alias])
293 if (demanded[alias]) yargs.demand(key, demanded[alias].msg)
295 if (~options.boolean.indexOf(alias)) yargs.boolean(key)
296 if (~options.count.indexOf(alias)) yargs.count(key)
297 if (~options.string.indexOf(alias)) yargs.string(key)
298 if (~options.normalize.indexOf(alias)) yargs.normalize(key)
299 if (~options.array.indexOf(alias)) yargs.array(key)
300 if (~options.number.indexOf(alias)) yargs.number(key)
305 // given a set of keys, place any keys that are
306 // ungrouped under the 'Options:' grouping.
307 function addUngroupedKeys (keys, aliases, groups) {
310 Object.keys(groups).forEach(function (group) {
311 groupedKeys = groupedKeys.concat(groups[group])
314 keys.forEach(function (key) {
315 toCheck = [key].concat(aliases[key])
316 if (!toCheck.some(function (k) {
317 return groupedKeys.indexOf(k) !== -1
319 groups[defaultGroup].push(key)
325 self.showHelp = function (level) {
326 if (!level) level = 'error'
327 var emit = typeof level === 'function' ? level : console[ level ]
331 self.functionDescription = function (fn) {
332 var description = fn.name ? decamelize(fn.name, '-') : __('generated-value')
333 return ['(', description, ')'].join('')
336 self.stringifiedValues = function (values, separator) {
338 var sep = separator || ', '
339 var array = [].concat(values)
341 if (!values || !array.length) return string
343 array.forEach(function (value) {
344 if (string.length) string += sep
345 string += JSON.stringify(value)
351 // format the default-value-string displayed in
352 // the right-hand column.
353 function defaultString (value, defaultDescription) {
354 var string = '[' + __('default:') + ' '
356 if (value === undefined && !defaultDescription) return null
358 if (defaultDescription) {
359 string += defaultDescription
361 switch (typeof value) {
363 string += JSON.stringify(value)
366 string += JSON.stringify(value)
376 // guess the width of the console window, max-width 80.
377 function windowWidth () {
378 return wsize.width ? Math.min(80, wsize.width) : null
381 // logic for displaying application version.
383 self.version = function (ver) {
387 self.showVersion = function () {
388 if (typeof version === 'function') console.log(version())
389 else console.log(version)
392 self.reset = function (globalLookup) {
393 // do not reset wrap here
396 failureOutput = false
401 descriptions = objFilter(descriptions, function (k, v) {
402 return globalLookup[k]