1 // this file handles outputting usage instructions,
2 // failures, etc. keeps logging in one place.
3 var cliui = require('cliui')
4 var decamelize = require('decamelize')
5 var stringWidth = require('string-width')
6 var wsize = require('window-size')
8 module.exports = function (yargs, y18n) {
12 // methods for ouputting/building failure message.
14 self.failFn = function (f) {
18 var failMessage = null
19 var showHelpOnFail = true
20 self.showHelpOnFail = function (enabled, message) {
21 if (typeof enabled === 'string') {
24 } else if (typeof enabled === 'undefined') {
28 showHelpOnFail = enabled
32 var failureOutput = false
33 self.fail = function (msg) {
35 fails.forEach(function (f) {
39 // don't output failure message more than once
42 if (showHelpOnFail) yargs.showHelp('error')
43 if (msg) console.error(msg)
45 if (msg) console.error('')
46 console.error(failMessage)
49 if (yargs.getExitProcess()) {
57 // methods for ouputting/building help (usage) message.
59 self.usage = function (msg) {
64 self.example = function (cmd, description) {
65 examples.push([cmd, description || ''])
69 self.command = function (cmd, description) {
70 commands.push([cmd, description || ''])
72 self.getCommands = function () {
77 self.describe = function (key, desc) {
78 if (typeof key === 'object') {
79 Object.keys(key).forEach(function (k) {
80 self.describe(k, key[k])
83 descriptions[key] = desc
86 self.getDescriptions = function () {
91 self.epilog = function (msg) {
95 var wrap = windowWidth()
96 self.wrap = function (cols) {
100 var deferY18nLookupPrefix = '__yargsString__:'
101 self.deferY18nLookup = function (str) {
102 return deferY18nLookupPrefix + str
105 var defaultGroup = 'Options:'
106 self.help = function () {
109 var demanded = yargs.getDemanded()
110 var groups = yargs.getGroups()
111 var options = yargs.getOptions()
112 var keys = Object.keys(
113 Object.keys(descriptions)
114 .concat(Object.keys(demanded))
115 .concat(Object.keys(options.default))
116 .reduce(function (acc, key) {
117 if (key !== '_') acc[key] = true
128 var u = usage.replace(/\$0/g, yargs.$0)
132 // your application's commands, i.e., non-option
133 // arguments populated in '_'.
134 if (commands.length) {
135 ui.div(__('Commands:'))
137 commands.forEach(function (command) {
139 {text: command[0], padding: [0, 2, 0, 2], width: maxWidth(commands) + 4},
147 // perform some cleanup on the keys array, making it
148 // only include top-level keys not their aliases.
149 var aliasKeys = (Object.keys(options.alias) || [])
150 .concat(Object.keys(yargs.parsed.newAliases) || [])
152 keys = keys.filter(function (key) {
153 return !yargs.parsed.newAliases[key] && aliasKeys.every(function (alias) {
154 return (options.alias[alias] || []).indexOf(key) === -1
158 // populate 'Options:' group with any keys that have not
159 // explicitly had a group set.
160 if (!groups[defaultGroup]) groups[defaultGroup] = []
161 addUngroupedKeys(keys, options.alias, groups)
163 // display 'Options:' table along with any custom tables:
164 Object.keys(groups).forEach(function (groupName) {
165 if (!groups[groupName].length) return
167 ui.div(__(groupName))
169 // if we've grouped the key 'f', but 'f' aliases 'foobar',
170 // normalizedKeys should contain only 'foobar'.
171 var normalizedKeys = groups[groupName].map(function (key) {
172 if (~aliasKeys.indexOf(key)) return key
173 for (var i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) {
174 if (~(options.alias[aliasKey] || []).indexOf(key)) return aliasKey
179 // actually generate the switches string --foo, -f, --bar.
180 var switches = normalizedKeys.reduce(function (acc, key) {
181 acc[key] = [ key ].concat(options.alias[key] || [])
183 return (sw.length > 1 ? '--' : '-') + sw
190 normalizedKeys.forEach(function (key) {
191 var kswitch = switches[key]
192 var desc = descriptions[key] || ''
195 if (~desc.lastIndexOf(deferY18nLookupPrefix)) desc = __(desc.substring(deferY18nLookupPrefix.length))
197 if (~options.boolean.indexOf(key)) type = '[' + __('boolean') + ']'
198 if (~options.count.indexOf(key)) type = '[' + __('count') + ']'
199 if (~options.string.indexOf(key)) type = '[' + __('string') + ']'
200 if (~options.normalize.indexOf(key)) type = '[' + __('string') + ']'
201 if (~options.array.indexOf(key)) type = '[' + __('array') + ']'
205 demanded[key] ? '[' + __('required') + ']' : null,
206 options.choices && options.choices[key] ? '[' + __('choices:') + ' ' +
207 self.stringifiedValues(options.choices[key]) + ']' : null,
208 defaultString(options.default[key], options.defaultDescription[key])
209 ].filter(Boolean).join(' ')
212 {text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches) + 4},
216 if (extra) ui.div({text: extra, padding: [0, 0, 0, 2], align: 'right'})
223 // describe some common use-cases for your application.
224 if (examples.length) {
225 ui.div(__('Examples:'))
227 examples.forEach(function (example) {
228 example[0] = example[0].replace(/\$0/g, yargs.$0)
231 examples.forEach(function (example) {
233 {text: example[0], padding: [0, 2, 0, 2], width: maxWidth(examples) + 4},
243 var e = epilog.replace(/\$0/g, yargs.$0)
250 // return the maximum width of a string
251 // in the left-hand column of a table.
252 function maxWidth (table) {
255 // table might be of the form [leftColumn],
256 // or {key: leftColumn}}
257 if (!Array.isArray(table)) {
258 table = Object.keys(table).map(function (key) {
263 table.forEach(function (v) {
264 width = Math.max(stringWidth(v[0]), width)
267 // if we've enabled 'wrap' we should limit
268 // the max-width of the left-column.
269 if (wrap) width = Math.min(width, parseInt(wrap * 0.5, 10))
274 // make sure any options set for aliases,
275 // are copied to the keys being aliased.
276 function normalizeAliases () {
277 var demanded = yargs.getDemanded()
278 var options = yargs.getOptions()
280 ;(Object.keys(options.alias) || []).forEach(function (key) {
281 options.alias[key].forEach(function (alias) {
282 // copy descriptions.
283 if (descriptions[alias]) self.describe(key, descriptions[alias])
285 if (demanded[alias]) yargs.demand(key, demanded[alias].msg)
287 if (~options.boolean.indexOf(alias)) yargs.boolean(key)
288 if (~options.count.indexOf(alias)) yargs.count(key)
289 if (~options.string.indexOf(alias)) yargs.string(key)
290 if (~options.normalize.indexOf(alias)) yargs.normalize(key)
291 if (~options.array.indexOf(alias)) yargs.array(key)
296 // given a set of keys, place any keys that are
297 // ungrouped under the 'Options:' grouping.
298 function addUngroupedKeys (keys, aliases, groups) {
301 Object.keys(groups).forEach(function (group) {
302 groupedKeys = groupedKeys.concat(groups[group])
305 keys.forEach(function (key) {
306 toCheck = [key].concat(aliases[key])
307 if (!toCheck.some(function (k) {
308 return groupedKeys.indexOf(k) !== -1
310 groups[defaultGroup].push(key)
316 self.showHelp = function (level) {
317 level = level || 'error'
318 console[level](self.help())
321 self.functionDescription = function (fn) {
322 var description = fn.name ? decamelize(fn.name, '-') : __('generated-value')
323 return ['(', description, ')'].join('')
326 self.stringifiedValues = function (values, separator) {
328 var sep = separator || ', '
329 var array = [].concat(values)
331 if (!values || !array.length) return string
333 array.forEach(function (value) {
334 if (string.length) string += sep
335 string += JSON.stringify(value)
341 // format the default-value-string displayed in
342 // the right-hand column.
343 function defaultString (value, defaultDescription) {
344 var string = '[' + __('default:') + ' '
346 if (value === undefined && !defaultDescription) return null
348 if (defaultDescription) {
349 string += defaultDescription
351 switch (typeof value) {
353 string += JSON.stringify(value)
356 string += JSON.stringify(value)
366 // guess the width of the console window, max-width 80.
367 function windowWidth () {
368 return wsize.width ? Math.min(80, wsize.width) : null
371 // logic for displaying application version.
373 self.version = function (ver, opt, msg) {
377 self.showVersion = function () {
378 if (typeof version === 'function') console.log(version())
379 else console.log(version)