coa 1.0.3 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|