UNPKG

cmdparser

Version:
465 lines (420 loc) 11.5 kB
'use strict'; var async = require('async'); var COMPLETING_ERROR = 'completeing'; var CmdParser = module.exports = function (commands, completers) { this._commands = commands.map(parseCommand); this._completers = completers; }; CmdParser.prototype.parse = function (str, callback) { var matches = []; async.each(this._commands, function (cmd, cb) { cmd.parse(str, function (err, r) { if (r) { matches.push(r); } cb(); }); }, function (err) { if (err) { return callback(err); } if (matches.length === 0) { return callback(null); } if (matches.length === 1) { return callback(null, matches[0]); } return callback(new Error('Multiple matches [' + JSON.stringify(matches) + '] for string "' + str + '".')); }); }; CmdParser.prototype.completer = function (str, callback) { var self = this; var matches = []; async.each(this._commands, function (cmd, cb) { cmd.completer(str, self._completers, function (err, r) { if (r) { matches.push(r); } cb(); }); }, function (err) { if (err) { return callback(err); } if (matches.length === 0) { return callback(null); } var bestPartial = matches[0].partial; // todo find a better way var results = [ [], bestPartial ]; matches.forEach(function (m) { if (m.partial.toLowerCase() === bestPartial.toLowerCase()) { if (m.value instanceof Array) { results[0] = results[0].concat(m.value); } else { results[0].push(m.value); } } }); callback(null, results); }); }; function parseCommand(cmd) { var debug = false; var m = cmd.match(/^(.*?)\s(.*)/); var commandName; var parts = []; var paramName; var i; if (m) { commandName = m[1]; parts.push({ op: 'commandName', name: commandName }); var cmdParameters = m[2]; for (i = 0; i < cmdParameters.length;) { if (cmdParameters[i] === '[') { i++; paramName = ''; while (cmdParameters[i] !== ']') { paramName += cmdParameters[i++]; } i++; var subParts = paramName.split(' '); var repeat = false; if (subParts[subParts.length - 1] === '...') { repeat = true; subParts.splice(subParts.length - 1); } subParts = subParts.map(function (name) { if (name.toUpperCase() === name) { return { op: 'literal', names: name.split('|') }; } return { op: 'requiredParameter', name: name }; }); parts.push({ repeat: repeat, op: 'optionalParameters', parts: subParts }); continue; } if (isWhitespace(cmdParameters[i])) { i++; continue; } paramName = ''; while (i < cmdParameters.length && !isWhitespace(cmdParameters[i])) { paramName += cmdParameters[i++]; } if (paramName.toUpperCase() === paramName) { parts.push({ op: 'literal', names: paramName.split('|') }); } else { parts.push({ op: 'requiredParameter', name: paramName }); } } } else { commandName = cmd; parts.push({ op: 'commandName', name: commandName }); } for (i = 0; i < parts.length; i++) { if (parts[i].op === 'optionalParameters' && parts[i].parts[0].op === 'literal') { var startOrPartIdx = i; var endOrPartIdx = i; while (endOrPartIdx < parts.length && parts[endOrPartIdx].op === 'optionalParameters' && parts[endOrPartIdx].parts[0].op === 'literal') { endOrPartIdx++; } if (startOrPartIdx !== endOrPartIdx - 1) { var orPart = { op: 'optionalParameterLiteralOr', parts: parts.slice(startOrPartIdx, endOrPartIdx) }; parts.splice(startOrPartIdx, endOrPartIdx - startOrPartIdx, orPart); i = startOrPartIdx - 1; } } } function doParse(str, callback) { var state = { parsing: true, strIdx: 0, startStrIdx: 0, str: str, cmd: cmd, debug: debug, result: { name: null, params: {} } }; parseAll(state, parts, callback); } function doCompleter(str, completers, callback) { var state = { completing: true, completers: completers, strIdx: 0, startStrIdx: 0, str: str, cmd: cmd, debug: debug, result: { name: null, params: {} } }; parseAll(state, parts, function (err) { callback(err, state.completer); }); } return { parse: doParse, completer: doCompleter }; } function isWhitespace(ch) { return /\s/.test(ch) } function skipWhitespace(state) { var startStrIdx = state.strIdx; while (state.strIdx < state.str.length && isWhitespace(state.str[state.strIdx])) { state.strIdx++; } return state.strIdx !== startStrIdx; } function readNextWord(state) { var word = ''; if (state.str[state.strIdx] === '"') { state.strIdx++; while (state.strIdx < state.str.length && state.str[state.strIdx] !== '"') { if (state.str[state.strIdx] === '\\') { state.strIdx++; word += state.str[state.strIdx++]; } else { word += state.str[state.strIdx++]; } } state.strIdx++; } else { while (state.strIdx < state.str.length && !isWhitespace(state.str[state.strIdx])) { word += state.str[state.strIdx++]; } } return word; } function peekNextWord(state) { var saveStrIdx = state.strIdx; var word = readNextWord(state); state.strIdx = saveStrIdx; return word; } function isEndOfString(state) { return state.str.length === state.strIdx; } function parseAll(state, parts, callback) { if (state.debug) { console.log('parts', state.cmd, JSON.stringify(parts, null, ' ')); } async.eachSeries(parts, function (part, callback) { if (state.completer) { return callback(); } if (part.op === 'commandName') { return parseCommandName(state, part, callback); } if (part.op === 'requiredParameter') { return parseRequiredParameter(state, part, callback); } if (part.op === 'optionalParameters') { return parseOptionalParameters(state, part, callback); } if (part.op === 'literal') { return parseLiteral(state, part, callback); } if (part.op === 'optionalParameterLiteralOr') { return parseOptionalParameterLiteralOr(state, part, callback); } return callback(new Error("could not parse: " + JSON.stringify(state))); }, function (err) { if (err) { return callback(err); } if (state.debug) { console.log('parseAll ok', JSON.stringify(state, null, ' ')); } return callback(null, state.result); }); } function parseRequiredParameter(state, part, callback) { if (state.debug) { console.log('parseRequiredParameter', state, part); } var val = readNextWord(state); if (val.length === 0 && !state.completing) { return callback(null, false); } state.result.params[part.name] = val; var endsInSpace = skipWhitespace(state); if (!endsInSpace && state.completers && state.completers[part.name]) { state.completers[part.name](val, function (err, values) { if (err) { return callback(err); } state.completer = { partial: val, value: values }; return callback(null, true); }); } else { return callback(null, true); } } function parseCommandName(state, part, callback) { if (state.debug) { console.log('parseCommandName', state, part); } var word = readNextWord(state); if (word.toLowerCase() !== part.name.toLowerCase()) { if (part.name.toLowerCase().indexOf(word.toLowerCase()) === 0 && isEndOfString(state)) { state.completer = { partial: word, value: part.name + ' ' }; } if (state.parsing) { return callback(new Error("Command name does not match. expected: " + part.name + ", found: " + word)); } else { return callback(COMPLETING_ERROR); } } state.result.name = part.name; skipWhitespace(state); return callback(); } function parseOptionalParameters(state, part, callback) { if (state.debug) { console.log('parseOptionalParameters', state, part); } if (part.repeat) { var saveResults = state.result; state.result = {params: {}}; async.whilst( function (cbt) { cbt(null, !isEndOfString(state)); }, function (cb) { parseAll(state, part.parts, function (err) { if (state.debug) { console.log('repeat', state); } mergeResultsAsArrays(saveResults, state.result); cb(); }); }, function (err) { state.result = saveResults; if (err) { return callback(err); } callback(); } ); } else { parseAll(state, part.parts, callback); } } function parseLiteral(state, part, callback) { if (state.debug) { console.log('parseLiteral', state, part); } var i; for (i = 0; i < part.names.length; i++) { state.result.params[part.names[i]] = false; } var word = readNextWord(state); if (!word) { state.completer = { partial: word, value: part.names }; part.names.forEach(function (name) { state.result.params[name] = false; }); return callback(new Error("No values left in string")); } for (i = 0; i < part.names.length; i++) { if (word.toLowerCase() === part.names[i].toLowerCase()) { state.result.params[part.names[i]] = true; skipWhitespace(state); return callback(); } } if (isEndOfString(state)) { var matches = []; for (i = 0; i < part.names.length; i++) { if (part.names[i].toLowerCase().indexOf(word.toLowerCase()) === 0) { matches.push(part.names[i]); } } if (matches.length > 0) { state.completer = { partial: word, value: matches }; } } if (state.parsing) { return callback(new Error("Partial match")); } else { return callback(COMPLETING_ERROR); } } function parseOptionalParameterLiteralOr(state, part, callback) { if (state.debug) { console.log('parseOptionalParameterLiteralOr', state, part); } var word = peekNextWord(state); var match = null; for (var i = 0; i < part.parts.length; i++) { for (var n = 0; n < part.parts[i].parts[0].names.length; n++) { var literalValue = part.parts[i].parts[0].names[n]; if (literalValue.toLowerCase() === word.toLowerCase()) { match = part.parts[i]; } else { state.result.params[literalValue] = false; } } } if (match) { if (state.debug) { console.log('parseOptionalParameterLiteralOr match', part.parts[i]); } return parseAll(state, match.parts, callback) } else { return callback(new Error("No matches found")); } } function mergeResultsAsArrays(dest, src) { for (var key in src.params) { var val = src.params[key]; if (dest.params[key] instanceof Array) { dest.params[key].push(val); } else if (dest.params[key]) { dest.params[key] = [dest.params[key], val]; } else { dest.params[key] = [val]; } } }