diff --git a/GNUmakefile b/GNUmakefile
index d07f03b..bee42e2 100644
--- a/GNUmakefile
+++ b/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
diff --git a/README.md b/README.md
index 64acabb..9559f8f 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -97,10 +101,24 @@ Apply function with arguments in context of command instance.
**@param** *Array* `args`
**@returns** *COA.Cmd* `this` instance (for chainability)
+#### Cmd.comp
+Set custom additional completion for current command.
+**@param** *Function* `fn` 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.helpful
Make command "helpful", i.e. add -h --help flags for print usage.
**@returns** *COA.Cmd* `this` instance (for chainability)
+#### Cmd.completable
+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.usage
Build full usage text for current command instance.
**@returns** *String* `usage` text
@@ -138,7 +156,6 @@ Set a long description for option to be used anywhere in text messages.
**@param** *String* `_title` option title
**@returns** *COA.Opt* `this` instance (for chainability)
-
#### Opt.short
Set a short key for option to be used with one hyphen from command line.
**@param** *String* `_short`
@@ -201,6 +218,15 @@ is present in parsed options (with any value).
or any other value treated as result.
**@returns** *COA.Opt* `this` instance (for chainability)
+#### Opt.comp
+Set custom additional completion for current option.
+**@param** *Function* `fn` 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.Opt* `this` instance (for chainability)
+
#### Opt.end
Finish chain for current option and return parent command instance.
**@returns** *COA.Cmd* `parent` command
@@ -249,6 +275,15 @@ Make argument value outputing stream.
It's add useful validation and shortcut for STDOUT.
**@returns** *COA.Arg* `this` instance (for chainability)
+#### Opt.comp
+Set custom additional completion for current argument.
+**@param** *Function* `fn` 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.Arg* `this` instance (for chainability)
+
#### Arg.end
Finish chain for current option and return parent command instance.
**@returns** *COA.Cmd* `parent` command
@@ -256,7 +291,6 @@ Finish chain for current option and return parent command instance.
## TODO
* Program API for use COA-covered programs as modules
-* Shell completion
* Localization
* Shell-mode
* Configs
diff --git a/lib/arg.js b/lib/arg.js
index ef8a82f..f05d16d 100644
--- a/lib/arg.js
+++ b/lib/arg.js
@@ -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
@@ -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)
diff --git a/lib/cmd.js b/lib/cmd.js
index 351ff3c..0cb0172 100644
--- a/lib/cmd.js
+++ b/lib/cmd.js
@@ -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
@@ -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.
@@ -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
@@ -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);
@@ -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]);
@@ -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
@@ -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));
diff --git a/lib/completion.js b/lib/completion.js
new file mode 100644
index 0000000..5ad10df
--- /dev/null
+++ b/lib/completion.js
@@ -0,0 +1,125 @@
+/**
+Most of the code adopted from the npm package shell completion code.
+See https://github.com/isaacs/npm/blob/master/lib/completion.js
+*/
+var Q, complete, dumpScript, escape, getOpts, unescape;
+Q = require('q');
+escape = require('./shell').escape;
+unescape = require('./shell').unescape;
+module.exports = function() {
+ return this.title('Shell completion').helpful().arg().name('raw').title('Completion words').arr().end().act(function(opts, args) {
+ var argv, cmd, e, _ref;
+ if (process.platform === 'win32') {
+ e = new Error('shell completion not supported on windows');
+ e.code = 'ENOTSUP';
+ e.errno = require('constants').ENOTSUP;
+ return this.reject(e);
+ }
+ if (!(process.env.COMP_CWORD != null) || !(process.env.COMP_LINE != null) || !(process.env.COMP_POINT != null)) {
+ return dumpScript(this._cmd._name);
+ }
+ console.error('COMP_LINE: %s', process.env.COMP_LINE);
+ console.error('COMP_CWORD: %s', process.env.COMP_CWORD);
+ console.error('COMP_POINT: %s', process.env.COMP_POINT);
+ console.error('args: %j', args.raw);
+ opts = getOpts(args.raw);
+ _ref = this._cmd._parseCmd(opts.partialWords), cmd = _ref.cmd, argv = _ref.argv;
+ return Q.when(complete(cmd, opts), function(compls) {
+ console.error('filtered: %j', compls);
+ return console.log(compls.map(escape).join('\n'));
+ });
+ });
+};
+dumpScript = function(name) {
+ var defer, fs, path;
+ fs = require('fs');
+ path = require('path');
+ defer = Q.defer();
+ fs.readFile(path.resolve(__dirname, 'completion.sh'), 'utf8', function(err, d) {
+ var onError;
+ if (err) {
+ return defer.reject(err);
+ }
+ d = d.replace(/{{cmd}}/g, path.basename(name)).replace(/^\#\!.*?\n/, '');
+ onError = function(err) {
+ if (err.errno === require('constants').EPIPE) {
+ process.stdout.removeListener('error', onError);
+ return defer.resolve();
+ } else {
+ return defer.reject(err);
+ }
+ };
+ process.stdout.on('error', onError);
+ return process.stdout.write(d, function() {
+ return defer.resolve();
+ });
+ });
+ return defer.promise;
+};
+getOpts = function(argv) {
+ var i, line, partialLine, partialWord, partialWords, point, w, word, words;
+ line = process.env.COMP_LINE;
+ w = +process.env.COMP_CWORD;
+ point = +process.env.COMP_POINT;
+ words = argv.map(unescape);
+ word = words[w];
+ partialLine = line.substr(0, point);
+ partialWords = words.slice(0, w);
+ partialWord = argv[w] || '';
+ i = partialWord.length;
+ while (partialWord.substr(0, i) !== partialLine.substr(-1 * i) && i > 0) {
+ i--;
+ }
+ partialWord = unescape(partialWord.substr(0, i));
+ if (partialWord) {
+ partialWords.push(partialWord);
+ }
+ return {
+ line: line,
+ w: w,
+ point: point,
+ words: words,
+ word: word,
+ partialLine: partialLine,
+ partialWords: partialWords,
+ partialWord: partialWord
+ };
+};
+complete = function(cmd, opts) {
+ var compls, m, o, opt, optPrefix, optWord;
+ compls = [];
+ if (opts.partialWord.indexOf('-')) {
+ compls = Object.keys(cmd._cmdsByName);
+ } else {
+ if (m = opts.partialWord.match(/^(--\w[\w-_]*)=(.*)$/)) {
+ optWord = m[1];
+ optPrefix = optWord + '=';
+ } else {
+ compls = Object.keys(cmd._optsByKey);
+ }
+ }
+ if (!(o = opts.partialWords[opts.w - 1]).indexOf('-')) {
+ optWord = o;
+ }
+ if (optWord && (opt = cmd._optsByKey[optWord])) {
+ if (!opt._flag && opt._comp) {
+ compls = Q.join(compls, Q.when(opt._comp(opts), function(c, o) {
+ return c.concat(o.map(function(v) {
+ return (optPrefix || '') + v;
+ }));
+ }));
+ }
+ }
+ if (cmd._comp) {
+ compls = Q.join(compls, Q.when(cmd._comp(opts)), function(c, o) {
+ return c.concat(o);
+ });
+ }
+ return Q.when(compls, function(compls) {
+ console.error('partialWord: %s', opts.partialWord);
+ console.error('compls: %j', compls);
+ return compls.filter(function(c) {
+ return c.indexOf(opts.partialWord) === 0;
+ });
+ });
+};
\ No newline at end of file
diff --git a/lib/completion.sh b/lib/completion.sh
new file mode 100644
index 0000000..6d15c87
--- /dev/null
+++ b/lib/completion.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+###-begin-{{cmd}}-completion-###
+#
+# {{cmd}} command completion script
+#
+# Installation: {{cmd}} completion >> ~/.bashrc (or ~/.zshrc)
+# Or, maybe: {{cmd}} completion > /usr/local/etc/bash_completion.d/{{cmd}}
+#
+
+COMP_WORDBREAKS=${COMP_WORDBREAKS/=/}
+COMP_WORDBREAKS=${COMP_WORDBREAKS/@/}
+export COMP_WORDBREAKS
+
+if complete &>/dev/null; then
+ _{{cmd}}_completion () {
+ local si="$IFS"
+ IFS=$'\n' COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \
+ COMP_LINE="$COMP_LINE" \
+ COMP_POINT="$COMP_POINT" \
+ {{cmd}} completion -- "${COMP_WORDS[@]}" \
+ 2>/dev/null)) || return $?
+ IFS="$si"
+ }
+ complete -F _{{cmd}}_completion {{cmd}}
+elif compctl &>/dev/null; then
+ _{{cmd}}_completion () {
+ local cword line point words si
+ read -Ac words
+ read -cn cword
+ let cword-=1
+ read -l line
+ read -ln point
+ si="$IFS"
+ IFS=$'\n' reply=($(COMP_CWORD="$cword" \
+ COMP_LINE="$line" \
+ COMP_POINT="$point" \
+ {{cmd}} completion -- "${words[@]}" \
+ 2>/dev/null)) || return $?
+ IFS="$si"
+ }
+ compctl -K _{{cmd}}_completion {{cmd}}
+fi
+###-end-{{cmd}}-completion-###
diff --git a/lib/opt.js b/lib/opt.js
index 87f45fa..b4508df 100644
--- a/lib/opt.js
+++ b/lib/opt.js
@@ -1,9 +1,12 @@
-var Cmd, Color, Opt, fs;
+var Cmd, Color, Opt, Q, fs;
+var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
fs = require('fs');
+Q = require('q');
Color = require('./color').Color;
Cmd = require('./cmd').Cmd;
/**
-## Option
+Option
+
Named entity. Options may have short and long keys for use from command line.
@namespace
@class Presents option
@@ -172,12 +175,14 @@ exports.Opt = Opt = (function() {
if (name in opts) {
res = act.apply(this, arguments);
if (opt._only) {
- return this.reject({
- toString: function() {
- return res;
- },
- exitCode: 0
- });
+ return Q.when(res, __bind(function(res) {
+ return this.reject({
+ toString: function() {
+ return res.toString();
+ },
+ exitCode: 0
+ });
+ }, this));
} else {
return res;
}
@@ -185,6 +190,16 @@ exports.Opt = Opt = (function() {
});
return this;
};
+ /**
+ Set custom additional completion for current option.
+ @param {Function} completion generation function,
+ invoked in the context of option instance.
+ Accepts parameters:
+ - {Object} opts completion options
+ It can return promise or any other value treated as result.
+ @returns {COA.Opt} this instance (for chainability)
+ */
+ Opt.prototype.comp = Cmd.prototype.comp;
Opt.prototype._saveVal = function(opts, val) {
var _name;
if (this._val) {
diff --git a/lib/shell.js b/lib/shell.js
new file mode 100644
index 0000000..dfd8c42
--- /dev/null
+++ b/lib/shell.js
@@ -0,0 +1,12 @@
+exports.unescape = function(w) {
+ w = w.charAt(0) === '"' ? w.replace(/^"|([^\\])"$/g, '$1') : w.replace(/\\ /g, ' ');
+ return w.replace(/\\("|'|\$|`|\\)/g, '$1');
+};
+exports.escape = function(w) {
+ w = w.replace(/(["'$`\\])/g, '\\$1');
+ if (w.match(/\s+/)) {
+ return '"' + w + '"';
+ } else {
+ return w;
+ }
+};
\ No newline at end of file
diff --git a/package.json b/package.json
index 15c7772..02d43b1 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,9 @@
"dependencies" : {
"q": ">=0.7.1"
},
+ "devDependencies" : {
+ "vows": ">=0.5.12"
+ },
"engines" : [ "node ~0.4.0" ],
"licenses" : [ { "type" : "AS IS" } ]
}
diff --git a/src/arg.coffee b/src/arg.coffee
index 604f3f1..1620937 100644
--- a/src/arg.coffee
+++ b/src/arg.coffee
@@ -3,7 +3,8 @@ 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
@@ -62,6 +63,17 @@ exports.Arg = class Arg
###
def: Opt::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)
+ ###
+ comp: Cmd::comp
+
###*
Make argument value inputting stream.
It's add useful validation and shortcut for STDIN.
diff --git a/src/cmd.coffee b/src/cmd.coffee
index a2db43b..31f006b 100644
--- a/src/cmd.coffee
+++ b/src/cmd.coffee
@@ -6,7 +6,8 @@ Q = require('q')
#inspect = require('eyes').inspector { maxLength: 99999, stream: process.stderr }
###*
-## Command
+Command
+
Top level entity. Commands may have options and arguments.
@namespace
@class Presents command
@@ -41,7 +42,9 @@ exports.Cmd = class Cmd
@param {String} _name command name
@returns {COA.Cmd} this instance (for chainability)
###
- name: (@_name) -> @_cmd._cmdsByName[_name] = @
+ name: (@_name) ->
+ if @_cmd isnt @ then @_cmd._cmdsByName[_name] = @
+ @
###*
Set a long description for command to be used anywhere in text messages.
@@ -94,6 +97,17 @@ exports.Cmd = class Cmd
@
+ ###*
+ 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)
+ ###
+ comp: (@_comp) -> @
+
###*
Apply function with arguments in context of command instance.
@param {Function} fn
@@ -118,6 +132,17 @@ exports.Cmd = class Cmd
return @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)
+ ###
+ completable: ->
+ @cmd()
+ .name('completion')
+ .apply(require './completion')
+ .end()
_exit: (msg, code) ->
if msg then sys.error msg
@@ -175,14 +200,36 @@ exports.Cmd = class Cmd
else
opts.splice(pos, 1)[0]
- _parseArr: (argv, opts = {}, args = {}) ->
- nonParsedOpts = @_opts.concat()
+ _checkRequired: (opts, args) ->
+ if not (@_opts.filter (o) -> o._only and o._name of opts).length
+ all = @_opts.concat @_args
+ while i = all.shift()
+ if i._req and i._checkParsed opts, args
+ return @reject i._requiredText()
+ _parseCmd: (argv, unparsed = []) ->
+ argv = argv.concat()
+ optSeen = false
while i = argv.shift()
- # opt
if not i.indexOf '-'
+ optSeen = true
+ if not optSeen and /^\w[\w-_]*$/.test(i) and cmd = @_cmdsByName[i]
+ return cmd._parseCmd argv, unparsed
+
+ unparsed.push i
+
+ { cmd: @, argv: unparsed }
- nonParsedArgs or= @_args.concat()
+ _parseOptsAndArgs: (argv) ->
+ opts = {}
+ args = {}
+
+ nonParsedOpts = @_opts.concat()
+ nonParsedArgs = @_args.concat()
+
+ while i = argv.shift()
+ # opt
+ if i isnt '--' and not i.indexOf '-'
if m = i.match /^(--\w[\w-_]*)=(.*)$/
i = m[1]
@@ -194,65 +241,52 @@ exports.Cmd = class Cmd
else
return @reject "Unknown option: #{ i }"
- # cmd
- else if not nonParsedArgs and /^\w[\w-_]*$/.test i
- cmd = @_cmdsByName[i]
- if cmd
- return cmd._parseArr argv, opts, args
- else
- nonParsedArgs = @_args.concat()
- argv.unshift i
-
# arg
else
- if arg = (nonParsedArgs or= @_args.concat()).shift()
- if arg._arr then nonParsedArgs.unshift arg
- if Q.isPromise res = arg._parse i, args
- return res
- else
- return @reject "Unknown argument: #{ i }"
+ if i is '--'
+ i = argv.splice(0)
- nonParsedArgs or= @_args.concat()
+ i = if Array.isArray(i) then i else [i]
- hitOnly = false
- for opt in @_opts
- if opt._only and opt._name of opts
- hitOnly = true
+ while a = i.shift()
+ if arg = nonParsedArgs.shift()
+ if arg._arr then nonParsedArgs.unshift arg
+ if Q.isPromise res = arg._parse a, args
+ return res
+ else
+ return @reject "Unknown argument: #{ a }"
- if not hitOnly
- nonParsed = nonParsedOpts.concat nonParsedArgs
- while i = nonParsed.shift()
- if i._req and i._checkParsed opts, args
- return @reject i._requiredText()
- if '_def' of i
- i._saveVal opts, i._def
+ # defaults
+ nonParsed = nonParsedOpts.concat nonParsedArgs
+ while i = nonParsed.shift()
+ if '_def' of i then i._saveVal opts, i._def
- { cmd: @, opts: opts, args: args }
+ { opts: opts, args: args }
+
+ _parseArr: (argv) ->
+ { cmd, argv } = @_parseCmd argv
+ if Q.isPromise res = cmd._parseOptsAndArgs argv
+ return res
+ { cmd: cmd, opts: res.opts, args: res.args }
_do: (input, succ, err) ->
defer = Q.defer()
parsed = @_parseArr input
cmd = parsed.cmd or @
- cmd._act?.reduce(
+ [@_checkRequired].concat(cmd._act or []).reduce(
(res, act) =>
- res.then (params) =>
- actRes = act.call(
+ res.then (res) =>
+ act.call(
cmd
- params.opts
- params.args
- params.res)
-
- if Q.isPromise actRes
- actRes
- else
- params.res ?= actRes
- params
+ parsed.opts
+ parsed.args
+ res)
defer.promise
)
.fail((res) => err.call cmd, res)
- .then((res) => succ.call cmd, res.res)
+ .then((res) => succ.call cmd, res)
- defer.resolve parsed
+ defer.resolve(if Q.isPromise parsed then parsed)
###*
Parse arguments from simple format like NodeJS process.argv
@@ -261,7 +295,7 @@ exports.Cmd = class Cmd
@returns {COA.Cmd} this instance (for chainability)
###
run: (argv = process.argv.slice(2)) ->
- cb = (code) -> (res) -> @_exit res.toString(), res.exitCode ? code
+ cb = (code) -> (res) -> @_exit res.stack ? res.toString(), res.exitCode ? code
@_do argv, cb(0), cb(1)
@
diff --git a/src/completion.coffee b/src/completion.coffee
new file mode 100644
index 0000000..2c1e358
--- /dev/null
+++ b/src/completion.coffee
@@ -0,0 +1,156 @@
+###*
+Most of the code adopted from the npm package shell completion code.
+See https://github.com/isaacs/npm/blob/master/lib/completion.js
+###
+
+Q = require 'q'
+escape = require('./shell').escape
+unescape = require('./shell').unescape
+
+module.exports = ->
+ @title('Shell completion')
+ .helpful()
+ .arg()
+ .name('raw')
+ .title('Completion words')
+ .arr()
+ .end()
+ .act (opts, args) ->
+ if process.platform == 'win32'
+ e = new Error 'shell completion not supported on windows'
+ e.code = 'ENOTSUP'
+ e.errno = require('constants').ENOTSUP
+ return @reject(e)
+
+ # if the COMP_* isn't in the env, then just dump the script
+ if !process.env.COMP_CWORD? or !process.env.COMP_LINE? or !process.env.COMP_POINT?
+ return dumpScript(@_cmd._name)
+
+ console.error 'COMP_LINE: %s', process.env.COMP_LINE
+ console.error 'COMP_CWORD: %s', process.env.COMP_CWORD
+ console.error 'COMP_POINT: %s', process.env.COMP_POINT
+ console.error 'args: %j', args.raw
+
+ # completion opts
+ opts = getOpts args.raw
+
+ # cmd
+ { cmd, argv } = @_cmd._parseCmd opts.partialWords
+ Q.when complete(cmd, opts), (compls) ->
+ console.error 'filtered: %j', compls
+ console.log compls.map(escape).join('\n')
+
+
+dumpScript = (name) ->
+ fs = require 'fs'
+ path = require 'path'
+ defer = Q.defer()
+
+ fs.readFile path.resolve(__dirname, 'completion.sh'), 'utf8', (err, d) ->
+ if err then return defer.reject err
+ d = d.replace(/{{cmd}}/g, path.basename name).replace(/^\#\!.*?\n/, '')
+
+ onError = (err) ->
+ # Darwin is a real dick sometimes.
+ #
+ # This is necessary because the "source" or "." program in
+ # bash on OS X closes its file argument before reading
+ # from it, meaning that you get exactly 1 write, which will
+ # work most of the time, and will always raise an EPIPE.
+ #
+ # Really, one should not be tossing away EPIPE errors, or any
+ # errors, so casually. But, without this, `. <(cmd completion)`
+ # can never ever work on OS X.
+ if err.errno == require('constants').EPIPE
+ process.stdout.removeListener 'error', onError
+ defer.resolve()
+ else
+ defer.reject(err)
+
+ process.stdout.on 'error', onError
+ process.stdout.write d, -> defer.resolve()
+
+ defer.promise
+
+
+getOpts = (argv) ->
+ # get the partial line and partial word, if the point isn't at the end
+ # ie, tabbing at: cmd foo b|ar
+ line = process.env.COMP_LINE
+ w = +process.env.COMP_CWORD
+ point = +process.env.COMP_POINT
+ words = argv.map unescape
+ word = words[w]
+ partialLine = line.substr 0, point
+ partialWords = words.slice 0, w
+
+ # figure out where in that last word the point is
+ partialWord = argv[w] or ''
+ i = partialWord.length
+ while partialWord.substr(0, i) isnt partialLine.substr(-1 * i) and i > 0
+ i--
+ partialWord = unescape partialWord.substr 0, i
+ if partialWord then partialWords.push partialWord
+
+ {
+ line: line
+ w: w
+ point: point
+ words: words
+ word: word
+ partialLine: partialLine
+ partialWords: partialWords
+ partialWord: partialWord
+ }
+
+
+complete = (cmd, opts) ->
+ compls = []
+
+ # complete on cmds
+ if opts.partialWord.indexOf('-')
+ compls = Object.keys(cmd._cmdsByName)
+ # Complete on required opts without '-' in last partial word
+ # (if required not already specified)
+ #
+ # Commented out because of uselessness:
+ # -b, --block suggest results in '-' on cmd line;
+ # next completion suggest all options, because of '-'
+ #.concat Object.keys(cmd._optsByKey).filter (v) -> cmd._optsByKey[v]._req
+ else
+ # complete on opt values: --opt=| case
+ if m = opts.partialWord.match /^(--\w[\w-_]*)=(.*)$/
+ optWord = m[1]
+ optPrefix = optWord + '='
+ else
+ # complete on opts
+ # don't complete on opts in case of --opt=val completion
+ # TODO: don't complete on opts in case of unknown arg after commands
+ # TODO: complete only on opts with arr() or not already used
+ # TODO: complete only on full opts?
+ compls = Object.keys cmd._optsByKey
+
+ # complete on opt values: next arg case
+ if not (o = opts.partialWords[opts.w - 1]).indexOf '-'
+ optWord = o
+
+ # complete on opt values: completion
+ if optWord and opt = cmd._optsByKey[optWord]
+ if not opt._flag and opt._comp
+ compls = Q.join compls, Q.when opt._comp(opts), (c, o) ->
+ c.concat o.map (v) -> (optPrefix or '') + v
+
+ # TODO: complete on args values (context aware, custom completion?)
+
+ # custom completion on cmds
+ if cmd._comp
+ compls = Q.join compls, Q.when(cmd._comp(opts)), (c, o) ->
+ c.concat o
+
+ # TODO: context aware custom completion on cmds, opts and args
+ # (can depend on already entered values, especially options)
+
+ Q.when compls, (compls) ->
+ console.error 'partialWord: %s', opts.partialWord
+ console.error 'compls: %j', compls
+ compls.filter (c) -> c.indexOf(opts.partialWord) is 0
diff --git a/src/opt.coffee b/src/opt.coffee
index fe56163..f23c919 100644
--- a/src/opt.coffee
+++ b/src/opt.coffee
@@ -1,9 +1,11 @@
fs = require 'fs'
+Q = require 'q'
Color = require('./color').Color
Cmd = require('./cmd').Cmd
###*
-## Option
+Option
+
Named entity. Options may have short and long keys for use from command line.
@namespace
@class Presents option
@@ -152,14 +154,26 @@ exports.Opt = class Opt
if name of opts
res = act.apply @, arguments
if opt._only
- @reject {
- toString: -> res
- exitCode: 0
- }
+ Q.when res, (res) =>
+ @reject {
+ toString: -> res.toString()
+ exitCode: 0
+ }
else
res
@
+ ###*
+ Set custom additional completion for current option.
+ @param {Function} completion generation function,
+ invoked in the context of option instance.
+ Accepts parameters:
+ - {Object} opts completion options
+ It can return promise or any other value treated as result.
+ @returns {COA.Opt} this instance (for chainability)
+ ###
+ comp: Cmd::comp
+
_saveVal: (opts, val) ->
if @_val then val = @_val val
if @_arr
diff --git a/src/shell.coffee b/src/shell.coffee
new file mode 100644
index 0000000..efad108
--- /dev/null
+++ b/src/shell.coffee
@@ -0,0 +1,10 @@
+exports.unescape = (w) ->
+ w = if w.charAt(0) is '"'
+ w.replace(/^"|([^\\])"$/g, '$1')
+ else
+ w.replace(/\\ /g, ' ')
+ w.replace(/\\("|'|\$|`|\\)/g, '$1')
+
+exports.escape = (w) ->
+ w = w.replace(/(["'$`\\])/g,'\\$1')
+ if w.match(/\s+/) then '"' + w + '"' else w
diff --git a/test/shell-test.js b/test/shell-test.js
new file mode 100644
index 0000000..72cc0c3
--- /dev/null
+++ b/test/shell-test.js
@@ -0,0 +1,59 @@
+var vows = require('vows'),
+ assert = require('assert'),
+ shell = require('../lib/shell');
+
+vows.describe('coa/shell').addBatch({
+
+ 'The shell module': {
+
+ '`escape`': {
+
+ topic: function() {
+ return shell.escape;
+ },
+
+ 'Should wrap values with spaces in double quotes': function(escape) {
+ assert.equal(escape('asd abc'), '"asd abc"');
+ },
+
+ 'Should escape double quote "': function(escape) {
+ assert.equal(escape('"asd'), '\\"asd');
+ },
+
+ "Should escape single quote '": function(escape) {
+ assert.equal(escape("'asd"), "\\'asd");
+ },
+
+ 'Should escape backslash \\': function(escape) {
+ assert.equal(escape('\\asd'), '\\\\asd');
+ },
+
+ 'Should escape dollar $': function(escape) {
+ assert.equal(escape('$asd'), '\\$asd');
+ },
+
+ 'Should escape backtick `': function(escape) {
+ assert.equal(escape('`asd'), '\\`asd');
+ }
+
+ },
+
+ '`unescape`': {
+
+ topic: function() {
+ return shell.unescape;
+ },
+
+ 'Should strip double quotes at the both ends': function(unescape) {
+ assert.equal(unescape('"asd"'), 'asd');
+ },
+
+ 'Should not strip escaped double quotes at the both ends': function(unescape) {
+ assert.equal(unescape('\\"asd\\"'), '"asd"');
+ }
+
+ }
+
+ }
+
+}).export(module);
diff --git a/tests/args.js b/tests/args.js
new file mode 100644
index 0000000..9670ace
--- /dev/null
+++ b/tests/args.js
@@ -0,0 +1,21 @@
+var argv = process.argv.slice(2);
+require('../lib/coa').Cmd()
+ .name('arg')
+ .title('Args test')
+ .helpful()
+ .opt()
+ .name('opt').title('Option')
+ .long('opt').short('o')
+ .end()
+ .arg()
+ .name('arg1').title('First arg')
+ .end()
+ .arg()
+ .name('arg2').title('Second array arg')
+ .arr()
+ .end()
+ .act(function(opts, args) {
+ console.log(opts);
+ console.log(args);
+ })
+ .run(argv.length? argv : ['-o', 'value', 'value', 'value 1', 'value 2']);
diff --git a/tests/rarg.js b/tests/rarg.js
new file mode 100644
index 0000000..3523c00
--- /dev/null
+++ b/tests/rarg.js
@@ -0,0 +1,13 @@
+var argv = process.argv.slice(2);
+require('../lib/coa').Cmd()
+ .name('rarg')
+ .title('Raw arg test')
+ .helpful()
+ .arg()
+ .name('raw').title('Raw arg')
+ .arr()
+ .end()
+ .act(function(opts, args) {
+ console.log(args);
+ })
+ .run(argv.length? argv : ['--', 'raw', 'arg', 'values']);