Skip to content

Commit

Permalink
Merge pull request #11 from arikon/comp
Browse files Browse the repository at this point in the history
Shell completion
  • Loading branch information
veged committed Oct 28, 2011
2 parents cfc200a + 7a62568 commit dfb69fd
Show file tree
Hide file tree
Showing 17 changed files with 743 additions and 135 deletions.
6 changes: 5 additions & 1 deletion GNUmakefile
@@ -1,7 +1,11 @@

.PHONY: all
all: lib

lib: $(foreach s,$(wildcard src/*.coffee),$(patsubst src/%.coffee,lib/%.js,$s))

lib/%.js: src/%.coffee
coffee -cb -o $(@D) $<

.PHONY: test
test: lib
./node_modules/.bin/vows --spec
38 changes: 36 additions & 2 deletions README.md
Expand Up @@ -4,6 +4,10 @@ COA is a yet another parser for command line options.
You can choose one of the [existing modules](https://github.com/joyent/node/wiki/modules#wiki-parsers-commandline),
or write your own like me.

## Features

* Shell completion (experimantal)

## Examples

````javascript
Expand Down Expand Up @@ -97,10 +101,24 @@ Apply function with arguments in context of command instance.<br>
**@param** *Array* `args`<br>
**@returns** *COA.Cmd* `this` instance (for chainability)

#### Cmd.comp
Set custom additional completion for current command.<br>
**@param** *Function* `fn` completion generation function,
invoked in the context of command instance.
Accepts parameters:<br>
- *Object* `opts` completion options<br>
It can return promise or any other value treated as result.<br>
**@returns** *COA.Cmd* `this` instance (for chainability)

#### Cmd.helpful
Make command "helpful", i.e. add -h --help flags for print usage.<br>
**@returns** *COA.Cmd* `this` instance (for chainability)

#### Cmd.completable
Adds shell completion to command, adds "completion" subcommand, that makes all the magic.<br>
Must be called only on root command.<br>
**@returns** *COA.Cmd* `this` instance (for chainability)

#### Cmd.usage
Build full usage text for current command instance.<br>
**@returns** *String* `usage` text
Expand Down Expand Up @@ -138,7 +156,6 @@ Set a long description for option to be used anywhere in text messages.<br>
**@param** *String* `_title` option title<br>
**@returns** *COA.Opt* `this` instance (for chainability)


#### Opt.short
Set a short key for option to be used with one hyphen from command line.<br>
**@param** *String* `_short`<br>
Expand Down Expand Up @@ -201,6 +218,15 @@ is present in parsed options (with any value).<br>
or any other value treated as result.<br>
**@returns** *COA.Opt* `this` instance (for chainability)

#### Opt.comp
Set custom additional completion for current option.<br>
**@param** *Function* `fn` completion generation function,
invoked in the context of command instance.
Accepts parameters:<br>
- *Object* `opts` completion options<br>
It can return promise or any other value treated as result.<br>
**@returns** *COA.Opt* `this` instance (for chainability)

#### Opt.end
Finish chain for current option and return parent command instance.<br>
**@returns** *COA.Cmd* `parent` command
Expand Down Expand Up @@ -249,14 +275,22 @@ Make argument value outputing stream.<br>
It's add useful validation and shortcut for STDOUT.<br>
**@returns** *COA.Arg* `this` instance (for chainability)

#### Opt.comp
Set custom additional completion for current argument.<br>
**@param** *Function* `fn` completion generation function,
invoked in the context of command instance.
Accepts parameters:<br>
- *Object* `opts` completion options<br>
It can return promise or any other value treated as result.<br>
**@returns** *COA.Arg* `this` instance (for chainability)

#### Arg.end
Finish chain for current option and return parent command instance.<br>
**@returns** *COA.Cmd* `parent` command


## TODO
* Program API for use COA-covered programs as modules
* Shell completion
* Localization
* Shell-mode
* Configs
Expand Down
13 changes: 12 additions & 1 deletion lib/arg.js
Expand Up @@ -3,7 +3,8 @@ Color = require('./color').Color;
Cmd = require('./cmd').Cmd;
Opt = require('./opt').Opt;
/**
## Argument
Argument
Unnamed entity. From command line arguments passed as list of unnamed values.
@namespace
@class Presents argument
Expand Down Expand Up @@ -57,6 +58,16 @@ exports.Arg = Arg = (function() {
*/
Arg.prototype.def = Opt.prototype.def;
/**
Set custom additional completion for current argument.
@param {Function} completion generation function,
invoked in the context of argument instance.
Accepts parameters:
- {Object} opts completion options
It can return promise or any other value treated as result.
@returns {COA.Arg} this instance (for chainability)
*/
Arg.prototype.comp = Cmd.prototype.comp;
/**
Make argument value inputting stream.
It's add useful validation and shortcut for STDIN.
@returns {COA.Arg} this instance (for chainability)
Expand Down
178 changes: 110 additions & 68 deletions lib/cmd.js
Expand Up @@ -5,7 +5,8 @@ path = require('path');
Color = require('./color').Color;
Q = require('q');
/**
## Command
Command
Top level entity. Commands may have options and arguments.
@namespace
@class Presents command
Expand Down Expand Up @@ -39,7 +40,10 @@ exports.Cmd = Cmd = (function() {
*/
Cmd.prototype.name = function(_name) {
this._name = _name;
return this._cmd._cmdsByName[_name] = this;
if (this._cmd !== this) {
this._cmd._cmdsByName[_name] = this;
}
return this;
};
/**
Set a long description for command to be used anywhere in text messages.
Expand Down Expand Up @@ -101,6 +105,19 @@ exports.Cmd = Cmd = (function() {
return this;
};
/**
Set custom additional completion for current command.
@param {Function} completion generation function,
invoked in the context of command instance.
Accepts parameters:
- {Object} opts completion options
It can return promise or any other value treated as result.
@returns {COA.Cmd} this instance (for chainability)
*/
Cmd.prototype.comp = function(_comp) {
this._comp = _comp;
return this;
};
/**
Apply function with arguments in context of command instance.
@param {Function} fn
@param {Array} args
Expand All @@ -121,6 +138,15 @@ exports.Cmd = Cmd = (function() {
return this.usage();
}).end();
};
/**
Adds shell completion to command, adds "completion" subcommand,
that makes all the magic.
Must be called only on root command.
@returns {COA.Cmd} this instance (for chainability)
*/
Cmd.prototype.completable = function() {
return this.cmd().name('completion').apply(require('./completion')).end();
};
Cmd.prototype._exit = function(msg, code) {
if (msg) {
sys.error(msg);
Expand Down Expand Up @@ -178,18 +204,50 @@ exports.Cmd = Cmd = (function() {
}
}
};
Cmd.prototype._parseArr = function(argv, opts, args) {
var arg, cmd, hitOnly, i, m, nonParsed, nonParsedArgs, nonParsedOpts, opt, res, _i, _len, _ref;
if (opts == null) {
opts = {};
Cmd.prototype._checkRequired = function(opts, args) {
var all, i, _results;
if (!(this._opts.filter(function(o) {
return o._only && o._name in opts;
})).length) {
all = this._opts.concat(this._args);
_results = [];
while (i = all.shift()) {
if (i._req && i._checkParsed(opts, args)) {
return this.reject(i._requiredText());
}
}
return _results;
}
if (args == null) {
args = {};
};
Cmd.prototype._parseCmd = function(argv, unparsed) {
var cmd, i, optSeen;
if (unparsed == null) {
unparsed = [];
}
nonParsedOpts = this._opts.concat();
argv = argv.concat();
optSeen = false;
while (i = argv.shift()) {
if (!i.indexOf('-')) {
nonParsedArgs || (nonParsedArgs = this._args.concat());
optSeen = true;
}
if (!optSeen && /^\w[\w-_]*$/.test(i) && (cmd = this._cmdsByName[i])) {
return cmd._parseCmd(argv, unparsed);
}
unparsed.push(i);
}
return {
cmd: this,
argv: unparsed
};
};
Cmd.prototype._parseOptsAndArgs = function(argv) {
var a, arg, args, i, m, nonParsed, nonParsedArgs, nonParsedOpts, opt, opts, res;
opts = {};
args = {};
nonParsedOpts = this._opts.concat();
nonParsedArgs = this._args.concat();
while (i = argv.shift()) {
if (i !== '--' && !i.indexOf('-')) {
if (m = i.match(/^(--\w[\w-_]*)=(.*)$/)) {
i = m[1];
argv.unshift(m[2]);
Expand All @@ -201,79 +259,63 @@ exports.Cmd = Cmd = (function() {
} else {
return this.reject("Unknown option: " + i);
}
} else if (!nonParsedArgs && /^\w[\w-_]*$/.test(i)) {
cmd = this._cmdsByName[i];
if (cmd) {
return cmd._parseArr(argv, opts, args);
} else {
nonParsedArgs = this._args.concat();
argv.unshift(i);
}
} else {
if (arg = (nonParsedArgs || (nonParsedArgs = this._args.concat())).shift()) {
if (arg._arr) {
nonParsedArgs.unshift(arg);
}
if (Q.isPromise(res = arg._parse(i, args))) {
return res;
if (i === '--') {
i = argv.splice(0);
}
i = Array.isArray(i) ? i : [i];
while (a = i.shift()) {
if (arg = nonParsedArgs.shift()) {
if (arg._arr) {
nonParsedArgs.unshift(arg);
}
if (Q.isPromise(res = arg._parse(a, args))) {
return res;
}
} else {
return this.reject("Unknown argument: " + a);
}
} else {
return this.reject("Unknown argument: " + i);
}
}
}
nonParsedArgs || (nonParsedArgs = this._args.concat());
hitOnly = false;
_ref = this._opts;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
opt = _ref[_i];
if (opt._only && opt._name in opts) {
hitOnly = true;
}
}
if (!hitOnly) {
nonParsed = nonParsedOpts.concat(nonParsedArgs);
while (i = nonParsed.shift()) {
if (i._req && i._checkParsed(opts, args)) {
return this.reject(i._requiredText());
}
if ('_def' in i) {
i._saveVal(opts, i._def);
}
nonParsed = nonParsedOpts.concat(nonParsedArgs);
while (i = nonParsed.shift()) {
if ('_def' in i) {
i._saveVal(opts, i._def);
}
}
return {
cmd: this,
opts: opts,
args: args
};
};
Cmd.prototype._parseArr = function(argv) {
var cmd, res, _ref;
_ref = this._parseCmd(argv), cmd = _ref.cmd, argv = _ref.argv;
if (Q.isPromise(res = cmd._parseOptsAndArgs(argv))) {
return res;
}
return {
cmd: cmd,
opts: res.opts,
args: res.args
};
};
Cmd.prototype._do = function(input, succ, err) {
var cmd, defer, parsed, _ref;
var cmd, defer, parsed;
defer = Q.defer();
parsed = this._parseArr(input);
cmd = parsed.cmd || this;
if ((_ref = cmd._act) != null) {
_ref.reduce(__bind(function(res, act) {
return res.then(__bind(function(params) {
var actRes, _ref2;
actRes = act.call(cmd, params.opts, params.args, params.res);
if (Q.isPromise(actRes)) {
return actRes;
} else {
if ((_ref2 = params.res) == null) {
params.res = actRes;
}
return params;
}
}, this));
}, this), defer.promise).fail(__bind(function(res) {
return err.call(cmd, res);
}, this)).then(__bind(function(res) {
return succ.call(cmd, res.res);
[this._checkRequired].concat(cmd._act || []).reduce(__bind(function(res, act) {
return res.then(__bind(function(res) {
return act.call(cmd, parsed.opts, parsed.args, res);
}, this));
}
return defer.resolve(parsed);
}, this), defer.promise).fail(__bind(function(res) {
return err.call(cmd, res);
}, this)).then(__bind(function(res) {
return succ.call(cmd, res);
}, this));
return defer.resolve(Q.isPromise(parsed) ? parsed : void 0);
};
/**
Parse arguments from simple format like NodeJS process.argv
Expand All @@ -288,8 +330,8 @@ exports.Cmd = Cmd = (function() {
}
cb = function(code) {
return function(res) {
var _ref;
return this._exit(res.toString(), (_ref = res.exitCode) != null ? _ref : code);
var _ref, _ref2;
return this._exit((_ref = res.stack) != null ? _ref : res.toString(), (_ref2 = res.exitCode) != null ? _ref2 : code);
};
};
this._do(argv, cb(0), cb(1));
Expand Down

0 comments on commit dfb69fd

Please sign in to comment.