-
Notifications
You must be signed in to change notification settings - Fork 30
/
index.js
485 lines (384 loc) · 11 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
// Native
import path from 'path'
import {spawn} from 'child_process'
// Packages
import parser from 'minimist'
import pkginfo from 'pkginfo'
import loudRejection from 'loud-rejection'
import camelcase from 'camelcase'
import chalk from 'chalk'
class Args {
constructor() {
// Will later hold registered options and commands
this.details = {
options: [],
commands: []
}
// Configuration defaults
this.config = {
help: true,
version: true,
usageFilter: null,
value: null,
name: null
}
// Make unhandled promise rejections fail loudly instead of the default silent fail
loudRejection()
}
options(list) {
if (list.constructor !== Array) {
throw new Error('Item passed to .options is not an array')
}
for (const item of list) {
const preset = item.defaultValue || false
const init = item.init || false
this.option(item.name, item.description, preset, init)
}
return this
}
option(name, description, defaultValue, init) {
let usage = []
// If name is an array, pick the values
// Otherwise just use the whole thing
switch (name.constructor) {
case String:
usage[0] = name.charAt(0)
usage[1] = name
break
case Array:
usage = usage.concat(name)
break
default:
throw new Error('Invalid name for option')
}
// Throw error if short option is too long
if (usage.length > 0 && usage[0].length > 1) {
throw new Error('Short version of option is longer than 1 char')
}
const optionDetails = {
defaultValue,
usage,
description
}
let defaultIsWrong
switch (defaultValue) {
case false:
defaultIsWrong = true
break
case null:
defaultIsWrong = true
break
case undefined:
defaultIsWrong = true
break
default:
defaultIsWrong = false
}
// Set initializer depending on type of default value
if (!defaultIsWrong) {
const initFunction = typeof init === 'function'
optionDetails.init = initFunction ? init : this.handleType(defaultValue)[1]
}
// Register option to global scope
this.details.options.push(optionDetails)
// Allow chaining of .option()
return this
}
command(usage, description, init, aliases) {
if (Array.isArray(init)) {
aliases = init
init = undefined
}
if (aliases && Array.isArray(aliases)) {
usage = [].concat([usage], aliases)
}
// Register command to global scope
this.details.commands.push({
usage,
description,
init: typeof init === 'function' ? init : false
})
// Allow chaining of .command()
return this
}
handleType(value) {
let type = value
if (typeof value !== 'function') {
type = value.constructor
}
// Depending on the type of the default value,
// select a default initializer function
switch (type) {
case String:
return ['[value]']
case Array:
return ['<list>']
case Number:
case parseInt:
return ['<n>', parseInt]
default:
return false
}
}
readOption(option) {
let value = false
const contents = {}
// If option has been used, get its value
for (const name of option.usage) {
const fromArgs = this.raw[name]
if (fromArgs) {
value = fromArgs
}
}
// Process the option's value
for (let name of option.usage) {
let propVal = value || option.defaultValue
let condition = true
if (option.init) {
// Only use the toString initializer if value is a number
if (option.init === toString) {
condition = propVal.constructor === Number
}
if (condition) {
// Pass it through the initializer
propVal = option.init(propVal)
}
}
// Camelcase option name
name = camelcase(name)
// Add option to list if it has a value
if (propVal) {
contents[name] = propVal
}
}
return contents
}
getOptions() {
const options = {}
const args = {}
// Copy over the arguments
Object.assign(args, this.raw)
delete args._
// Set option defaults
for (const option of this.details.options) {
if (!option.defaultValue) {
continue
}
Object.assign(options, this.readOption(option))
}
// Override defaults if used in command line
for (const option in args) {
if (!{}.hasOwnProperty.call(args, option)) {
continue
}
const related = this.isDefined(option, 'options')
if (related) {
const details = this.readOption(related)
Object.assign(options, details)
}
}
return options
}
generateDetails(kind) {
// Get all properties of kind from global scope
const items = this.details[kind]
const parts = []
const isCmd = kind === 'commands'
// Sort items alphabetically
items.sort((a, b) => {
const first = isCmd ? a.usage : a.usage[1]
const second = isCmd ? b.usage : b.usage[1]
switch (true) {
case (first < second): return -1
case (first > second): return 1
default: return 0
}
})
for (const item in items) {
if (!{}.hasOwnProperty.call(items, item)) {
continue
}
let usage = items[item].usage
let initial = items[item].defaultValue
// If usage is an array, show its contents
if (usage.constructor === Array) {
if (isCmd) {
usage = usage.join(', ')
} else {
const isVersion = usage.indexOf('v')
usage = `-${usage[0]}, --${usage[1]}`
if (!initial) {
initial = items[item].init
}
usage += (initial && isVersion === -1) ? ' ' + this.handleType(initial)[0] : ''
}
}
// Overwrite usage with readable syntax
items[item].usage = usage
}
// Find length of longest option or command
// Before doing that, make a copy of the original array
const longest = items.slice().sort((a, b) => {
return b.usage.length - a.usage.length
})[0].usage.length
for (const item of items) {
let usage = item.usage
const difference = longest - usage.length
// Compensate the difference to longest property with spaces
usage += ' '.repeat(difference)
// Add some space around it as well
parts.push(' ' + chalk.yellow(usage) + ' ' + chalk.dim(item.description))
}
return parts
}
runCommand(details, options) {
// If help is disabled, remove initializer
if (details.usage === 'help' && !this.config.help) {
details.init = false
}
// If command has initializer, call it
if (details.init) {
const sub = [].concat(this.sub)
sub.shift()
return details.init.bind(this)(details.usage, sub, options)
}
// Generate full name of binary
const full = this.binary + '-' + (Array.isArray(details.usage) ? details.usage[0] : details.usage)
const args = process.argv
let i = 0
while (i < 3) {
args.shift()
i++
}
// Run binary of sub command
this.child = spawn(full, args, {
stdio: 'inherit'
})
// Throw an error if something fails within that binary
this.child.on('error', err => {
throw err
})
}
checkVersion() {
const parent = module.parent
// Load parent module
pkginfo(parent)
// And get its version propery
const version = parent.exports.version
if (version) {
// If it exists, register it as a default option
this.option('version', 'Output the version number', version)
// And immediately output it if used in command line
if (this.raw.v || this.raw.version) {
console.log(version)
process.exit()
}
}
}
isDefined(name, list) {
// Get all items of kind
const children = this.details[list]
// Check if a child matches the requested name
for (const child of children) {
const usage = child.usage
const type = usage.constructor
if (type === Array && usage.indexOf(name) > -1) {
return child
}
if (type === String && usage === name) {
return child
}
}
// If nothing matches, item is not defined
return false
}
parse(argv, options) {
// Override default option values
Object.assign(this.config, options)
if (this.config.help) {
// Register default options and commands
this.option('help', 'Output usage information')
this.command('help', 'Display help', this.showHelp)
}
// Parse arguments using minimist
this.raw = parser(argv.slice(1))
this.binary = path.basename(this.raw._[0])
// If default version is allowed, check for it
if (this.config.version) {
this.checkVersion()
}
const subCommand = this.raw._[1]
const helpTriggered = this.raw.h || this.raw.help
const args = {}
const defined = this.isDefined(subCommand, 'commands')
const optionList = this.getOptions()
Object.assign(args, this.raw)
args._.shift()
// Export sub arguments of command
this.sub = args._
// If sub command is defined, run it
if (defined) {
this.runCommand(defined, optionList)
return {}
}
// Show usage information if "help" or "h" option was used
// And respect the option related to it
if (this.config.help && helpTriggered) {
this.showHelp()
}
// Hand back list of options
return optionList
}
showHelp() {
const name = this.config.name || this.binary.replace('-', ' ')
const firstBig = word => word.charAt(0).toUpperCase() + word.substr(1)
const parts = []
const groups = {
commands: true,
options: true
}
for (const group in groups) {
if (this.details[group].length > 0) {
continue
}
groups[group] = false
}
const optionHandle = groups.options ? ' [options]' : ''
const cmdHandle = groups.commands ? ' [command]' : ''
const value = typeof this.config.value === 'string' ? ' ' + this.config.value : ''
parts.push([
'',
'Usage: ' + chalk.yellow(name) + chalk.dim(optionHandle + cmdHandle + value),
''
])
for (const group in groups) {
if (!groups[group]) {
continue
}
parts.push([
'',
firstBig(group) + ':',
'',
''
])
parts.push(this.generateDetails(group))
parts.push(['', ''])
}
let output = ''
// And finally, merge and output them
for (const part of parts) {
output += part.join('\n ')
}
if (!groups.commands && !groups.options) {
output = 'No sub commands or options available'
}
const usageFilter = this.config.usageFilter
// If filter is available, pass usage information through
if (typeof usageFilter === 'function') {
output = usageFilter(output) || output
}
console.log(output)
process.exit()
}
}
export default new Args()