Projekt

Obecné

Profil

Stáhnout (15 KB) Statistiky
| Větev: | Revize:
1
'use strict'
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')
9

    
10
module.exports = function usage (yargs, y18n) {
11
  const __ = y18n.__
12
  const self = {}
13

    
14
  // methods for ouputting/building failure message.
15
  const fails = []
16
  self.failFn = function failFn (f) {
17
    fails.push(f)
18
  }
19

    
20
  let failMessage = null
21
  let showHelpOnFail = true
22
  self.showHelpOnFail = function showHelpOnFailFn (enabled, message) {
23
    if (typeof enabled === 'string') {
24
      message = enabled
25
      enabled = true
26
    } else if (typeof enabled === 'undefined') {
27
      enabled = true
28
    }
29
    failMessage = message
30
    showHelpOnFail = enabled
31
    return self
32
  }
33

    
34
  let failureOutput = false
35
  self.fail = function fail (msg, err) {
36
    const logger = yargs._getLoggerInstance()
37

    
38
    if (fails.length) {
39
      for (let i = fails.length - 1; i >= 0; --i) {
40
        fails[i](msg, err, self)
41
      }
42
    } else {
43
      if (yargs.getExitProcess()) setBlocking(true)
44

    
45
      // don't output failure message more than once
46
      if (!failureOutput) {
47
        failureOutput = true
48
        if (showHelpOnFail) {
49
          yargs.showHelp('error')
50
          logger.error()
51
        }
52
        if (msg || err) logger.error(msg || err)
53
        if (failMessage) {
54
          if (msg || err) logger.error('')
55
          logger.error(failMessage)
56
        }
57
      }
58

    
59
      err = err || new YError(msg)
60
      if (yargs.getExitProcess()) {
61
        return yargs.exit(1)
62
      } else if (yargs._hasParseCallback()) {
63
        return yargs.exit(1, err)
64
      } else {
65
        throw err
66
      }
67
    }
68
  }
69

    
70
  // methods for ouputting/building help (usage) message.
71
  let usages = []
72
  let usageDisabled = false
73
  self.usage = (msg, description) => {
74
    if (msg === null) {
75
      usageDisabled = true
76
      usages = []
77
      return
78
    }
79
    usageDisabled = false
80
    usages.push([msg, description || ''])
81
    return self
82
  }
83
  self.getUsage = () => {
84
    return usages
85
  }
86
  self.getUsageDisabled = () => {
87
    return usageDisabled
88
  }
89

    
90
  self.getPositionalGroupName = () => {
91
    return __('Positionals:')
92
  }
93

    
94
  let examples = []
95
  self.example = (cmd, description) => {
96
    examples.push([cmd, description || ''])
97
  }
98

    
99
  let commands = []
100
  self.command = function command (cmd, description, isDefault, aliases) {
101
    // the last default wins, so cancel out any previously set default
102
    if (isDefault) {
103
      commands = commands.map((cmdArray) => {
104
        cmdArray[2] = false
105
        return cmdArray
106
      })
107
    }
108
    commands.push([cmd, description || '', isDefault, aliases])
109
  }
110
  self.getCommands = () => commands
111

    
112
  let descriptions = {}
113
  self.describe = function describe (key, desc) {
114
    if (typeof key === 'object') {
115
      Object.keys(key).forEach((k) => {
116
        self.describe(k, key[k])
117
      })
118
    } else {
119
      descriptions[key] = desc
120
    }
121
  }
122
  self.getDescriptions = () => descriptions
123

    
124
  let epilog
125
  self.epilog = (msg) => {
126
    epilog = msg
127
  }
128

    
129
  let wrapSet = false
130
  let wrap
131
  self.wrap = (cols) => {
132
    wrapSet = true
133
    wrap = cols
134
  }
135

    
136
  function getWrap () {
137
    if (!wrapSet) {
138
      wrap = windowWidth()
139
      wrapSet = true
140
    }
141

    
142
    return wrap
143
  }
144

    
145
  const deferY18nLookupPrefix = '__yargsString__:'
146
  self.deferY18nLookup = str => deferY18nLookupPrefix + str
147

    
148
  const defaultGroup = 'Options:'
149
  self.help = function help () {
150
    normalizeAliases()
151

    
152
    // handle old demanded API
153
    const base$0 = path.basename(yargs.$0)
154
    const demandedOptions = yargs.getDemandedOptions()
155
    const demandedCommands = yargs.getDemandedCommands()
156
    const groups = yargs.getGroups()
157
    const options = yargs.getOptions()
158

    
159
    let keys = []
160
    keys = keys.concat(Object.keys(descriptions))
161
    keys = keys.concat(Object.keys(demandedOptions))
162
    keys = keys.concat(Object.keys(demandedCommands))
163
    keys = keys.concat(Object.keys(options.default))
164
    keys = keys.filter(filterHiddenOptions)
165
    keys = Object.keys(keys.reduce((acc, key) => {
166
      if (key !== '_') acc[key] = true
167
      return acc
168
    }, {}))
169

    
170
    const theWrap = getWrap()
171
    const ui = require('cliui')({
172
      width: theWrap,
173
      wrap: !!theWrap
174
    })
175

    
176
    // the usage string.
177
    if (!usageDisabled) {
178
      if (usages.length) {
179
        // user-defined usage.
180
        usages.forEach((usage) => {
181
          ui.div(`${usage[0].replace(/\$0/g, base$0)}`)
182
          if (usage[1]) {
183
            ui.div({text: `${usage[1]}`, padding: [1, 0, 0, 0]})
184
          }
185
        })
186
        ui.div()
187
      } else if (commands.length) {
188
        let u = null
189
        // demonstrate how commands are used.
190
        if (demandedCommands._) {
191
          u = `${base$0} <${__('command')}>\n`
192
        } else {
193
          u = `${base$0} [${__('command')}]\n`
194
        }
195
        ui.div(`${u}`)
196
      }
197
    }
198

    
199
    // your application's commands, i.e., non-option
200
    // arguments populated in '_'.
201
    if (commands.length) {
202
      ui.div(__('Commands:'))
203

    
204
      const context = yargs.getContext()
205
      const parentCommands = context.commands.length ? `${context.commands.join(' ')} ` : ''
206

    
207
      commands.forEach((command) => {
208
        const commandString = `${base$0} ${parentCommands}${command[0].replace(/^\$0 ?/, '')}` // drop $0 from default commands.
209
        ui.span(
210
          {
211
            text: commandString,
212
            padding: [0, 2, 0, 2],
213
            width: maxWidth(commands, theWrap, `${base$0}${parentCommands}`) + 4
214
          },
215
          {text: command[1]}
216
        )
217
        const hints = []
218
        if (command[2]) hints.push(`[${__('default:').slice(0, -1)}]`) // TODO hacking around i18n here
219
        if (command[3] && command[3].length) {
220
          hints.push(`[${__('aliases:')} ${command[3].join(', ')}]`)
221
        }
222
        if (hints.length) {
223
          ui.div({text: hints.join(' '), padding: [0, 0, 0, 2], align: 'right'})
224
        } else {
225
          ui.div()
226
        }
227
      })
228

    
229
      ui.div()
230
    }
231

    
232
    // perform some cleanup on the keys array, making it
233
    // only include top-level keys not their aliases.
234
    const aliasKeys = (Object.keys(options.alias) || [])
235
      .concat(Object.keys(yargs.parsed.newAliases) || [])
236

    
237
    keys = keys.filter(key => !yargs.parsed.newAliases[key] && aliasKeys.every(alias => (options.alias[alias] || []).indexOf(key) === -1))
238

    
239
    // populate 'Options:' group with any keys that have not
240
    // explicitly had a group set.
241
    if (!groups[defaultGroup]) groups[defaultGroup] = []
242
    addUngroupedKeys(keys, options.alias, groups)
243

    
244
    // display 'Options:' table along with any custom tables:
245
    Object.keys(groups).forEach((groupName) => {
246
      if (!groups[groupName].length) return
247

    
248
      // if we've grouped the key 'f', but 'f' aliases 'foobar',
249
      // normalizedKeys should contain only 'foobar'.
250
      const normalizedKeys = groups[groupName].filter(filterHiddenOptions).map((key) => {
251
        if (~aliasKeys.indexOf(key)) return key
252
        for (let i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) {
253
          if (~(options.alias[aliasKey] || []).indexOf(key)) return aliasKey
254
        }
255
        return key
256
      })
257

    
258
      if (normalizedKeys.length < 1) return
259

    
260
      ui.div(__(groupName))
261

    
262
      // actually generate the switches string --foo, -f, --bar.
263
      const switches = normalizedKeys.reduce((acc, key) => {
264
        acc[key] = [ key ].concat(options.alias[key] || [])
265
          .map(sw => {
266
            // for the special positional group don't
267
            // add '--' or '-' prefix.
268
            if (groupName === self.getPositionalGroupName()) return sw
269
            else return (sw.length > 1 ? '--' : '-') + sw
270
          })
271
          .join(', ')
272

    
273
        return acc
274
      }, {})
275

    
276
      normalizedKeys.forEach((key) => {
277
        const kswitch = switches[key]
278
        let desc = descriptions[key] || ''
279
        let type = null
280

    
281
        if (~desc.lastIndexOf(deferY18nLookupPrefix)) desc = __(desc.substring(deferY18nLookupPrefix.length))
282

    
283
        if (~options.boolean.indexOf(key)) type = `[${__('boolean')}]`
284
        if (~options.count.indexOf(key)) type = `[${__('count')}]`
285
        if (~options.string.indexOf(key)) type = `[${__('string')}]`
286
        if (~options.normalize.indexOf(key)) type = `[${__('string')}]`
287
        if (~options.array.indexOf(key)) type = `[${__('array')}]`
288
        if (~options.number.indexOf(key)) type = `[${__('number')}]`
289

    
290
        const extra = [
291
          type,
292
          (key in demandedOptions) ? `[${__('required')}]` : null,
293
          options.choices && options.choices[key] ? `[${__('choices:')} ${
294
            self.stringifiedValues(options.choices[key])}]` : null,
295
          defaultString(options.default[key], options.defaultDescription[key])
296
        ].filter(Boolean).join(' ')
297

    
298
        ui.span(
299
          {text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches, theWrap) + 4},
300
          desc
301
        )
302

    
303
        if (extra) ui.div({text: extra, padding: [0, 0, 0, 2], align: 'right'})
304
        else ui.div()
305
      })
306

    
307
      ui.div()
308
    })
309

    
310
    // describe some common use-cases for your application.
311
    if (examples.length) {
312
      ui.div(__('Examples:'))
313

    
314
      examples.forEach((example) => {
315
        example[0] = example[0].replace(/\$0/g, base$0)
316
      })
317

    
318
      examples.forEach((example) => {
319
        if (example[1] === '') {
320
          ui.div(
321
            {
322
              text: example[0],
323
              padding: [0, 2, 0, 2]
324
            }
325
          )
326
        } else {
327
          ui.div(
328
            {
329
              text: example[0],
330
              padding: [0, 2, 0, 2],
331
              width: maxWidth(examples, theWrap) + 4
332
            }, {
333
              text: example[1]
334
            }
335
          )
336
        }
337
      })
338

    
339
      ui.div()
340
    }
341

    
342
    // the usage string.
343
    if (epilog) {
344
      const e = epilog.replace(/\$0/g, base$0)
345
      ui.div(`${e}\n`)
346
    }
347

    
348
    // Remove the trailing white spaces
349
    return ui.toString().replace(/\s*$/, '')
350
  }
