UNPKG

concierge-bot

Version:

Extensible general purpose chat bot.

191 lines (183 loc) 6.39 kB
/** * A generic, multipurpose CLI arguments parser. * * Written By: * Matthew Knox * * License: * MIT License. All code unless otherwise specified is * Copyright (c) Matthew Knox and Contributors 2017. */ class OutputBuffer { constructor(consoleOutput) { this.output = ''; this.consoleOutput = consoleOutput; } write(data) { this.output += data; if (this.consoleOutput) { process.stdout.write(data); } } log(data) { this.write(data + '\n'); } clear() { this.output = ''; if (this.consoleOutput) { process.stdout.write('\u001b[2J\u001b[0;0H'); } } toString() { return this.output; } } const verifyUnique = (options) => { const uniqueTest = {}; for (let arg of options) { if (uniqueTest.hasOwnProperty(arg.long) || uniqueTest.hasOwnProperty(arg.short)) { throw new Error(`Options should not overlap (check "${uniqueTest.hasOwnProperty(arg.long) ? arg.long : arg.short}").`); } uniqueTest[arg.long] = uniqueTest[arg.short] = true; } }; const generateHelp = (options, config) => { const colourise = (str, col) => { return config.colours ? str[col] : str; }; options.push({ long: '--help', short: '-h', description: 'Shows this help.', run: (out) => { let result = 'USAGE\n\t' + colourise(config.string, 'red') + ' ' + colourise('<options...>', 'cyan') + '\nOPTIONS\n'; for (let i = 0; i < options.length; i++) { let infoStr = '\t' + colourise(options[i].short + ', ' + options[i].long, 'cyan'); if (options[i].expects) { infoStr += ' '; for (let j = 0; j < options[i].expects.length; j++) { infoStr += '{' + colourise(options[i].expects[j], 'yellow') + '} '; } } result += infoStr + '\n\t\t' + options[i].description + '\n'; } out.clear(); out.log(result); return true; } }); }; /** * parseArguments - Parses arguments contained within an array. * * @param {Array<string>} args Input arguments from which to extract meaning. * @param {Array<Object>} options Array of option objects to parse. See example. * @param {Object} help An object determining if a help option (-h/--help) should be added to the options, and if so what its options are. See method prototype for options. * @param {boolean} consoleOutput For arguments that output data, determines if that data should be forwarded to the console. * @param {boolean} ignoreError When parsing, determines if errors should be ignored (by default this is turned off). * @return {Object} An object representing the parsed arguments. General structure is as follows: {parsed:{'-i':{vals:['en'],output:''}}, unassociated:['foo','bar']} * * @example * ```js * { * long: '--example', * short: '-e', * description: 'An example option that calls a function with its associated arguments "FILE" and "FOLDER"', * expects: ['FILE', 'FOLDER'], * defaults: ['default_folder'], * run: (out, vals) => { * // vals[0] === file * } * } * ``` * @example * ```js * { * long: '--example2', * short: '-e2', * description: 'A second example option that takes no arguments and calls no method' * } * ``` */ exports.parseArguments = (args, options, help = {enabled:false, string:null, colours:true}, consoleOutput = false, ignoreError = false) => { if (help && help.enabled) { generateHelp(options, help); } if (!ignoreError) { verifyUnique(options); } args = args.slice(); const parsed = { parsed: {}, unassociated: null }; for (let i = 0; i < args.length; i++) { const arg = args[i], pargs = options.filter(value => value.short === arg || value.long === arg); if (pargs.length === 0) { continue; } else if (pargs.length > 1 && !ignoreError) { throw new Error('Invalid Arguments'); } let count = (pargs[0].expects || []).length; const def = (pargs[0].defaults || []).length; const vals = Array(count - def).concat(pargs[0].defaults || []); for (let j = 1; j <= count; j++) { const nexta = args[i + j]; if (nexta && !options.some(value => value.short === nexta || value.long === nexta)) { vals[j - 1] = args[i + j]; } else if (!vals.includes(void(0)) || ignoreError) { count = j - 1; break; } else { throw new Error(`Too few arguments given to "${arg}"`); } } const out = new OutputBuffer(consoleOutput), p = { vals: vals, out: null }; let res; try { res = pargs[0].run ? pargs[0].run(out, vals) : false; p.out = out.toString(); } catch (e) { // if there is a default, execute that instead if (pargs[0].defaults && pargs[0].run) { for (let j = pargs[0].defaults.length - 1, k = vals.length - 1; j >= 0; j--, k--) { vals[k] = pargs[0].defaults[j]; count--; } out.clear(); res = pargs[0].run(out, vals); p.out = out.toString(); } else { throw e; } } if (parsed.parsed[pargs[0].short]) { let next = parsed.parsed[pargs[0].short]; while (next.next) { next = next.next; } next.next = p; } else { parsed.parsed[pargs[0].short] = p; } const diff = 1 + count; args.splice(i, diff); i -= diff; if (res) { break; } } parsed.unassociated = args; return parsed; };