1// this file handles outputting usage instructions, 2// failures, etc. keeps logging in one place. 3var cliui = require('cliui'), 4 decamelize = require('decamelize'), 5 wsize = require('window-size') 6 7module.exports = function (yargs) { 8 var self = {} 9 10 // methods for ouputting/building failure message. 11 var fails = [] 12 self.failFn = function (f) { 13 fails.push(f) 14 } 15 16 var failMessage = null 17 var showHelpOnFail = true 18 self.showHelpOnFail = function (enabled, message) { 19 if (typeof enabled === 'string') { 20 message = enabled 21 enabled = true 22 } else if (typeof enabled === 'undefined') { 23 enabled = true 24 } 25 failMessage = message 26 showHelpOnFail = enabled 27 return self 28 } 29 30 self.fail = function (msg) { 31 if (fails.length) { 32 fails.forEach(function (f) { 33 f(msg) 34 }) 35 } else { 36 if (showHelpOnFail) yargs.showHelp('error') 37 if (msg) console.error(msg) 38 if (failMessage) { 39 if (msg) console.error('') 40 console.error(failMessage) 41 } 42 if (yargs.getExitProcess()) { 43 process.exit(1) 44 } else { 45 throw new Error(msg) 46 } 47 } 48 } 49 50 // methods for ouputting/building help (usage) message. 51 var usage 52 self.usage = function (msg) { 53 usage = msg 54 } 55 56 var examples = [] 57 self.example = function (cmd, description) { 58 examples.push([cmd, description || '']) 59 } 60 61 var commands = [] 62 self.command = function (cmd, description) { 63 commands.push([cmd, description || '']) 64 } 65 self.getCommands = function () { 66 return commands 67 } 68 69 var descriptions = {} 70 self.describe = function (key, desc) { 71 if (typeof key === 'object') { 72 Object.keys(key).forEach(function (k) { 73 self.describe(k, key[k]) 74 }) 75 } else { 76 descriptions[key] = desc 77 } 78 } 79 self.getDescriptions = function () { 80 return descriptions 81 } 82 83 var epilog 84 self.epilog = function (msg) { 85 epilog = msg 86 } 87 88 var wrap = windowWidth() 89 self.wrap = function (cols) { 90 wrap = cols 91 } 92 93 self.help = function () { 94 normalizeAliases() 95 96 var demanded = yargs.getDemanded(), 97 options = yargs.getOptions(), 98 keys = Object.keys( 99 Object.keys(descriptions) 100 .concat(Object.keys(demanded)) 101 .concat(Object.keys(options.default)) 102 .reduce(function (acc, key) { 103 if (key !== '_') acc[key] = true 104 return acc 105 }, {}) 106 ), 107 ui = cliui({ 108 width: wrap, 109 wrap: !!wrap 110 }) 111 112 // the usage string. 113 if (usage) { 114 var u = usage.replace(/\$0/g, yargs.$0) 115 ui.div(u + '\n') 116 } 117 118 // your application's commands, i.e., non-option 119 // arguments populated in '_'. 120 if (commands.length) { 121 ui.div('Commands:') 122 123 commands.forEach(function (command) { 124 ui.div( 125 {text: command[0], padding: [0, 2, 0, 2], width: maxWidth(commands) + 4}, 126 {text: command[1]} 127 ) 128 }) 129 130 ui.div() 131 } 132 133 // the options table. 134 var aliasKeys = (Object.keys(options.alias) || []) 135 .concat(Object.keys(yargs.parsed.newAliases) || []) 136 137 keys = keys.filter(function (key) { 138 return !yargs.parsed.newAliases[key] && aliasKeys.every(function (alias) { 139 return (options.alias[alias] || []).indexOf(key) === -1 140 }) 141 }) 142 143 var switches = keys.reduce(function (acc, key) { 144 acc[key] = [ key ].concat(options.alias[key] || []) 145 .map(function (sw) { 146 return (sw.length > 1 ? '--' : '-') + sw 147 }) 148 .join(', ') 149 150 return acc 151 }, {}) 152 153 if (keys.length) { 154 ui.div('Options:') 155 156 keys.forEach(function (key) { 157 var kswitch = switches[key] 158 var desc = descriptions[key] || '' 159 var type = null 160 161 if (~options.boolean.indexOf(key)) type = '[boolean]' 162 if (~options.count.indexOf(key)) type = '[count]' 163 if (~options.string.indexOf(key)) type = '[string]' 164 if (~options.normalize.indexOf(key)) type = '[string]' 165 if (~options.array.indexOf(key)) type = '[array]' 166 167 var extra = [ 168 type, 169 demanded[key] ? '[required]' : null, 170 defaultString(options.default[key], options.defaultDescription[key]) 171 ].filter(Boolean).join(' ') 172 173 ui.span( 174 {text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches) + 4}, 175 desc 176 ) 177 178 if (extra) ui.div({text: extra, padding: [0, 0, 0, 2], align: 'right'}) 179 else ui.div() 180 }) 181 182 ui.div() 183 } 184 185 // describe some common use-cases for your application. 186 if (examples.length) { 187 ui.div('Examples:') 188 189 examples.forEach(function (example) { 190 example[0] = example[0].replace(/\$0/g, yargs.$0) 191 }) 192 193 examples.forEach(function (example) { 194 ui.div( 195 {text: example[0], padding: [0, 2, 0, 2], width: maxWidth(examples) + 4}, 196 example[1] 197 ) 198 }) 199 200 ui.div() 201 } 202 203 // the usage string. 204 if (epilog) { 205 var e = epilog.replace(/\$0/g, yargs.$0) 206 ui.div(e + '\n') 207 } 208 209 return ui.toString() 210 } 211 212 // return the maximum width of a string 213 // in the left-hand column of a table. 214 function maxWidth (table) { 215 var width = 0 216 217 // table might be of the form [leftColumn], 218 // or {key: leftColumn}} 219 if (!Array.isArray(table)) { 220 table = Object.keys(table).map(function (key) { 221 return [table[key]] 222 }) 223 } 224 225 table.forEach(function (v) { 226 width = Math.max(v[0].length, width) 227 }) 228 229 // if we've enabled 'wrap' we should limit 230 // the max-width of the left-column. 231 if (wrap) width = Math.min(width, parseInt(wrap * 0.5, 10)) 232 233 return width 234 } 235 236 // make sure any options set for aliases, 237 // are copied to the keys being aliased. 238 function normalizeAliases () { 239 var options = yargs.getOptions(), 240 demanded = yargs.getDemanded() 241 242 ;(Object.keys(options.alias) || []).forEach(function (key) { 243 options.alias[key].forEach(function (alias) { 244 // copy descriptions. 245 if (descriptions[alias]) self.describe(key, descriptions[alias]) 246 // copy demanded. 247 if (demanded[alias]) yargs.demand(key, demanded[alias].msg) 248 249 // type messages. 250 if (~options.boolean.indexOf(alias)) yargs.boolean(key) 251 if (~options.count.indexOf(alias)) yargs.count(key) 252 if (~options.string.indexOf(alias)) yargs.string(key) 253 if (~options.normalize.indexOf(alias)) yargs.normalize(key) 254 if (~options.array.indexOf(alias)) yargs.array(key) 255 }) 256 }) 257 } 258 259 self.showHelp = function (level) { 260 level = level || 'error' 261 console[level](self.help()) 262 } 263 264 self.functionDescription = function (fn, defaultDescription) { 265 if (defaultDescription) { 266 return defaultDescription 267 } 268 var description = fn.name ? decamelize(fn.name, '-') : 'generated-value' 269 return ['(', description, ')'].join('') 270 } 271 272 // format the default-value-string displayed in 273 // the right-hand column. 274 function defaultString (value, defaultDescription) { 275 var string = '[default: ' 276 277 if (value === undefined) return null 278 279 if (defaultDescription) { 280 string += defaultDescription 281 } else { 282 switch (typeof value) { 283 case 'string': 284 string += JSON.stringify(value) 285 break 286 case 'object': 287 string += JSON.stringify(value) 288 break 289 default: 290 string += value 291 } 292 } 293 294 return string + ']' 295 } 296 297 // guess the width of the console window, max-width 80. 298 function windowWidth () { 299 return wsize.width ? Math.min(80, wsize.width) : null 300 } 301 302 // logic for displaying application version. 303 var version = null 304 self.version = function (ver, opt, msg) { 305 version = ver 306 } 307 308 self.showVersion = function () { 309 if (typeof version === 'function') console.log(version()) 310 else console.log(version) 311 } 312 313 return self 314} 315