lineparser
Version:
A meta-data driven command line parser for Node.js
476 lines (412 loc) • 16.3 kB
JavaScript
// meta data schema
var flag_attrs = ['short_name', 'name', 'description'];
var param_attrs = ['short_name', 'name', 'description', 'default'];
var usage_attrs = ['subcommand', 'options', 'args', 'description', 'handler'];
// meta manager
function meta_manager(meta) {
// init
flag_attr_idx = {};
for (var i = 0; i < flag_attrs.length; ++i) {
flag_attr_idx[flag_attrs[i]] = i;
}
param_attr_idx = {};
for (var i = 0; i < param_attrs.length; ++i) {
param_attr_idx[param_attrs[i]] = i;
}
usage_attr_idx = {};
for (var i = 0; i < usage_attrs.length; ++i) {
usage_attr_idx[usage_attrs[i]] = i;
}
// internal functions definitions
function _flag_attr_value(flag, attr_name) {
return flag_attr_idx[attr_name] >= 0 ? flag[flag_attr_idx[attr_name]] : null;
}
function _param_attr_value(param, attr_name) {
return param_attr_idx[attr_name] >= 0 ? param[param_attr_idx[attr_name]] : null;
}
function _usage_attr_value(usage, attr_name) {
return usage_attr_idx[attr_name] >= 0 ? usage[usage_attr_idx[attr_name]] : null;
}
function _get_opt_by_attr(attr_name, attr_value) {
for (var i = 0; i < meta.options.flags.length; ++i) {
var flag = meta.options.flags[i];
if (attr_value == _flag_attr_value(flag, attr_name)) {
return { type : 'flag', data : flag };
}
};
for (var i = 0; i < meta.options.parameters.length; ++i) {
var param = meta.options.parameters[i];
if (attr_value == _param_attr_value(param, attr_name)) {
return { type : 'parameter', data : param };
}
};
return null;
}
function _query_attr_value(select_attr_name, where_attr_name, where_attr_value) {
var opt = _get_opt_by_attr(where_attr_name, where_attr_value);
if (null != opt) {
if ('parameter' == opt.type) {
return _param_attr_value(opt.data, select_attr_name);
}
else if ('flag' == opt.type) {
return _flag_attr_value(opt.data, select_attr_name);
}
}
return null;
}
function _get_opt(name) {
return (1 == name.length ? _get_opt_by_attr('short_name', name) : _get_opt_by_attr('name', name));
}
function _is_flag(name) {
if (null == name) return false;
var opt = _get_opt(name);
return null != opt && 'flag' == opt.type;
}
function _is_param(name) {
if (null == name) return false;
var opt = _get_opt(name);
return null != opt && 'parameter' == opt.type;
}
function _opt_full_name(name) {
var opt = _get_opt(name);
var full_name = null;
if (null != opt) {
if ('flag' == opt.type) {
full_name =_flag_attr_value(opt.data, 'name');
}
else if ('parameter' == opt.type) {
full_name = _param_attr_value(opt.data, 'name');
}
}
return full_name;
}
function _opt_short_name(name) {
var opt = _get_opt(name);
var short_name = null;
if (null != opt) {
if ('flag' == opt.type) {
short_name =_flag_attr_value(opt.data, 'short_name');
}
else if ('parameter' == opt.type) {
short_name = _param_attr_value(opt.data, 'short_name');
}
}
return short_name;
}
function _opt_alias(name) {
if (null == name) {
return name;
}
if (1 == name.length) {
return _opt_full_name(name);
}
else {
return _opt_short_name(name);
}
}
return {
flag_attr_value : _flag_attr_value,
param_attr_value : _param_attr_value,
usage_attr_value : _usage_attr_value,
get_opt : _get_opt,
query_attr_value : _query_attr_value,
is_flag : _is_flag,
is_param : _is_param,
opt_full_name : _opt_full_name,
opt_short_name : _opt_short_name,
opt_alias : _opt_alias
};
}
// lineparser
function init(meta) {
var mm = meta_manager(meta);
function _is_blank(str) {
for (var i = 0; null != str && i < str.length; ++i) {
if (' ' != str.charAt(i)) {
return false;
}
}
return true;
}
function _check_meta_subcmds() {
if (null == meta.subcommands) {
meta.subcommands = [];
}
if (! meta.subcommands instanceof Array) {
throw new Error('Invalid subcommands definition, type: ' + typeof(meta.subcommands));
}
for (var i = 0; i < meta.subcommands.length; ++i) {
var subcmd = meta.subcommands[i];
if (_is_blank(subcmd)) {
throw new Error('Invalid subcommand "' + subcmd + '"');
}
}
return true;
}
function _check_meta_options() {
if (null == meta.options) meta.options = {};
if (null == meta.options.flags) meta.options.flags = [];
if (null == meta.options.parameters) meta.options.parameters = [];
// options.flags
for (var i = 0; i < meta.options.flags.length; ++i) {
var flag = meta.options.flags[i];
// check flag type
if (! flag instanceof Array) {
throw new Error('Invalid flag definition, type: ' + typeof(flag));
}
// check name
var short_name = mm.flag_attr_value(flag, 'short_name');
var name = mm.flag_attr_value(flag, 'name');
if (_is_blank(short_name) && _is_blank(name)) {
throw new Error("Flag name can't be empty");
}
if (null != short_name && 1 != short_name.length) {
throw new Error('Invalid flag short name: ' + short_name);
}
if (null != name && name.length <= 1) {
throw new Error('Invalid flag name: ' + name);
}
}
// options.parameters
for (var i = 0; i < meta.options.parameters.length; ++i) {
var param = meta.options.parameters[i];
// check flag type
if (! param instanceof Array) {
throw new Error('Invalid parameter definition, type: ' + typeof(param));
}
// check name
var short_name = mm.param_attr_value(param, 'short_name');
var name = mm.param_attr_value(param, 'name');
if (null == short_name && null == name) {
throw new Error("Parameter name must not be empty");
}
if (null != short_name && 1 != short_name.length) {
throw new Error('Invalid parameter short name: ' + short_name);
}
if (null != name && name.length <= 1) {
throw new Error('Invalid parameter name: ' + name);
}
}
}
function _check_meta_usages() {
if (null == meta.usages || 0 == meta.usages.length) {
throw new Error("Usages can't be null or empty");
}
for (var i = 0; i < meta.usages.length; ++i) {
var usage = meta.usages[i];
var subcmd = mm.usage_attr_value(usage, 'subcommand');
if (null != subcmd && -1 == meta.subcommands.indexOf(subcmd)) {
throw new Error('Undefined subcommand "' + subcmd + '"');
}
}
}
function _validate(meta) {
if (null == meta) {
throw new Error('Meta data is ' + meta);
}
if (null == meta.program) meta.program = process.argv[1];
if (null == meta.name) meta.name = meta.program;
_check_meta_subcmds();
_check_meta_options();
_check_meta_usages();
return true
}
function _help() {
var s = meta.name;
if (meta.version) {
s += ' ' + meta.version
}
s += '\n\n';
s += 'Usage: ' + meta.program + ' [<subcommand>] [options...] [args...]\n\n';
meta.usages.forEach(function(u, i, usages) {
var descr = mm.usage_attr_value(u, 'description');
var subcmd = mm.usage_attr_value(u, 'subcommand');
var options = mm.usage_attr_value(u, 'options');
var args = mm.usage_attr_value(u, 'args');
s += '' + (i + 1) + ". " + (null != descr ? descr : '') + '\n';
s += meta.program;
if (subcmd) {
s += ' ' + subcmd;
}
if (options && options.length > 0) {
options.forEach(function(opt_name, i) {
var optional = ('[' == opt_name.charAt(0) && ']' == opt_name.charAt(opt_name.length-1));
optional && (opt_name = opt_name.substr(1, opt_name.length - 2));
if (null == opt_name) {
throw new Error('Undefined option "' + opt_name + '"');
}
else {
var str = (opt_name.length > 1 ? '--' : '-');
if (mm.is_flag(opt_name)) {
str += opt_name;
}
else if (mm.is_param(opt_name)) {
if (1 == opt_name.length) {
full_name = mm.opt_full_name(opt_name);
(null == full_name) && (full_name = opt_name);
}
else {
full_name = opt_name;
}
str += opt_name + ' <' + full_name + '>';
}
else {
throw new Error('Undefined option "' + opt_name + '"');
}
optional ? (s += ' [' + str + ']') : (s += ' ' + str);
}
});
}
if (args && args.length > 0) {
args.forEach(function(arg, i) {
s += ' <' + arg + '>';
});
}
s += '\n\n';
});
return s;
}
function _parse(argv, token) {
var i;
// use command line args by default
if (null == argv) {
argv = process.argv.slice(2);
}
if (! argv instanceof Array) {
throw new Error('Invalid arguments type: ' + typeof(argv));
}
for (i = 0; i < meta.usages.length; ++i) {
var usage = meta.usages[i];
var r = _match_usage(usage, argv);
if (r.matched) {
var handler = mm.usage_attr_value(usage, 'handler');
r.help = _help;
handler(r, token);
break;
}
}
function _is_optional(name) {
return '[' == name.charAt(0) && ']' == name.charAt(name.length-1);
}
function unwrap_name(name) {
if (_is_optional(name)) {
return name.substr(1, name.length - 2);
}
if (name.length > 2 && '-' == name.charAt(0) && '-' == name.charAt(1)) {
return name.substr(2);
}
if (name.length > 1 && '-' == name.charAt(0)) {
return name.substr(1);
}
return name;
}
function _match_usage(usage, argv) {
var r = { matched : false, subcommand : null, flags : {}, parameters : {}, args : [] };
var has_subcmd = false;
var subcmd = mm.usage_attr_value(usage, 'subcommand');
if (null != subcmd) {
// match subcommand
if (0 == argv.length || subcmd != argv[0]) {
return r;
}
r.subcommand = subcmd;
has_subcmd = true;
}
// parse command line argv
var is_arg = false;
for (var i = (has_subcmd ? 1 : 0); i < argv.length; ++i) {
if (is_arg) {
r.args.push(argv[i]);
}
else {
// check if argv[i] is an option
var opt_name = null;
if (argv[i].length > 2 && 0 == argv[i].indexOf('--')) {
opt_name = argv[i].substr(2);
}
else if (argv[i].length > 1 && 0 == argv[i].indexOf('-')) {
opt_name = argv[i].substr(1);
}
if (null != opt_name) {
var alias = mm.opt_alias(opt_name);
if (mm.is_param(opt_name)) {
if (i + 1 < argv.length) {
if (null == r.parameters[opt_name]) {
r.parameters[opt_name] = argv[i+1];
if (alias) {
r.parameters[alias] = argv[i+1];
}
}
else if (r.parameters[opt_name] instanceof Array) {
r.parameters[opt_name].push(argv[i+1]);
if (alias) {
r.parameters[alias].push(argv[i+1]);
}
}
else {
r.parameters[opt_name] = [r.parameters[opt_name], argv[i+1]];
if (alias) {
r.parameters[alias] = [r.parameters[alias], argv[i+1]];
}
}
++i;
}
}
else if (mm.is_flag(opt_name)) {
r.flags[opt_name] = true;
if (alias) {
r.flags[alias] = true;
}
}
}
else {
r.args.push(argv[i]);
is_arg = true;
}
}
}
// try to match args against the usage pattern
var usage_options = (null == usage[1] ? [] : usage[1]);
for (var i = 0; i < usage_options.length; ++i) {
var usage_opt = usage_options[i];
var optional = _is_optional(usage_opt);
var opt_name = unwrap_name(usage_opt);
var is_param = mm.is_param(opt_name);
if (!optional) {
if (is_param) {
if (null == r.parameters[opt_name]) {
return r;
}
}
else {
if (null == r.flags[opt_name]) {
return r;
}
}
}
else {
if (is_param && null == r.parameters[opt_name]) {
var default_value = (1 == opt_name.length
? mm.query_attr_value('default', 'short_name', opt_name)
: mm.query_attr_value('default', 'name', opt_name)
);
if (null != default_value) {
r.parameters[opt_name] = default_value;
var alias = mm.opt_alias(opt_name);
if (alias) {
r.parameters[alias] = default_value;
}
}
}
}
}
r.matched = true;
return r;
}
}
return !_validate(meta) ? null : {
help : _help,
parse : _parse
};
}
exports.init = init;