351

    
352
  // return the maximum width of a string
353
  // in the left-hand column of a table.
354
  function maxWidth (table, theWrap, modifier) {
355
    let width = 0
356

    
357
    // table might be of the form [leftColumn],
358
    // or {key: leftColumn}
359
    if (!Array.isArray(table)) {
360
      table = Object.keys(table).map(key => [table[key]])
361
    }
362

    
363
    table.forEach((v) => {
364
      width = Math.max(
365
        stringWidth(modifier ? `${modifier} ${v[0]}` : v[0]),
366
        width
367
      )
368
    })
369

    
370
    // if we've enabled 'wrap' we should limit
371
    // the max-width of the left-column.
372
    if (theWrap) width = Math.min(width, parseInt(theWrap * 0.5, 10))
373

    
374
    return width
375
  }
376

    
377
  // make sure any options set for aliases,
378
  // are copied to the keys being aliased.
379
  function normalizeAliases () {
380
    // handle old demanded API
381
    const demandedOptions = yargs.getDemandedOptions()
382
    const options = yargs.getOptions()
383

    
384
    ;(Object.keys(options.alias) || []).forEach((key) => {
385
      options.alias[key].forEach((alias) => {
386
        // copy descriptions.
387
        if (descriptions[alias]) self.describe(key, descriptions[alias])
388
        // copy demanded.
389
        if (alias in demandedOptions) yargs.demandOption(key, demandedOptions[alias])
390
        // type messages.
391
        if (~options.boolean.indexOf(alias)) yargs.boolean(key)
392
        if (~options.count.indexOf(alias)) yargs.count(key)
393
        if (~options.string.indexOf(alias)) yargs.string(key)
394
        if (~options.normalize.indexOf(alias)) yargs.normalize(key)
395
        if (~options.array.indexOf(alias)) yargs.array(key)
396
        if (~options.number.indexOf(alias)) yargs.number(key)
397
      })
398
    })
399
  }
