123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948 |
- var MAX_LINE_WIDTH = process.stdout.columns || 200;
- var MIN_OFFSET = 25;
- var errorHandler;
- var commandsPath;
- var reAstral = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
- var ansiRegex = /\x1B\[([0-9]{1,3}(;[0-9]{1,3})*)?[m|K]/g;
- var hasOwnProperty = Object.prototype.hasOwnProperty;
- function stringLength(str){
- return str
- .replace(ansiRegex, '')
- .replace(reAstral, ' ')
- .length;
- }
- function camelize(name){
- return name.replace(/-(.)/g, function(m, ch){
- return ch.toUpperCase();
- });
- }
- function assign(dest, source){
- for (var key in source)
- if (hasOwnProperty.call(source, key))
- dest[key] = source[key];
- return dest;
- }
- function returnFirstArg(value){
- return value;
- }
- function pad(width, str){
- return str + Array(Math.max(0, width - stringLength(str)) + 1).join(' ');
- }
- function noop(){
- // nothing todo
- }
- function parseParams(str){
- // params [..<required>] [..[optional]]
- // <foo> - require
- // [foo] - optional
- var tmp;
- var left = str.trim();
- var result = {
- minArgsCount: 0,
- maxArgsCount: 0,
- args: []
- };
- do {
- tmp = left;
- left = left.replace(/^<([a-zA-Z][a-zA-Z0-9\-\_]*)>\s*/, function(m, name){
- result.args.push(new Argument(name, true));
- result.minArgsCount++;
- result.maxArgsCount++;
- return '';
- });
- }
- while (tmp != left);
- do {
- tmp = left;
- left = left.replace(/^\[([a-zA-Z][a-zA-Z0-9\-\_]*)\]\s*/, function(m, name){
- result.args.push(new Argument(name, false));
- result.maxArgsCount++;
- return '';
- });
- }
- while (tmp != left);
- if (left)
- throw new SyntaxError('Bad parameter description: ' + str);
- return result.args.length ? result : false;
- }
- /**
- * @class
- */
- var SyntaxError = function(message){
- this.message = message;
- };
- SyntaxError.prototype = Object.create(Error.prototype);
- SyntaxError.prototype.name = 'SyntaxError';
- SyntaxError.prototype.clap = true;
- /**
- * @class
- */
- var Argument = function(name, required){
- this.name = name;
- this.required = required;
- };
- Argument.prototype = {
- required: false,
- name: '',
- normalize: returnFirstArg,
- suggest: function(){
- return [];
- }
- };
- /**
- * @class
- * @param {string} usage
- * @param {string} description
- */
- var Option = function(usage, description){
- var self = this;
- var params;
- var left = usage.trim()
- // short usage
- // -x
- .replace(/^-([a-zA-Z])(?:\s*,\s*|\s+)/, function(m, name){
- self.short = name;
- return '';
- })
- // long usage
- // --flag
- // --no-flag - invert value if flag is boolean
- .replace(/^--([a-zA-Z][a-zA-Z0-9\-\_]+)\s*/, function(m, name){
- self.long = name;
- self.name = name.replace(/(^|-)no-/, '$1');
- self.defValue = self.name != self.long;
- return '';
- });
- if (!this.long)
- throw new SyntaxError('Usage has no long name: ' + usage);
- try {
- params = parseParams(left);
- } catch(e) {
- throw new SyntaxError('Bad paramenter description in usage for option: ' + usage, e);
- }
- if (params)
- {
- left = '';
- this.name = this.long;
- this.defValue = undefined;
- assign(this, params);
- }
- if (left)
- throw new SyntaxError('Bad usage description for option: ' + usage);
- if (!this.name)
- this.name = this.long;
- this.description = description || '';
- this.usage = usage.trim();
- this.camelName = camelize(this.name);
- };
- Option.prototype = {
- name: '',
- description: '',
- short: '',
- long: '',
- beforeInit: false,
- required: false,
- minArgsCount: 0,
- maxArgsCount: 0,
- args: null,
- defValue: undefined,
- normalize: returnFirstArg
- };
- //
- // Command
- //
- function createOption(usage, description, opt_1, opt_2){
- var option = new Option(usage, description);
- // if (option.bool && arguments.length > 2)
- // throw new SyntaxError('bool flags can\'t has default value or validator');
- if (arguments.length == 3)
- {
- if (opt_1 && opt_1.constructor === Object)
- {
- for (var key in opt_1)
- if (key == 'normalize' ||
- key == 'defValue' ||
- key == 'beforeInit')
- option[key] = opt_1[key];
- // old name for `beforeInit` setting is `hot`
- if (opt_1.hot)
- option.beforeInit = true;
- }
- else
- {
- if (typeof opt_1 == 'function')
- option.normalize = opt_1;
- else
- option.defValue = opt_1;
- }
- }
- if (arguments.length == 4)
- {
- if (typeof opt_1 == 'function')
- option.normalize = opt_1;
- option.defValue = opt_2;
- }
- return option;
- }
- function addOptionToCommand(command, option){
- var commandOption;
- // short
- if (option.short)
- {
- commandOption = command.short[option.short];
- if (commandOption)
- throw new SyntaxError('Short option name -' + option.short + ' already in use by ' + commandOption.usage + ' ' + commandOption.description);
- command.short[option.short] = option;
- }
- // long
- commandOption = command.long[option.long];
- if (commandOption)
- throw new SyntaxError('Long option --' + option.long + ' already in use by ' + commandOption.usage + ' ' + commandOption.description);
- command.long[option.long] = option;
- // camel
- commandOption = command.options[option.camelName];
- if (commandOption)
- throw new SyntaxError('Name option ' + option.camelName + ' already in use by ' + commandOption.usage + ' ' + commandOption.description);
- command.options[option.camelName] = option;
- // set default value
- if (typeof option.defValue != 'undefined')
- command.setOption(option.camelName, option.defValue, true);
- // add to suggestions
- command.suggestions.push('--' + option.long);
- return option;
- }
- function findVariants(obj, entry){
- return obj.suggestions.filter(function(item){
- return item.substr(0, entry.length) == entry;
- });
- }
- function processArgs(command, args, suggest){
- function processOption(option, command){
- var params = [];
- if (option.maxArgsCount)
- {
- for (var j = 0; j < option.maxArgsCount; j++)
- {
- var suggestPoint = suggest && i + 1 + j >= args.length - 1;
- var nextToken = args[i + 1];
- // TODO: suggestions for options
- if (suggestPoint)
- {
- // search for suggest
- noSuggestions = true;
- i = args.length;
- return;
- }
- if (!nextToken || nextToken[0] == '-')
- break;
- params.push(args[++i]);
- }
- if (params.length < option.minArgsCount)
- throw new SyntaxError('Option ' + token + ' should be used with at least ' + option.minArgsCount + ' argument(s)\nUsage: ' + option.usage);
- if (option.maxArgsCount == 1)
- params = params[0];
- }
- else
- {
- params = !option.defValue;
- }
- //command.values[option.camelName] = newValue;
- resultToken.options.push({
- option: option,
- value: params
- });
- }
- var resultToken = {
- command: command,
- args: [],
- literalArgs: [],
- options: []
- };
- var result = [resultToken];
- var suggestStartsWith = '';
- var noSuggestions = false;
- var collectArgs = false;
- var commandArgs = [];
- var noOptionsYet = true;
- var option;
- commandsPath = [command.name];
- for (var i = 0; i < args.length; i++)
- {
- var suggestPoint = suggest && i == args.length - 1;
- var token = args[i];
- if (collectArgs)
- {
- commandArgs.push(token);
- continue;
- }
- if (suggestPoint && (token == '--' || token == '-' || token[0] != '-'))
- {
- suggestStartsWith = token;
- break; // returns long option & command list outside the loop
- }
- if (token == '--')
- {
- noOptionsYet = false;
- collectArgs = true;
- continue;
- }
- if (token[0] == '-')
- {
- noOptionsYet = false;
- if (commandArgs.length)
- {
- //command.args_.apply(command, commandArgs);
- resultToken.args = commandArgs;
- commandArgs = [];
- }
- if (token[1] == '-')
- {
- // long option
- option = command.long[token.substr(2)];
- if (!option)
- {
- // option doesn't exist
- if (suggestPoint)
- return findVariants(command, token);
- else
- throw new SyntaxError('Unknown option: ' + token);
- }
- // process option
- processOption(option, command);
- }
- else
- {
- // short flags sequence
- if (!/^-[a-zA-Z]+$/.test(token))
- throw new SyntaxError('Wrong short option sequence: ' + token);
- if (token.length == 2)
- {
- option = command.short[token[1]];
- if (!option)
- throw new SyntaxError('Unknown short option name: -' + token[1]);
- // single option
- processOption(option, command);
- }
- else
- {
- // short options sequence
- for (var j = 1; j < token.length; j++)
- {
- option = command.short[token[j]];
- if (!option)
- throw new SyntaxError('Unknown short option name: -' + token[j]);
- if (option.maxArgsCount)
- throw new SyntaxError('Non-boolean option -' + token[j] + ' can\'t be used in short option sequence: ' + token);
- processOption(option, command);
- }
- }
- }
- }
- else
- {
- if (command.commands[token] && (!command.params || commandArgs.length >= command.params.minArgsCount))
- {
- if (noOptionsYet)
- {
- resultToken.args = commandArgs;
- commandArgs = [];
- }
- if (command.params && resultToken.args.length < command.params.minArgsCount)
- throw new SyntaxError('Missed required argument(s) for command `' + command.name + '`');
- // switch control to another command
- command = command.commands[token];
- noOptionsYet = true;
- commandsPath.push(command.name);
- resultToken = {
- command: command,
- args: [],
- literalArgs: [],
- options: []
- };
- result.push(resultToken);
- }
- else
- {
- if (noOptionsYet && command.params && commandArgs.length < command.params.maxArgsCount)
- {
- commandArgs.push(token);
- continue;
- }
- if (suggestPoint)
- return findVariants(command, token);
- else
- throw new SyntaxError('Unknown command: ' + token);
- }
- }
- }
- if (suggest)
- {
- if (collectArgs || noSuggestions)
- return [];
- return findVariants(command, suggestStartsWith);
- }
- else
- {
- if (!noOptionsYet)
- resultToken.literalArgs = commandArgs;
- else
- resultToken.args = commandArgs;
- if (command.params && resultToken.args.length < command.params.minArgsCount)
- throw new SyntaxError('Missed required argument(s) for command `' + command.name + '`');
- }
- return result;
- }
- function setFunctionFactory(name){
- return function(fn){
- var property = name + '_';
- if (this[property] !== noop)
- throw new SyntaxError('Method `' + name + '` could be invoked only once');
- if (typeof fn != 'function')
- throw new SyntaxError('Value for `' + name + '` method should be a function');
- this[property] = fn;
- return this;
- }
- }
- /**
- * @class
- */
- var Command = function(name, params){
- this.name = name;
- this.params = false;
- try {
- if (params)
- this.params = parseParams(params);
- } catch(e) {
- throw new SyntaxError('Bad paramenter description in command definition: ' + this.name + ' ' + params);
- }
- this.commands = {};
- this.options = {};
- this.short = {};
- this.long = {};
- this.values = {};
- this.defaults_ = {};
- this.suggestions = [];
- this.option('-h, --help', 'Output usage information', function(){
- this.showHelp();
- process.exit(0);
- }, undefined);
- };
- Command.prototype = {
- params: null,
- commands: null,
- options: null,
- short: null,
- long: null,
- values: null,
- defaults_: null,
- suggestions: null,
- description_: '',
- version_: '',
- initContext_: noop,
- init_: noop,
- delegate_: noop,
- action_: noop,
- args_: noop,
- end_: null,
- option: function(usage, description, opt_1, opt_2){
- addOptionToCommand(this, createOption.apply(null, arguments));
- return this;
- },
- shortcut: function(usage, description, fn, opt_1, opt_2){
- if (typeof fn != 'function')
- throw new SyntaxError('fn should be a function');
- var command = this;
- var option = addOptionToCommand(this, createOption(usage, description, opt_1, opt_2));
- var normalize = option.normalize;
- option.normalize = function(value){
- var values;
- value = normalize.call(command, value);
- values = fn(value);
- for (var name in values)
- if (hasOwnProperty.call(values, name))
- if (hasOwnProperty.call(command.options, name))
- command.setOption(name, values[name]);
- else
- command.values[name] = values[name];
- command.values[option.name] = value;
- return value;
- };
- return this;
- },
- hasOption: function(name){
- return hasOwnProperty.call(this.options, name);
- },
- hasOptions: function(){
- return Object.keys(this.options).length > 0;
- },
- setOption: function(name, value, isDefault){
- if (!this.hasOption(name))
- throw new SyntaxError('Option `' + name + '` is not defined');
- var option = this.options[name];
- var oldValue = this.values[name];
- var newValue = option.normalize.call(this, value, oldValue);
- this.values[name] = option.maxArgsCount ? newValue : value;
- if (isDefault && !hasOwnProperty.call(this.defaults_, name))
- this.defaults_[name] = this.values[name];
- },
- setOptions: function(values){
- for (var name in values)
- if (hasOwnProperty.call(values, name) && this.hasOption(name))
- this.setOption(name, values[name]);
- },
- reset: function(){
- this.values = {};
- assign(this.values, this.defaults_);
- },
- command: function(nameOrCommand, params){
- var name;
- var command;
- if (nameOrCommand instanceof Command)
- {
- command = nameOrCommand;
- name = command.name;
- }
- else
- {
- name = nameOrCommand;
- if (!/^[a-zA-Z][a-zA-Z0-9\-\_]*$/.test(name))
- throw new SyntaxError('Wrong command name: ' + name);
- }
- // search for existing one
- var subcommand = this.commands[name];
- if (!subcommand)
- {
- // create new one if not exists
- subcommand = command || new Command(name, params);
- subcommand.end_ = this;
- this.commands[name] = subcommand;
- this.suggestions.push(name);
- }
- return subcommand;
- },
- end: function() {
- return this.end_;
- },
- hasCommands: function(){
- return Object.keys(this.commands).length > 0;
- },
- version: function(version, usage, description){
- if (this.version_)
- throw new SyntaxError('Version for command could be set only once');
- this.version_ = version;
- this.option(
- usage || '-v, --version',
- description || 'Output version',
- function(){
- console.log(this.version_);
- process.exit(0);
- },
- undefined
- );
- return this;
- },
- description: function(description){
- if (this.description_)
- throw new SyntaxError('Description for command could be set only once');
- this.description_ = description;
- return this;
- },
- init: setFunctionFactory('init'),
- initContext: setFunctionFactory('initContext'),
- args: setFunctionFactory('args'),
- delegate: setFunctionFactory('delegate'),
- action: setFunctionFactory('action'),
- extend: function(fn){
- fn.apply(null, [this].concat(Array.prototype.slice.call(arguments, 1)));
- return this;
- },
- parse: function(args, suggest){
- if (!args)
- args = process.argv.slice(2);
- if (!errorHandler)
- return processArgs(this, args, suggest);
- else
- try {
- return processArgs(this, args, suggest);
- } catch(e) {
- errorHandler(e.message || e);
- }
- },
- run: function(args, context){
- var commands = this.parse(args);
- if (!commands)
- return;
- var prevCommand;
- var context = assign({}, context || this.initContext_());
- for (var i = 0; i < commands.length; i++)
- {
- var item = commands[i];
- var command = item.command;
- // reset command values
- command.reset();
- command.context = context;
- command.root = this;
- if (prevCommand)
- prevCommand.delegate_(command);
- // apply beforeInit options
- item.options.forEach(function(entry){
- if (entry.option.beforeInit)
- command.setOption(entry.option.camelName, entry.value);
- });
- command.init_(item.args);
- if (item.args.length)
- command.args_(item.args);
- // apply regular options
- item.options.forEach(function(entry){
- if (!entry.option.beforeInit)
- command.setOption(entry.option.camelName, entry.value);
- });
- prevCommand = command;
- }
- // return last command action result
- if (command)
- return command.action_(item.args, item.literalArgs);
- },
- normalize: function(values){
- var result = {};
- if (!values)
- values = {};
- for (var name in this.values)
- if (hasOwnProperty.call(this.values, name))
- result[name] = hasOwnProperty.call(values, name) && hasOwnProperty.call(this.options, name)
- ? this.options[name].normalize.call(this, values[name])
- : this.values[name];
- for (var name in values)
- if (hasOwnProperty.call(values, name) && !hasOwnProperty.call(result, name))
- result[name] = values[name];
- return result;
- },
- showHelp: function(){
- console.log(showCommandHelp(this));
- }
- };
- //
- // help
- //
- /**
- * Return program help documentation.
- *
- * @return {String}
- * @api private
- */
- function showCommandHelp(command){
- function breakByLines(str, offset){
- var words = str.split(' ');
- var maxWidth = MAX_LINE_WIDTH - offset || 0;
- var lines = [];
- var line = '';
- while (words.length)
- {
- var word = words.shift();
- if (!line || (line.length + word.length + 1) < maxWidth)
- {
- line += (line ? ' ' : '') + word;
- }
- else
- {
- lines.push(line);
- words.unshift(word);
- line = '';
- }
- }
- lines.push(line);
- return lines.map(function(line, idx){
- return (idx && offset ? pad(offset, '') : '') + line;
- }).join('\n');
- }
- function args(command){
- return command.params.args.map(function(arg){
- return arg.required
- ? '<' + arg.name + '>'
- : '[' + arg.name + ']';
- }).join(' ');
- }
- function commandsHelp(){
- if (!command.hasCommands())
- return '';
- var maxNameLength = MIN_OFFSET - 2;
- var lines = Object.keys(command.commands).sort().map(function(name){
- var subcommand = command.commands[name];
- var line = {
- name: chalk.green(name) + chalk.gray(
- (subcommand.params ? ' ' + args(subcommand) : '')
- // (subcommand.hasOptions() ? ' [options]' : '')
- ),
- description: subcommand.description_ || ''
- };
- maxNameLength = Math.max(maxNameLength, stringLength(line.name));
- return line;
- });
- return [
- '',
- 'Commands:',
- '',
- lines.map(function(line){
- return ' ' + pad(maxNameLength, line.name) + ' ' + breakByLines(line.description, maxNameLength + 4);
- }).join('\n'),
- ''
- ].join('\n');
- }
- function optionsHelp(){
- if (!command.hasOptions())
- return '';
- var hasShortOptions = Object.keys(command.short).length > 0;
- var maxNameLength = MIN_OFFSET - 2;
- var lines = Object.keys(command.long).sort().map(function(name){
- var option = command.long[name];
- var line = {
- name: option.usage
- .replace(/^(?:-., |)/, function(m){
- return m || (hasShortOptions ? ' ' : '');
- })
- .replace(/(^|\s)(-[^\s,]+)/ig, function(m, p, flag){
- return p + chalk.yellow(flag);
- }),
- description: option.description
- };
- maxNameLength = Math.max(maxNameLength, stringLength(line.name));
- return line;
- });
- // Prepend the help information
- return [
- '',
- 'Options:',
- '',
- lines.map(function(line){
- return ' ' + pad(maxNameLength, line.name) + ' ' + breakByLines(line.description, maxNameLength + 4);
- }).join('\n'),
- ''
- ].join('\n');
- }
- var output = [];
- var chalk = require('chalk');
- chalk.enabled = module.exports.color && process.stdout.isTTY;
- if (command.description_)
- output.push(command.description_ + '\n');
- output.push(
- 'Usage:\n\n ' +
- chalk.cyan(commandsPath ? commandsPath.join(' ') : command.name) +
- (command.params ? ' ' + chalk.magenta(args(command)) : '') +
- (command.hasOptions() ? ' [' + chalk.yellow('options') + ']' : '') +
- (command.hasCommands() ? ' [' + chalk.green('command') + ']' : ''),
- commandsHelp() +
- optionsHelp()
- );
- return output.join('\n');
- };
- //
- // export
- //
- module.exports = {
- color: true,
- Error: SyntaxError,
- Argument: Argument,
- Command: Command,
- Option: Option,
- error: function(fn){
- if (errorHandler)
- throw new SyntaxError('Error handler should be set only once');
- if (typeof fn != 'function')
- throw new SyntaxError('Error handler should be a function');
- errorHandler = fn;
- return this;
- },
- create: function(name, params){
- return new Command(name || require('path').basename(process.argv[1]) || 'cli', params);
- },
- confirm: function(message, fn){
- process.stdout.write(message);
- process.stdin.setEncoding('utf8');
- process.stdin.once('data', function(val){
- process.stdin.pause();
- fn(/^y|yes|ok|true$/i.test(val.trim()));
- });
- process.stdin.resume();
- }
- };
|