coa 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmignore +6 -0
- package/.nyc_output/1f2a0db5a6d6559149db56d397f47cfc.json +1 -0
- package/.nyc_output/75b82d38f2186df930141082076e11c6.json +1 -0
- package/.travis.yml +9 -0
- package/GNUmakefile +34 -0
- package/README.md +1 -19
- package/coverage/base.css +212 -0
- package/coverage/coa/index.html +93 -0
- package/coverage/coa/index.js.html +68 -0
- package/coverage/coa/lib/arg.js.html +239 -0
- package/coverage/coa/lib/cmd.js.html +1556 -0
- package/coverage/coa/lib/coaobject.js.html +365 -0
- package/coverage/coa/lib/coaparam.js.html +440 -0
- package/coverage/coa/lib/color.js.html +131 -0
- package/coverage/coa/lib/completion.js.html +593 -0
- package/coverage/coa/lib/index.html +197 -0
- package/coverage/coa/lib/index.js.html +107 -0
- package/coverage/coa/lib/opt.js.html +524 -0
- package/coverage/coa/lib/shell.js.html +107 -0
- package/coverage/index.html +106 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +1 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +158 -0
- package/index.js +1 -1
- package/lib/arg.js +161 -44
- package/lib/cmd.js +547 -434
- package/lib/color.js +22 -19
- package/lib/completion.js +119 -161
- package/lib/index.js +10 -14
- package/lib/opt.js +313 -130
- package/lib/shell.js +13 -13
- package/package.json +14 -19
- package/qq.js +17 -0
- package/src/arg.coffee +130 -0
- package/src/cmd.coffee +456 -0
- package/src/color.coffee +25 -0
- package/src/completion.coffee +156 -0
- package/src/index.coffee +5 -0
- package/src/opt.coffee +243 -0
- package/src/shell.coffee +10 -0
- package/test/coa.js +496 -0
- package/test/mocha.opts +2 -0
- package/test/shell-test.js +60 -0
- package/tests/api-h.js +9 -0
- package/tests/h.js +6 -0
- package/LICENSE +0 -21
- package/lib/coaobject.js +0 -100
- package/lib/coaparam.js +0 -125
package/src/cmd.coffee
ADDED
@@ -0,0 +1,456 @@
|
|
1
|
+
UTIL = require 'util'
|
2
|
+
PATH = require 'path'
|
3
|
+
Color = require('./color').Color
|
4
|
+
Q = require('q')
|
5
|
+
|
6
|
+
#inspect = require('eyes').inspector { maxLength: 99999, stream: process.stderr }
|
7
|
+
|
8
|
+
###*
|
9
|
+
Command
|
10
|
+
|
11
|
+
Top level entity. Commands may have options and arguments.
|
12
|
+
@namespace
|
13
|
+
@class Presents command
|
14
|
+
###
|
15
|
+
exports.Cmd = class Cmd
|
16
|
+
|
17
|
+
###*
|
18
|
+
@constructs
|
19
|
+
@param {COA.Cmd} [cmd] parent command
|
20
|
+
###
|
21
|
+
constructor: (cmd) ->
|
22
|
+
if this not instanceof Cmd
|
23
|
+
return new Cmd cmd
|
24
|
+
|
25
|
+
@_parent cmd
|
26
|
+
|
27
|
+
@_cmds = []
|
28
|
+
@_cmdsByName = {}
|
29
|
+
|
30
|
+
@_opts = []
|
31
|
+
@_optsByKey = {}
|
32
|
+
|
33
|
+
@_args = []
|
34
|
+
|
35
|
+
@_ext = false
|
36
|
+
|
37
|
+
@get: (propertyName, func) ->
|
38
|
+
Object.defineProperty @::, propertyName,
|
39
|
+
configurable: true
|
40
|
+
enumerable: true
|
41
|
+
get: func
|
42
|
+
|
43
|
+
###*
|
44
|
+
Returns object containing all its subcommands as methods
|
45
|
+
to use from other programs.
|
46
|
+
@returns {Object}
|
47
|
+
###
|
48
|
+
@get 'api', () ->
|
49
|
+
if not @_api
|
50
|
+
@_api = => @invoke.apply @, arguments
|
51
|
+
for c of @_cmdsByName
|
52
|
+
do (c) =>
|
53
|
+
@_api[c] = @_cmdsByName[c].api
|
54
|
+
@_api
|
55
|
+
|
56
|
+
_parent: (cmd) ->
|
57
|
+
@_cmd = cmd or this
|
58
|
+
if cmd
|
59
|
+
cmd._cmds.push @
|
60
|
+
if @_name then @_cmd._cmdsByName[@_name] = @
|
61
|
+
@
|
62
|
+
|
63
|
+
###*
|
64
|
+
Set a canonical command identifier to be used anywhere in the API.
|
65
|
+
@param {String} _name command name
|
66
|
+
@returns {COA.Cmd} this instance (for chainability)
|
67
|
+
###
|
68
|
+
name: (@_name) ->
|
69
|
+
if @_cmd isnt @ then @_cmd._cmdsByName[_name] = @
|
70
|
+
@
|
71
|
+
|
72
|
+
###*
|
73
|
+
Set a long description for command to be used anywhere in text messages.
|
74
|
+
@param {String} _title command title
|
75
|
+
@returns {COA.Cmd} this instance (for chainability)
|
76
|
+
###
|
77
|
+
title: (@_title) -> @
|
78
|
+
|
79
|
+
###*
|
80
|
+
Create new or add existing subcommand for current command.
|
81
|
+
@param {COA.Cmd} [cmd] existing command instance
|
82
|
+
@returns {COA.Cmd} new subcommand instance
|
83
|
+
###
|
84
|
+
cmd: (cmd) ->
|
85
|
+
if cmd then cmd._parent @
|
86
|
+
else new Cmd @
|
87
|
+
|
88
|
+
###*
|
89
|
+
Create option for current command.
|
90
|
+
@returns {COA.Opt} new option instance
|
91
|
+
###
|
92
|
+
opt: -> new (require('./opt').Opt) @
|
93
|
+
|
94
|
+
###*
|
95
|
+
Create argument for current command.
|
96
|
+
@returns {COA.Opt} new argument instance
|
97
|
+
###
|
98
|
+
arg: -> new (require('./arg').Arg) @
|
99
|
+
|
100
|
+
###*
|
101
|
+
Add (or set) action for current command.
|
102
|
+
@param {Function} act action function,
|
103
|
+
invoked in the context of command instance
|
104
|
+
and has the parameters:
|
105
|
+
- {Object} opts parsed options
|
106
|
+
- {Array} args parsed arguments
|
107
|
+
- {Object} res actions result accumulator
|
108
|
+
It can return rejected promise by Cmd.reject (in case of error)
|
109
|
+
or any other value treated as result.
|
110
|
+
@param {Boolean} [force=false] flag for set action instead add to existings
|
111
|
+
@returns {COA.Cmd} this instance (for chainability)
|
112
|
+
###
|
113
|
+
act: (act, force) ->
|
114
|
+
return @ unless act
|
115
|
+
|
116
|
+
if not force and @_act
|
117
|
+
@_act.push act
|
118
|
+
else
|
119
|
+
@_act = [act]
|
120
|
+
|
121
|
+
@
|
122
|
+
|
123
|
+
###*
|
124
|
+
Set custom additional completion for current command.
|
125
|
+
@param {Function} completion generation function,
|
126
|
+
invoked in the context of command instance.
|
127
|
+
Accepts parameters:
|
128
|
+
- {Object} opts completion options
|
129
|
+
It can return promise or any other value treated as result.
|
130
|
+
@returns {COA.Cmd} this instance (for chainability)
|
131
|
+
###
|
132
|
+
comp: (@_comp) -> @
|
133
|
+
|
134
|
+
###*
|
135
|
+
Apply function with arguments in context of command instance.
|
136
|
+
@param {Function} fn
|
137
|
+
@param {Array} args
|
138
|
+
@returns {COA.Cmd} this instance (for chainability)
|
139
|
+
###
|
140
|
+
apply: (fn, args...) ->
|
141
|
+
fn.apply this, args
|
142
|
+
@
|
143
|
+
|
144
|
+
###*
|
145
|
+
Make command "helpful", i.e. add -h --help flags for print usage.
|
146
|
+
@returns {COA.Cmd} this instance (for chainability)
|
147
|
+
###
|
148
|
+
helpful: ->
|
149
|
+
@opt()
|
150
|
+
.name('help').title('Help')
|
151
|
+
.short('h').long('help')
|
152
|
+
.flag()
|
153
|
+
.only()
|
154
|
+
.act ->
|
155
|
+
return @usage()
|
156
|
+
.end()
|
157
|
+
|
158
|
+
###*
|
159
|
+
Adds shell completion to command, adds "completion" subcommand,
|
160
|
+
that makes all the magic.
|
161
|
+
Must be called only on root command.
|
162
|
+
@returns {COA.Cmd} this instance (for chainability)
|
163
|
+
###
|
164
|
+
completable: ->
|
165
|
+
@cmd()
|
166
|
+
.name('completion')
|
167
|
+
.apply(require './completion')
|
168
|
+
.end()
|
169
|
+
|
170
|
+
###*
|
171
|
+
Allow command to be extendable by external node.js modules.
|
172
|
+
@param {String} [pattern] Pattern of node.js module to find subcommands at.
|
173
|
+
@returns {COA.Cmd} this instance (for chainability)
|
174
|
+
###
|
175
|
+
extendable: (pattern) ->
|
176
|
+
@_ext = pattern or true
|
177
|
+
@
|
178
|
+
|
179
|
+
_exit: (msg, code) ->
|
180
|
+
process.once 'exit', ->
|
181
|
+
if msg then console.error msg
|
182
|
+
process.exit code or 0
|
183
|
+
|
184
|
+
###*
|
185
|
+
Build full usage text for current command instance.
|
186
|
+
@returns {String} usage text
|
187
|
+
###
|
188
|
+
usage: ->
|
189
|
+
res = []
|
190
|
+
|
191
|
+
if @_title then res.push @_fullTitle()
|
192
|
+
|
193
|
+
res.push('', 'Usage:')
|
194
|
+
|
195
|
+
if @_cmds.length then res.push(['', '',
|
196
|
+
Color('lred', @_fullName()),
|
197
|
+
Color('lblue', 'COMMAND'),
|
198
|
+
Color('lgreen', '[OPTIONS]'),
|
199
|
+
Color('lpurple', '[ARGS]')].join ' ')
|
200
|
+
|
201
|
+
if @_opts.length + @_args.length then res.push(['', '',
|
202
|
+
Color('lred', @_fullName()),
|
203
|
+
Color('lgreen', '[OPTIONS]'),
|
204
|
+
Color('lpurple', '[ARGS]')].join ' ')
|
205
|
+
|
206
|
+
res.push(
|
207
|
+
@_usages(@_cmds, 'Commands'),
|
208
|
+
@_usages(@_opts, 'Options'),
|
209
|
+
@_usages(@_args, 'Arguments'))
|
210
|
+
|
211
|
+
res.join '\n'
|
212
|
+
|
213
|
+
_usage: ->
|
214
|
+
Color('lblue', @_name) + ' : ' + @_title
|
215
|
+
|
216
|
+
_usages: (os, title) ->
|
217
|
+
unless os.length then return
|
218
|
+
res = ['', title + ':']
|
219
|
+
for o in os
|
220
|
+
res.push ' ' + o._usage()
|
221
|
+
res.join '\n'
|
222
|
+
|
223
|
+
_fullTitle: ->
|
224
|
+
(if @_cmd is this then '' else @_cmd._fullTitle() + '\n') + @_title
|
225
|
+
|
226
|
+
_fullName: ->
|
227
|
+
(if this._cmd is this then '' else @_cmd._fullName() + ' ') + PATH.basename(@_name)
|
228
|
+
|
229
|
+
_ejectOpt: (opts, opt) ->
|
230
|
+
if (pos = opts.indexOf(opt)) >= 0
|
231
|
+
if opts[pos]._arr
|
232
|
+
opts[pos]
|
233
|
+
else
|
234
|
+
opts.splice(pos, 1)[0]
|
235
|
+
|
236
|
+
_checkRequired: (opts, args) ->
|
237
|
+
if not (@_opts.filter (o) -> o._only and o._name of opts).length
|
238
|
+
all = @_opts.concat @_args
|
239
|
+
while i = all.shift()
|
240
|
+
if i._req and i._checkParsed opts, args
|
241
|
+
return @reject i._requiredText()
|
242
|
+
|
243
|
+
_parseCmd: (argv, unparsed = []) ->
|
244
|
+
argv = argv.concat()
|
245
|
+
optSeen = false
|
246
|
+
while i = argv.shift()
|
247
|
+
if not i.indexOf '-'
|
248
|
+
optSeen = true
|
249
|
+
if not optSeen and /^\w[\w-_]*$/.test(i)
|
250
|
+
cmd = @_cmdsByName[i]
|
251
|
+
|
252
|
+
if not cmd and @_ext
|
253
|
+
# construct package name to require
|
254
|
+
if typeof @_ext is 'string'
|
255
|
+
if ~@_ext.indexOf('%s')
|
256
|
+
# use formatted string
|
257
|
+
pkg = UTIL.format(@_ext, i)
|
258
|
+
else
|
259
|
+
# just append subcommand name to the prefix
|
260
|
+
pkg = @_ext + i
|
261
|
+
else if @_ext is true
|
262
|
+
# use default scheme: <command>-<subcommand>-<subcommand> and so on
|
263
|
+
pkg = i
|
264
|
+
c = @
|
265
|
+
loop
|
266
|
+
pkg = c._name + '-' + pkg
|
267
|
+
if c._cmd is c then break
|
268
|
+
c = c._cmd
|
269
|
+
|
270
|
+
try
|
271
|
+
cmdDesc = require(pkg)
|
272
|
+
catch e
|
273
|
+
|
274
|
+
if cmdDesc
|
275
|
+
if typeof cmdDesc == 'function'
|
276
|
+
# set create subcommand, set its name and apply imported function
|
277
|
+
@cmd()
|
278
|
+
.name(i)
|
279
|
+
.apply(cmdDesc)
|
280
|
+
.end()
|
281
|
+
else if typeof cmdDesc == 'object'
|
282
|
+
# register subcommand
|
283
|
+
@cmd(cmdDesc)
|
284
|
+
# set command name
|
285
|
+
cmdDesc.name(i)
|
286
|
+
else
|
287
|
+
throw new Error 'Error: Unsupported command declaration type, ' +
|
288
|
+
'should be function or COA.Cmd() object'
|
289
|
+
cmd = @_cmdsByName[i]
|
290
|
+
if cmd
|
291
|
+
return cmd._parseCmd argv, unparsed
|
292
|
+
|
293
|
+
unparsed.push i
|
294
|
+
|
295
|
+
{ cmd: @, argv: unparsed }
|
296
|
+
|
297
|
+
_parseOptsAndArgs: (argv) ->
|
298
|
+
opts = {}
|
299
|
+
args = {}
|
300
|
+
|
301
|
+
nonParsedOpts = @_opts.concat()
|
302
|
+
nonParsedArgs = @_args.concat()
|
303
|
+
|
304
|
+
while i = argv.shift()
|
305
|
+
# opt
|
306
|
+
if i isnt '--' and not i.indexOf '-'
|
307
|
+
|
308
|
+
if m = i.match /^(--\w[\w-_]*)=(.*)$/
|
309
|
+
i = m[1]
|
310
|
+
|
311
|
+
# suppress 'unknown argument' error for flag options with values
|
312
|
+
if not @_optsByKey[i]._flag
|
313
|
+
argv.unshift m[2]
|
314
|
+
|
315
|
+
if opt = @_ejectOpt nonParsedOpts, @_optsByKey[i]
|
316
|
+
if Q.isRejected(res = opt._parse argv, opts)
|
317
|
+
return res
|
318
|
+
else
|
319
|
+
return @reject "Unknown option: #{ i }"
|
320
|
+
|
321
|
+
# arg
|
322
|
+
else
|
323
|
+
if i is '--'
|
324
|
+
i = argv.splice(0)
|
325
|
+
|
326
|
+
i = if Array.isArray(i) then i else [i]
|
327
|
+
|
328
|
+
while a = i.shift()
|
329
|
+
if arg = nonParsedArgs.shift()
|
330
|
+
if arg._arr then nonParsedArgs.unshift arg
|
331
|
+
if Q.isRejected(res = arg._parse a, args)
|
332
|
+
return res
|
333
|
+
else
|
334
|
+
return @reject "Unknown argument: #{ a }"
|
335
|
+
|
336
|
+
# set defaults
|
337
|
+
{
|
338
|
+
opts: @_setDefaults(opts, nonParsedOpts),
|
339
|
+
args: @_setDefaults(args, nonParsedArgs)
|
340
|
+
}
|
341
|
+
|
342
|
+
_setDefaults: (params, desc) ->
|
343
|
+
for i in desc
|
344
|
+
if i._name not of params and '_def' of i
|
345
|
+
i._saveVal params, i._def
|
346
|
+
params
|
347
|
+
|
348
|
+
_processParams: (params, desc) ->
|
349
|
+
notExists = []
|
350
|
+
for i in desc
|
351
|
+
n = i._name
|
352
|
+
if n not of params
|
353
|
+
notExists.push i
|
354
|
+
continue
|
355
|
+
|
356
|
+
vals = params[n]
|
357
|
+
delete params[n]
|
358
|
+
if not Array.isArray vals
|
359
|
+
vals = [vals]
|
360
|
+
|
361
|
+
for v in vals
|
362
|
+
if Q.isRejected(res = i._saveVal(params, v))
|
363
|
+
return res
|
364
|
+
|
365
|
+
# set defaults
|
366
|
+
@_setDefaults params, notExists
|
367
|
+
|
368
|
+
_parseArr: (argv) ->
|
369
|
+
Q.when @_parseCmd(argv), (p) ->
|
370
|
+
Q.when p.cmd._parseOptsAndArgs(p.argv), (r) ->
|
371
|
+
{ cmd: p.cmd, opts: r.opts, args: r.args }
|
372
|
+
|
373
|
+
_do: (input) ->
|
374
|
+
Q.when input, (input) =>
|
375
|
+
cmd = input.cmd
|
376
|
+
[@_checkRequired].concat(cmd._act or []).reduce(
|
377
|
+
(res, act) ->
|
378
|
+
Q.when res, (res) ->
|
379
|
+
act.call(
|
380
|
+
cmd
|
381
|
+
input.opts
|
382
|
+
input.args
|
383
|
+
res)
|
384
|
+
undefined
|
385
|
+
)
|
386
|
+
|
387
|
+
###*
|
388
|
+
Parse arguments from simple format like NodeJS process.argv
|
389
|
+
and run ahead current program, i.e. call process.exit when all actions done.
|
390
|
+
@param {Array} argv
|
391
|
+
@returns {COA.Cmd} this instance (for chainability)
|
392
|
+
###
|
393
|
+
run: (argv = process.argv.slice(2)) ->
|
394
|
+
cb = (code) => (res) =>
|
395
|
+
if res
|
396
|
+
@_exit res.stack ? res.toString(), res.exitCode ? code
|
397
|
+
else
|
398
|
+
@_exit()
|
399
|
+
Q.when(@do(argv), cb(0), cb(1)).done()
|
400
|
+
@
|
401
|
+
|
402
|
+
###*
|
403
|
+
Convenient function to run command from tests.
|
404
|
+
@param {Array} argv
|
405
|
+
@returns {Q.Promise}
|
406
|
+
###
|
407
|
+
do: (argv) ->
|
408
|
+
@_do(@_parseArr argv || [])
|
409
|
+
|
410
|
+
###*
|
411
|
+
Invoke specified (or current) command using provided
|
412
|
+
options and arguments.
|
413
|
+
@param {String|Array} cmds subcommand to invoke (optional)
|
414
|
+
@param {Object} opts command options (optional)
|
415
|
+
@param {Object} args command arguments (optional)
|
416
|
+
@returns {Q.Promise}
|
417
|
+
###
|
418
|
+
invoke: (cmds = [], opts = {}, args = {}) ->
|
419
|
+
if typeof cmds == 'string'
|
420
|
+
cmds = cmds.split(' ')
|
421
|
+
|
422
|
+
if arguments.length < 3
|
423
|
+
if not Array.isArray cmds
|
424
|
+
args = opts
|
425
|
+
opts = cmds
|
426
|
+
cmds = []
|
427
|
+
|
428
|
+
Q.when @_parseCmd(cmds), (p) =>
|
429
|
+
if p.argv.length
|
430
|
+
return @reject "Unknown command: " + cmds.join ' '
|
431
|
+
|
432
|
+
Q.all([@_processParams(opts, @_opts), @_processParams(args, @_args)])
|
433
|
+
.spread (opts, args) =>
|
434
|
+
@_do({ cmd: p.cmd, opts: opts, args: args })
|
435
|
+
# catch fails from .only() options
|
436
|
+
.fail (res) =>
|
437
|
+
if res and res.exitCode is 0
|
438
|
+
res.toString()
|
439
|
+
else
|
440
|
+
@reject(res)
|
441
|
+
|
442
|
+
###*
|
443
|
+
Return reject of actions results promise with error code.
|
444
|
+
Use in .act() for return with error.
|
445
|
+
@param {Object} reject reason
|
446
|
+
You can customize toString() method and exitCode property
|
447
|
+
of reason object.
|
448
|
+
@returns {Q.promise} rejected promise
|
449
|
+
###
|
450
|
+
reject: (reason) -> Q.reject(reason)
|
451
|
+
|
452
|
+
###*
|
453
|
+
Finish chain for current subcommand and return parent command instance.
|
454
|
+
@returns {COA.Cmd} parent command
|
455
|
+
###
|
456
|
+
end: -> @_cmd
|
package/src/color.coffee
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
colors =
|
2
|
+
black: '30'
|
3
|
+
dgray: '1;30'
|
4
|
+
red: '31'
|
5
|
+
lred: '1;31'
|
6
|
+
green: '32'
|
7
|
+
lgreen: '1;32'
|
8
|
+
brown: '33'
|
9
|
+
yellow: '1;33'
|
10
|
+
blue: '34'
|
11
|
+
lblue: '1;34'
|
12
|
+
purple: '35'
|
13
|
+
lpurple: '1;35'
|
14
|
+
cyan: '36'
|
15
|
+
lcyan: '1;36'
|
16
|
+
lgray: '37'
|
17
|
+
white: '1;37'
|
18
|
+
|
19
|
+
exports.Color = (c, str) ->
|
20
|
+
# Use \x1B instead of \033 because of CoffeeScript 1.3.x compilation error
|
21
|
+
[
|
22
|
+
'\x1B[', colors[c], 'm'
|
23
|
+
str
|
24
|
+
'\x1B[m'
|
25
|
+
].join ''
|
@@ -0,0 +1,156 @@
|
|
1
|
+
###*
|
2
|
+
Most of the code adopted from the npm package shell completion code.
|
3
|
+
See https://github.com/isaacs/npm/blob/master/lib/completion.js
|
4
|
+
###
|
5
|
+
|
6
|
+
Q = require 'q'
|
7
|
+
escape = require('./shell').escape
|
8
|
+
unescape = require('./shell').unescape
|
9
|
+
|
10
|
+
module.exports = ->
|
11
|
+
@title('Shell completion')
|
12
|
+
.helpful()
|
13
|
+
.arg()
|
14
|
+
.name('raw')
|
15
|
+
.title('Completion words')
|
16
|
+
.arr()
|
17
|
+
.end()
|
18
|
+
.act (opts, args) ->
|
19
|
+
if process.platform == 'win32'
|
20
|
+
e = new Error 'shell completion not supported on windows'
|
21
|
+
e.code = 'ENOTSUP'
|
22
|
+
e.errno = require('constants').ENOTSUP
|
23
|
+
return @reject(e)
|
24
|
+
|
25
|
+
# if the COMP_* isn't in the env, then just dump the script
|
26
|
+
if !process.env.COMP_CWORD? or !process.env.COMP_LINE? or !process.env.COMP_POINT?
|
27
|
+
return dumpScript(@_cmd._name)
|
28
|
+
|
29
|
+
console.error 'COMP_LINE: %s', process.env.COMP_LINE
|
30
|
+
console.error 'COMP_CWORD: %s', process.env.COMP_CWORD
|
31
|
+
console.error 'COMP_POINT: %s', process.env.COMP_POINT
|
32
|
+
console.error 'args: %j', args.raw
|
33
|
+
|
34
|
+
# completion opts
|
35
|
+
opts = getOpts args.raw
|
36
|
+
|
37
|
+
# cmd
|
38
|
+
{ cmd, argv } = @_cmd._parseCmd opts.partialWords
|
39
|
+
Q.when complete(cmd, opts), (compls) ->
|
40
|
+
console.error 'filtered: %j', compls
|
41
|
+
console.log compls.map(escape).join('\n')
|
42
|
+
|
43
|
+
|
44
|
+
dumpScript = (name) ->
|
45
|
+
fs = require 'fs'
|
46
|
+
path = require 'path'
|
47
|
+
defer = Q.defer()
|
48
|
+
|
49
|
+
fs.readFile path.resolve(__dirname, 'completion.sh'), 'utf8', (err, d) ->
|
50
|
+
if err then return defer.reject err
|
51
|
+
d = d.replace(/{{cmd}}/g, path.basename name).replace(/^\#\!.*?\n/, '')
|
52
|
+
|
53
|
+
onError = (err) ->
|
54
|
+
# Darwin is a real dick sometimes.
|
55
|
+
#
|
56
|
+
# This is necessary because the "source" or "." program in
|
57
|
+
# bash on OS X closes its file argument before reading
|
58
|
+
# from it, meaning that you get exactly 1 write, which will
|
59
|
+
# work most of the time, and will always raise an EPIPE.
|
60
|
+
#
|
61
|
+
# Really, one should not be tossing away EPIPE errors, or any
|
62
|
+
# errors, so casually. But, without this, `. <(cmd completion)`
|
63
|
+
# can never ever work on OS X.
|
64
|
+
if err.errno == require('constants').EPIPE
|
65
|
+
process.stdout.removeListener 'error', onError
|
66
|
+
defer.resolve()
|
67
|
+
else
|
68
|
+
defer.reject(err)
|
69
|
+
|
70
|
+
process.stdout.on 'error', onError
|
71
|
+
process.stdout.write d, -> defer.resolve()
|
72
|
+
|
73
|
+
defer.promise
|
74
|
+
|
75
|
+
|
76
|
+
getOpts = (argv) ->
|
77
|
+
# get the partial line and partial word, if the point isn't at the end
|
78
|
+
# ie, tabbing at: cmd foo b|ar
|
79
|
+
line = process.env.COMP_LINE
|
80
|
+
w = +process.env.COMP_CWORD
|
81
|
+
point = +process.env.COMP_POINT
|
82
|
+
words = argv.map unescape
|
83
|
+
word = words[w]
|
84
|
+
partialLine = line.substr 0, point
|
85
|
+
partialWords = words.slice 0, w
|
86
|
+
|
87
|
+
# figure out where in that last word the point is
|
88
|
+
partialWord = argv[w] or ''
|
89
|
+
i = partialWord.length
|
90
|
+
while partialWord.substr(0, i) isnt partialLine.substr(-1 * i) and i > 0
|
91
|
+
i--
|
92
|
+
partialWord = unescape partialWord.substr 0, i
|
93
|
+
if partialWord then partialWords.push partialWord
|
94
|
+
|
95
|
+
{
|
96
|
+
line: line
|
97
|
+
w: w
|
98
|
+
point: point
|
99
|
+
words: words
|
100
|
+
word: word
|
101
|
+
partialLine: partialLine
|
102
|
+
partialWords: partialWords
|
103
|
+
partialWord: partialWord
|
104
|
+
}
|
105
|
+
|
106
|
+
|
107
|
+
complete = (cmd, opts) ->
|
108
|
+
compls = []
|
109
|
+
|
110
|
+
# complete on cmds
|
111
|
+
if opts.partialWord.indexOf('-')
|
112
|
+
compls = Object.keys(cmd._cmdsByName)
|
113
|
+
# Complete on required opts without '-' in last partial word
|
114
|
+
# (if required not already specified)
|
115
|
+
#
|
116
|
+
# Commented out because of uselessness:
|
117
|
+
# -b, --block suggest results in '-' on cmd line;
|
118
|
+
# next completion suggest all options, because of '-'
|
119
|
+
#.concat Object.keys(cmd._optsByKey).filter (v) -> cmd._optsByKey[v]._req
|
120
|
+
else
|
121
|
+
# complete on opt values: --opt=| case
|
122
|
+
if m = opts.partialWord.match /^(--\w[\w-_]*)=(.*)$/
|
123
|
+
optWord = m[1]
|
124
|
+
optPrefix = optWord + '='
|
125
|
+
else
|
126
|
+
# complete on opts
|
127
|
+
# don't complete on opts in case of --opt=val completion
|
128
|
+
# TODO: don't complete on opts in case of unknown arg after commands
|
129
|
+
# TODO: complete only on opts with arr() or not already used
|
130
|
+
# TODO: complete only on full opts?
|
131
|
+
compls = Object.keys cmd._optsByKey
|
132
|
+
|
133
|
+
# complete on opt values: next arg case
|
134
|
+
if not (o = opts.partialWords[opts.w - 1]).indexOf '-'
|
135
|
+
optWord = o
|
136
|
+
|
137
|
+
# complete on opt values: completion
|
138
|
+
if optWord and opt = cmd._optsByKey[optWord]
|
139
|
+
if not opt._flag and opt._comp
|
140
|
+
compls = Q.join compls, Q.when opt._comp(opts), (c, o) ->
|
141
|
+
c.concat o.map (v) -> (optPrefix or '') + v
|
142
|
+
|
143
|
+
# TODO: complete on args values (context aware, custom completion?)
|
144
|
+
|
145
|
+
# custom completion on cmds
|
146
|
+
if cmd._comp
|
147
|
+
compls = Q.join compls, Q.when(cmd._comp(opts)), (c, o) ->
|
148
|
+
c.concat o
|
149
|
+
|
150
|
+
# TODO: context aware custom completion on cmds, opts and args
|
151
|
+
# (can depend on already entered values, especially options)
|
152
|
+
|
153
|
+
Q.when compls, (compls) ->
|
154
|
+
console.error 'partialWord: %s', opts.partialWord
|
155
|
+
console.error 'compls: %j', compls
|
156
|
+
compls.filter (c) -> c.indexOf(opts.partialWord) is 0
|