400

    
401
  // given a set of keys, place any keys that are
402
  // ungrouped under the 'Options:' grouping.
403
  function addUngroupedKeys (keys, aliases, groups) {
404
    let groupedKeys = []
405
    let toCheck = null
406
    Object.keys(groups).forEach((group) => {
407
      groupedKeys = groupedKeys.concat(groups[group])
408
    })
409

    
410
    keys.forEach((key) => {
411
      toCheck = [key].concat(aliases[key])
412
      if (!toCheck.some(k => groupedKeys.indexOf(k) !== -1)) {
413
        groups[defaultGroup].push(key)
414
      }
415
    })
416
    return groupedKeys
417
  }
418

    
419
  function filterHiddenOptions (key) {
420
    return yargs.getOptions().hiddenOptions.indexOf(key) < 0 || yargs.parsed.argv[yargs.getOptions().showHiddenOpt]
421
  }
422

    
423
  self.showHelp = (level) => {
424
    const logger = yargs._getLoggerInstance()
425
    if (!level) level = 'error'
426
    const emit = typeof level === 'function' ? level : logger[level]
427
    emit(self.help())
428
  }
429

    
430
  self.functionDescription = (fn) => {
431
    const description = fn.name ? require('decamelize')(fn.name, '-') : __('generated-value')
432
    return ['(', description, ')'].join('')
433
  }
