UNPKG

chedder

Version:
545 lines (540 loc) 20.6 kB
"use strict"; /** #Simple and minimum command line args parser * ##Functionalities: * * Rebuilds JSON object from several CL args. * --Note: spaces *must* be used around { and } * ##Separates positional arguments from --options ------------------------------- ``` >myContract transfer { account_id:luciotato, dest:other.account.betanet, stake:false } --amount 100N result: positional: [ { account_id:"luciotato", dest:"other.account.betanet", stake:false, } ] options: [ "amount" : "100_000_000_000_000_000_000_000_000" ] ``` ----------------------------- ## Planned functionalities: ### parse [ ] */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommandLineArgs = exports.ShowHelpOptions = void 0; const path_1 = require("path"); // host OS path separator const util_1 = require("util"); const color = __importStar(require("./color.js")); // ---------------------------------------------------- // construct and show help page based on valid options // ---------------------------------------------------- function ShowHelpOptions(optionsDeclaration) { // show help about declared options console.log(); console.log("-".repeat(60)); console.log("Options:"); for (const key in optionsDeclaration) { let line = ""; const opt = optionsDeclaration[key]; let text = "--" + key; if (opt.valueType) text = text + " " + opt.valueType; if (opt.shortName) { text = text + ", -" + opt.shortName; if (opt.valueType) text = text + " " + opt.valueType; } line = ` ${text}`.padEnd(50) + (opt.helpText ? opt.helpText : ""); console.log(line); } console.log("-".repeat(60)); } exports.ShowHelpOptions = ShowHelpOptions; // -------------------------- // -- main exported class -- // -------------------------- class CommandLineArgs { constructor(options) { this.clArgs = process.argv; this.optDeclarations = options; this.positional = []; // remove 'node' if called as a node script if (this.clArgs.length && (this.clArgs[0] === 'node' || this.clArgs[0].endsWith(path_1.sep + 'node')) || this.clArgs[0].endsWith(path_1.sep + 'node.exe')) { this.clArgs = this.clArgs.slice(1); } // remove this script/executable name from command line arguments this.clArgs = this.clArgs.slice(1); // process each item separating options from positional args // First: process --options for (const key in options) { const optionDecl = options[key]; // search for option name & variations const pos = this.searchOption(optionDecl); if (pos >= 0) { // found in command line args const literal = this.clArgs[pos]; // as written this.clArgs.splice(pos, 1); // remove from cl args if (optionDecl.valueType) { // has a value if (pos >= this.clArgs.length) { color.logErr("expecting value after " + literal); process.exit(1); } const value = this.clArgs[pos]; // take value options[key].value = value; // set value this.clArgs.splice(pos, 1); // also remove value from list } else // valueless option { options[key].value = true; // set as present } } } // if at this point there are still --options in the command line args array, those are unknown options let hasErrors = false; for (const item of this.clArgs) { if (item.startsWith("-")) { color.logErr("UNKNOWN option: " + item); hasErrors = true; } } if (hasErrors) { ShowHelpOptions(options); process.exit(1); } // create consumable positional arguments, parsing also JSON command-line format for (let index = 0; index < this.clArgs.length; index++) { const item = this.clArgs[index]; if (item == "{") { // a JSON object in the command line const extracted = this.extractJSONObject(index); this.positional.push(extracted.value); index = extracted.end; } else { this.positional.push(item); } } } /** * When the first argument is the command to execute * returns "" if there's no arguments */ getCommand() { if (this.positional.length > 0 && typeof this.positional[0] !== "string") { color.logErr("expected a command as first argument'"); process.exit(1); } else { if (this.positional.length === 0) return ""; // take the first argument as this.command return this.positional.shift(); } } /** * consume one string from the positional args * if it matches the expected string * returns false if the next arg doesn't match * @param which which string is expected */ optionalString(which) { if (this.positional.length == 0) return false; if (typeof this.positional[0] !== "string") { color.logErr(`expected a string argument, got {... }`); process.exit(1); } if (this.positional[0] == which) { this.positional.shift(); // consume return true; } return false; // not the expected string } /** * requires a string as the next positional argument * @param name */ consumeString(name) { if (this.positional.length == 0) { color.logErr(`expected '${name}' argument`); process.exit(1); } if (typeof this.positional[0] !== "string") { color.logErr(`expected ${name} string argument, got {... }`); process.exit(1); } return this.positional.shift(); } /** * requires an amount in NEAR or YOCTO as the next positional argument * @param name */ consumeAmount(name, units) { const value = this.consumeString(name); return this.convertAmount(value, units, name); } /** * requires a JSON as the next positional arg * @param name */ // eslint-disable-next-line @typescript-eslint/no-explicit-any consumeJSON(name) { if (this.positional.length == 0) { color.logErr(`expected ${name} as { }`); process.exit(1); } if (typeof this.positional[0] === "string") { color.logErr(`expected ${name} as {... } got a string: '${this.positional[0]}'`); process.exit(1); } return this.positional.shift(); } moreArgs() { return this.positional.length > 0; } /** * marks the end of the required arguments * if there are more arguments => error */ noMoreArgs() { if (this.positional.length) { color.logErr(`unrecognized extra arguments`); console.log(util_1.inspect(this.positional)); process.exit(1); } } findDeclarationKey(opt) { for (const key in this.optDeclarations) { if (opt.shortName && this.optDeclarations[key].shortName == opt.shortName) return key; if (opt.helpText && this.optDeclarations[key].helpText == opt.helpText) return key; } throw new Error("shortName|helpText not found in declarations: " + util_1.inspect(opt)); } /** * requires the presence of an option with a string value * @param optionName option name */ requireOptionString(opt) { if (opt.value == undefined || opt.value == "") { const key = this.findDeclarationKey(opt); color.logErr(`required --${key}`); process.exit(1); } } /** * requires the presence of an option with an amount * @param optionName option name */ requireOptionWithAmount(opt, units) { const value = opt.value ? opt.value.toString().trim() : ""; const key = this.findDeclarationKey(opt); if (!value) { color.logErr(`required --${key} [number]`); process.exit(1); } const converted = this.convertAmount(value, units, key); opt.value = converted; // store in the required units } /** * search for the presence of an option * removes it from the options if found * * @param optionName option name */ consumeOption(opt) { const value = opt.value; if (value) { // found opt.value = undefined; // remove from options (consume) } return value; } /** * converts an argument from the command line into a numeric string expressed in the required units * example: * convertAmount("10N","N") => "10" * convertAmount("1.25N","Y") => "12500000000000000000000000" * convertAmount("1365465465464564654654Y","N") => "0.00000000001365465465464564654654" * convertAmount("100_000_000Y","Y") => "100000000" * * @param value string as read from the command line * @param requiredUnits N|Y unit in which the amount is required */ convertAmount(value, requiredUnits, name) { let result = value.toUpperCase(); name = color.yellow + name + color.normal; result = result.replace("_", ""); // allow 100_000_000, ignore _ if (result.endsWith("Y")) { // value ends in YOCTOS if (result.includes(".")) { color.logErr(name + ": invalid amount format, YOCTOS can't have decimals: " + value); process.exit(1); } result = result.slice(0, -1); // remove Y if (requiredUnits == "Y") { return result; } // already in Yoctos if (requiredUnits == "I" || requiredUnits == "F") { return result; } // NEARS required -- convert to NEARS if (result.length <= 24) { result = "0." + result.padStart(24, '0').slice(-24); } else { // insert decimal point at 1e24 result = result.slice(0, result.length - 24) + "." + result.slice(-24); } return result; } else { // other, assume amount in NEARS (default) if (!result.slice(-1).match(/\d|N|I|F/)) { //should end with N|I|F or a digit color.logErr(name + ": invalid denominator, expected Y|N|I|F => yoctos|near|int|float. Received:" + result); process.exit(1); } if (result.endsWith("I") || result.endsWith("F")) { result = result.slice(0, -1); // remove denom, store as number return result; } if (result.endsWith("N")) result = result.slice(0, -1); // remove N if (requiredUnits == "N") { return result; } // already in Nears // Yoctos required -- convert to yoctos const parts = result.split("."); if (parts.length > 2) { color.logErr(name + ": invalid amount format, too many decimal points: " + value); process.exit(1); } if (parts.length == 1) { parts.push(""); } // .0 const decimalString = parts[1].padEnd(24, '0'); result = parts[0] + "" + decimalString; // +""+ is for making sure + means concat here return result; } } /** * extract { a: b, d:100 } from the command line as a JSON object * @param start open brace position in this.list */ extractJSONObject(start) { // find the closing "}" let opened = 1; let end = -1; for (let n = start + 1; n < this.clArgs.length; n++) { const item = this.clArgs[n]; if (item == "{") { opened++; } else if (item == "}") { opened--; if (opened == 0) { end = n; break; } } } if (end == -1) { // unmatched opener error color.logErr("Unmatched '{' . remember to put spaces around { and }"); this.clArgs[start] = color.yellow + "{" + color.normal; console.log(this.clArgs.join(" ")); process.exit(1); } // Here we have start & end for matching { } const resultObj = {}; for (let index = start + 1; index < end; index++) { let propName = this.clArgs[index]; let propValue; if (propName == ",") continue; if ("{}".includes(propName)) { color.logErr("expected name:value"); this.clArgs[index] = color.yellow + propName + color.normal; console.log(this.clArgs.slice(start, end + 1).join(" ")); process.exit(1); } const parts = propName.split(":"); if (parts.length > 2) { color.logErr(` too many ':' (found ${parts.length - 1}) at ${propName}`); process.exit(1); } propName = parts[0].trim(); propValue = parts[1].trim(); if (propValue == undefined || propValue == "") { // let's assume the user typed "name: value" instead of "name:value" index++; // take the next arg propValue = this.clArgs[index]; if (propValue.endsWith(":")) { color.logErr(` missing value after ':' for ${propName}`); } if (index >= end || propValue == "}") { console.log(`ERROR: expected value after ${propName}`); process.exit(1); } } if (propValue == "{") { // subordinated object const subObj = this.extractJSONObject(index); // recursive*** // store as object resultObj[propName] = subObj.value; index = subObj.end; // skip internal object continue; } // it's a string // remove ending "," if it's there if (propValue.endsWith(",")) propValue = propValue.slice(0, propValue.length - 1); // check if it's a number if (propValue.toUpperCase().match(/^[0-9.]+[Y|N|I|F]{0,1}$/)) { // amount (optionally [Y|N|I|F] expressed in nears. yoctos, integer or float propValue = this.convertAmount(propValue, "Y", propName); // process and convert to Yoctos if expressed in nears } // store resultObj[propName] = propValue; } // end for // return positions and composed object return { start: start, end: end, value: resultObj }; } // --------------------------- /** * removes valueless options into the options object * returns true if the option was present * @param shortName short name, e.g -verb * @param fullName full name,e.g. --verbose */ /* option(shortName: string, fullName: string) { //if .getPos(shortOption,argName) into var pos >= 0 var pos = this.removeOption(shortName, fullName); if (pos >= 0) { this.positional.splice(pos, 1); return true; }; return false; } */ // --------------------------- /** * removes options that has a value after it * @param shortName short name, e.g -ata 100N * @param fullName full name,e.g. --attach 100N */ /* valueFor(shortName: string, fullName: string) { var pos = this.removeOption(shortName, fullName); if (pos >= 0) { //found var value = this.positional[pos + 1]; //take value this.positional.splice(pos, 2); return value; }; return undefined; //not found } */ // --------------------------- /** * search for an option in the command line args, with variations * removes the option from the array * return position in the array where it was found|-1 */ searchOption(option) { const name = this.findDeclarationKey(option); const shortName = option.shortName; // search several possible forms of the option, e.g. -o --o -outDir --outDir const variants = ['-' + name, '--' + name]; if (shortName) { variants.push('--' + shortName, '-' + shortName); } // for each item in list for (const variant of variants) { const inx = this.clArgs.indexOf(variant); if (inx >= 0) { return inx; // found } } return -1; // not found } // ---------------------------------------------------- // construct and show help page based on valid options // ---------------------------------------------------- ShowHelpOptions() { // show help about declared options console.log(); console.log("-".repeat(60)); console.log("Options:"); for (const key in this.optDeclarations) { let line = ""; const opt = this.optDeclarations[key]; let text = "--" + key; if (opt.valueType) text = text + " " + opt.valueType; if (opt.shortName) { text = text + ", -" + opt.shortName; if (opt.valueType) text = text + " " + opt.valueType; } line = ` ${text}`.padEnd(50) + (opt.helpText ? opt.helpText : ""); console.log(line); } console.log("-".repeat(60)); } static getMethods(API) { const list = []; const proto = Object.getPrototypeOf(API); for (const key of Object.getOwnPropertyNames(proto)) { if (key != "constructor" && key != "_call" && key != "_view" && !key.endsWith("_HELP")) { list.push(key); } } return list; } // ---------------------------------------------------- // construct and show a help page based on the API for the commands // ---------------------------------------------------- ShowHelpPage(forCommand, API) { // list functions in the Extended and ContractAPI class, except the class constructor and view/call/HELP helpers const list = CommandLineArgs.getMethods(API) .concat(CommandLineArgs.getMethods(Object.getPrototypeOf(API))); list.sort(); // print all commands and their help if it's there for (const name of list) { if (forCommand && name != forCommand) continue; console.log("-".repeat(60)); console.log('command: ' + color.yellow + name + color.normal); // name the command //@ts-ignore if (API[name + "_HELP"]) { //if there's help... //@ts-ignore console.log(API[name + "_HELP"]()); // print the help } } this.ShowHelpOptions(); } } exports.CommandLineArgs = CommandLineArgs; // end class CommandLineArgs //# sourceMappingURL=CommandLineArgs.js.map