434

    
435
  self.stringifiedValues = function stringifiedValues (values, separator) {
436
    let string = ''
437
    const sep = separator || ', '
438
    const array = [].concat(values)
439

    
440
    if (!values || !array.length) return string
441

    
442
    array.forEach((value) => {
443
      if (string.length) string += sep
444
      string += JSON.stringify(value)
445
    })
446

    
447
    return string
448
  }
449

    
450
  // format the default-value-string displayed in
451
  // the right-hand column.
452
  function defaultString (value, defaultDescription) {
453
    let string = `[${__('default:')} `
454

    
455
    if (value === undefined && !defaultDescription) return null
456

    
457
    if (defaultDescription) {
458
      string += defaultDescription
459
    } else {
460
      switch (typeof value) {
461
        case 'string':
462
          string += `"${value}"`
463
          break
464
        case 'object':
465
          string += JSON.stringify(value)
466
          break
467
        default:
468
          string += value
469
      }
470
    }
471

    
472
    return `${string}]`
473
  }
474

    
475
  // guess the width of the console window, max-width 80.
476
  function windowWidth () {
477
    const maxWidth = 80
478
    if (typeof process === 'object' && process.stdout && process.stdout.columns) {
479
      return Math.min(maxWidth, process.stdout.columns)
480
    } else {
481
      return maxWidth
482
    }
483
  }
484

    
485
  // logic for displaying application version.
486
  let version = null
487
  self.version = (ver) => {
488
    version = ver
489
  }
490

    
491
  self.showVersion = () => {
492
    const logger = yargs._getLoggerInstance()
493
    logger.log(version)
494
  }
495

    
496
  self.reset = function reset (localLookup) {
497
    // do not reset wrap here
498
    // do not reset fails here
499
    failMessage = null
500
    failureOutput = false
501
    usages = []
502
    usageDisabled = false
503
    epilog = undefined
504
    examples = []
505
    commands = []
506
    descriptions = objFilter(descriptions, (k, v) => !localLookup[k])
507
    return self
508
  }
509

    
510
  let frozen
511
  self.freeze = function freeze () {
512
    frozen = {}
513
    frozen.failMessage = failMessage
514
    frozen.failureOutput = failureOutput
515
    frozen.usages = usages
516
    frozen.usageDisabled = usageDisabled
517
    frozen.epilog = epilog
518
    frozen.examples = examples
519
    frozen.commands = commands
520
    frozen.descriptions = descriptions
521
  }
522
  self.unfreeze = function unfreeze () {
523
    failMessage = frozen.failMessage
524
    failureOutput = frozen.failureOutput
525
    usages = frozen.usages
526
    usageDisabled = frozen.usageDisabled
527
    epilog = frozen.epilog
528
    examples = frozen.examples
529
    commands = frozen.commands
530
    descriptions = frozen.descriptions
531
    frozen = undefined
532
  }
533

    
534
  return self
535
}
(8-8/10)