Added CLI support for stand-alone (no sub-command) tools.

This commit is contained in:
Richard Moore 2019-07-27 18:51:05 -03:00
parent 74dbc281ed
commit b67b121239
No known key found for this signature in database
GPG Key ID: 665176BE8E9DC651
2 changed files with 250 additions and 82 deletions

@ -7,7 +7,7 @@ import { join as pathJoin } from "path";
import { ethers } from 'ethers';
import { ArgParser, CLI, Plugin } from '../cli';
import { ArgParser, CLI, Help, Plugin } from '../cli';
import { header as Header, generate as generateTypeScript } from "../typescript";
import { compile, ContractCode } from "../solc";
@ -50,7 +50,11 @@ function walkFilenames(filenames: Array<string>): Array<string> {
return result;
}
let cli = new CLI("generate");
let cli = new CLI(null, {
account: false,
provider: false,
transaction: false
});
class GeneratePlugin extends Plugin {
@ -60,12 +64,39 @@ class GeneratePlugin extends Plugin {
optimize: boolean;
noBytecode: boolean;
static getHelp(): Help {
return {
name: "FILENAME [ ... ]",
help: "Generates a TypeScript file of all Contracts. May specify folders."
};
}
static getOptionHelp(): Array<Help> {
return [
{
name: "--output FILENAME",
help: "Write the output to FILENAME (default: stdout)"
},
{
name: "--force",
help: "Overwrite files if they already exist"
},
{
name: "--no-optimize",
help: "Do not run the solc optimizer"
},
{
name: "--no-bytecode",
help: "Do not include bytecode and Factory methods"
}
];
}
async prepareOptions(argParser: ArgParser): Promise<void> {
await super.prepareOptions(argParser);
this.output = argParser.consumeOption("output");
this.force = argParser.consumeFlag("force");
this.optimize = argParser.consumeFlag("no-optimize");
this.optimize = !argParser.consumeFlag("no-optimize");
this.noBytecode = argParser.consumeFlag("no-bytecode");
}
@ -121,6 +152,6 @@ class GeneratePlugin extends Plugin {
return Promise.resolve(null);
}
}
cli.addPlugin("generate", GeneratePlugin);
cli.setPlugin(GeneratePlugin);
cli.run(process.argv.slice(2))

@ -1,12 +1,15 @@
"use strict";
import fs from "fs";
import { basename } from "path";
import { ethers } from "ethers";
import scrypt from "scrypt-js";
import { getChoice, getPassword, getProgressBar } from "./prompt";
import { version } from "./_version";
class UsageError extends Error { }
@ -473,7 +476,7 @@ export interface PluginType {
getOptionHelp?: () => Array<Help>;
}
export class Plugin {
export abstract class Plugin {
network: ethers.providers.Network;
provider: ethers.providers.Provider;
@ -654,110 +657,212 @@ export class Plugin {
}
if (address === ethers.constants.AddressZero && !allowZero) {
this.throwError(message);
this.throwError(message || "cannot use the zero address");
}
return address;
});
}
// Dumps formatted data
dump(header: string, info: any): void {
dump(header, info);
}
// Throwing a UsageError causes the --help to be shown above
// the error.message
throwUsageError(message?: string): never {
throw new UsageError(message);
}
// Shows error.message
throwError(message: string): never {
throw new Error(message);
}
}
class CheckPlugin extends Plugin { }
/////////////////////////////
// Command Line Runner
export type Options = {
account?: boolean;
provider?: boolean;
transaction?: boolean;
};
export class CLI {
readonly defaultCommand: string;
readonly plugins: { [ command: string ]: PluginType };
readonly standAlone: PluginType;
readonly options: Options;
constructor(defaultCommand?: string, options?: Options) {
ethers.utils.defineReadOnly(this, "options", {
account: true,
provider: true,
transaction: true
});
if (options) {
["account", "provider", "transaction"].forEach((key) => {
if ((<any>options)[key] == null) { return; }
(<any>(this.options))[key] = !!((<any>options)[key]);
});
}
Object.freeze(this.options);
constructor(defaultCommand: string) {
ethers.utils.defineReadOnly(this, "defaultCommand", defaultCommand || null);
ethers.utils.defineReadOnly(this, "plugins", { });
}
static getAppName(): string {
let appName = "ethers";
try {
appName = basename(process.mainModule.filename).split(".")[0];
} catch (error) { }
return appName;
}
// @TODO: Better way to specify default; i.e. may not have args
addPlugin(command: string, plugin: PluginType) {
this.plugins[command] = plugin;
if (this.standAlone) {
ethers.errors.throwError("only setPlugin or addPlugin may be used at once", ethers.errors.UNSUPPORTED_OPERATION, {
operation: "addPlugin"
});
} else if (this.plugins[command]) {
ethers.errors.throwError("command already exists", ethers.errors.UNSUPPORTED_OPERATION, {
operation: "addPlugin",
command: command
});
}
ethers.utils.defineReadOnly(this.plugins, command, plugin);
}
setPlugin(plugin: PluginType) {
if (Object.keys(this.plugins).length !== 0) {
ethers.errors.throwError("only setPlugin or addPlugin may be used at once", ethers.errors.UNSUPPORTED_OPERATION, {
operation: "setPlugin"
});
}
if (this.standAlone) {
ethers.errors.throwError("cannot setPlugin more than once", ethers.errors.UNSUPPORTED_OPERATION, {
operation: "setPlugin"
});
}
ethers.utils.defineReadOnly(this, "standAlone", plugin);
}
showUsage(message?: string, status?: number): never {
// Limit: | |
console.log("Usage:");
let lines: Array<string> = [];
for (let cmd in this.plugins) {
let plugin = this.plugins[cmd];
let help = (plugin.getHelp ? plugin.getHelp(): null);
if (help == null) { continue; }
let helpLine = " " + help.name;
if (helpLine.length > 28) {
lines.push(helpLine);
lines.push(repeat(" ", 30) + help.help);
if (this.standAlone) {
let help = ethers.utils.getStatic<() => Help>(this.standAlone, "getHelp")();
console.log(` ${ CLI.getAppName() } ${ help.name } [ OPTIONS ]`);
console.log("");
let lines: Array<string> = [];
let optionHelp = ethers.utils.getStatic<() => Array<Help>>(this.standAlone, "getOptionHelp")();
optionHelp.forEach((help) => {
lines.push(" " + help.name + repeat(" ", 28 - help.name.length) + help.help);
});
if (lines.length) {
console.log("OPTIONS");
lines.forEach((line) => {
console.log(line);
});
console.log("");
}
} else {
if (this.defaultCommand) {
console.log(` ${ CLI.getAppName() } [ COMMAND ] [ ARGS ] [ OPTIONS ]`);
console.log("");
} else {
helpLine += repeat(" ", 30 - helpLine.length);
lines.push(helpLine + help.help);
console.log(` ${ CLI.getAppName() } COMMAND [ ARGS ] [ OPTIONS ]`);
console.log("");
}
let optionHelp = (plugin.getOptionHelp ? plugin.getOptionHelp(): [ ]);
optionHelp.forEach((help) => {
lines.push(" " + help.name + repeat(" ", 27 - help.name.length) + help.help);
});
let lines: Array<string> = [];
for (let cmd in this.plugins) {
let plugin = this.plugins[cmd];
let help = ethers.utils.getStatic<() => Help>(plugin, "getHelp")();
if (help == null) { continue; }
let helpLine = " " + help.name;
if (helpLine.length > 28) {
lines.push(helpLine);
lines.push(repeat(" ", 30) + help.help);
} else {
helpLine += repeat(" ", 30 - helpLine.length);
lines.push(helpLine + help.help);
}
let optionHelp = ethers.utils.getStatic<() => Array<Help>>(plugin, "getOptionHelp")();
optionHelp.forEach((help) => {
lines.push(" " + help.name + repeat(" ", 27 - help.name.length) + help.help);
});
}
if (lines.length) {
if (this.defaultCommand) {
console.log(`COMMANDS (default: ${ this.defaultCommand })`);
} else {
console.log("COMMANDS");
}
lines.forEach((line) => {
console.log(line);
});
console.log("");
}
}
if (lines.length) {
if (this.defaultCommand) {
console.log(" ethers [ COMMAND ] [ ARGS ] [ OPTIONS ]");
console.log("");
console.log(`COMMANDS (default: ${this.defaultCommand})`);
} else {
console.log(" ethers COMMAND [ ARGS ] [ OPTIONS ]");
console.log("");
console.log("COMMANDS");
}
lines.forEach((line) => {
console.log(line);
});
if (this.options.account) {
console.log("ACCOUNT OPTIONS");
console.log(" --account FILENAME Load from a file (JSON, RAW or mnemonic)");
console.log(" --account RAW_KEY Use a private key (insecure *)");
console.log(" --account 'MNEMONIC' Use a mnemonic (insecure *)");
console.log(" --account - Use secure entry for a raw key or mnemonic");
console.log(" --account-void ADDRESS Udd an address as a void signer");
console.log(" --account-void ENS_NAME Add the resolved address as a void signer");
console.log(" --account-rpc ADDRESS Add the address from a JSON-RPC provider");
console.log(" --account-rpc INDEX Add the index from a JSON-RPC provider");
console.log(" --mnemonic-password Prompt for a password for mnemonics");
console.log(" --xxx-mnemonic-password Prompt for a (experimental) hard password");
console.log("");
}
if (this.options.provider) {
console.log("PROVIDER OPTIONS (default: all + homestead)");
console.log(" --alchemy Include Alchemy");
console.log(" --etherscan Include Etherscan");
console.log(" --infura Include INFURA");
console.log(" --nodesmith Include nodesmith");
console.log(" --rpc URL Include a custom JSON-RPC");
console.log(" --offline Dump signed transactions (no send)");
console.log(" --network NETWORK Network to connect to (default: homestead)");
console.log("");
}
if (this.options.transaction) {
console.log("TRANSACTION OPTIONS (default: query network)");
console.log(" --gasPrice GWEI Default gas price for transactions(in wei)");
console.log(" --gasLimit GAS Default gas limit for transactions");
console.log(" --nonce NONCE Initial nonce for the first transaction");
console.log(" --value VALUE Default value (in ether) for transactions");
console.log(" --yes Always accept Siging and Sending");
console.log("");
}
console.log("ACCOUNT OPTIONS");
console.log(" --account FILENAME Load from a file (JSON, RAW or mnemonic)");
console.log(" --account RAW_KEY Use a private key (insecure *)");
console.log(" --account 'MNEMONIC' Use a mnemonic (insecure *)");
console.log(" --account - Use secure entry for a raw key or mnemonic");
console.log(" --account-void ADDRESS Udd an address as a void signer");
console.log(" --account-void ENS_NAME Add the resolved address as a void signer");
console.log(" --account-rpc ADDRESS Add the address from a JSON-RPC provider");
console.log(" --account-rpc INDEX Add the index from a JSON-RPC provider");
console.log(" --mnemonic-password Prompt for a password for mnemonics");
console.log(" --xxx-mnemonic-password Prompt for a (experimental) hard password");
console.log("");
console.log("PROVIDER OPTIONS (default: getDefaultProvider)");
console.log(" --alchemy Include Alchemy");
console.log(" --etherscan Include Etherscan");
console.log(" --infura Include INFURA");
console.log(" --nodesmith Include nodesmith");
console.log(" --rpc URL Include a custom JSON-RPC");
console.log(" --offline Dump signed transactions (no send)");
console.log(" --network NETWORK Network to connect to (default: homestead)");
console.log("");
console.log("TRANSACTION OPTIONS (default: query the network)");
console.log(" --gasPrice GWEI Default gas price for transactions(in wei)");
console.log(" --gasLimit GAS Default gas limit for transactions");
console.log(" --nonce NONCE Initial nonce for the first transaction");
console.log(" --value VALUE Default value (in ether) for transactions");
console.log(" --yes Always accept Siging and Sending");
console.log("");
console.log("OTHER OPTIONS");
console.log(" --help Show this usage and quit");
console.log(" --debug Show stack traces for errors");
console.log(" --help Show this usage and exit");
console.log(" --version Show this version and exit");
console.log("");
console.log("(*) By including mnemonics or private keys on the command line they are");
console.log(" possibly readable by other users on your system and may get stored in");
@ -776,13 +881,25 @@ export class CLI {
async run(args: Array<string>): Promise<void> {
args = args.slice();
if (this.defaultCommand && !this.plugins[this.defaultCommand]) {
throw new Error("missing defaultCommand plugin");
}
let command: string = null;
// We run a temporary argument parser to check for a command by processing standard options
{
let argParser = new ArgParser(args);
[ "debug", "help", "mnemonic-password", "offline", "xxx-mnemonic-password", "yes"].forEach((key) => {
let plugin = new CheckPlugin();
await plugin.prepareOptions(argParser);
[ "debug", "help", "version"].forEach((key) => {
argParser.consumeFlag(key);
});
/*
[ "mnemonic-password", "offline", "xxx-mnemonic-password", "yes"].forEach((key) => {
argParser.consumeFlag(key);
});
@ -792,31 +909,49 @@ export class CLI {
[ "network", "rpc", "account", "account-rpc", "account-void", "gas-price", "gas-limit", "nonce" ].forEach((option) => {
argParser.consumeOption(option);
});
*/
let commandIndex = argParser._checkCommandIndex();
if (commandIndex === -1) {
command = this.defaultCommand;
} else {
command = args[commandIndex];
args.splice(commandIndex, 1);
// Find the first unconsumed argument
if (!this.standAlone) {
let commandIndex = argParser._checkCommandIndex();
if (commandIndex === -1) {
command = this.defaultCommand;
} else {
command = args[commandIndex];
args.splice(commandIndex, 1);
}
}
}
// Reset the argument parser
let argParser = new ArgParser(args);
if (argParser.consumeFlag("version")) {
let app = "ethers";
try {
app = basename(process.mainModule.filename).split(".")[0];
} catch (error) { }
console.log(app + "/" + version);
return;
}
if (argParser.consumeFlag("help")) {
return this.showUsage();
}
let debug = argParser.consumeFlag("debug");
// Create PLug-in instance
// Create Plug-in instance
let plugin: Plugin = null;
try {
plugin = new this.plugins[command]();
} catch (error) {
if (command) { this.showUsage("unknown command - " + command); }
return this.showUsage("no command provided", 1);
if (this.standAlone) {
plugin = new this.standAlone;
} else {
try {
plugin = new this.plugins[command]();
} catch (error) {
if (command) { this.showUsage("unknown command - " + command); }
return this.showUsage("no command provided", 1);
}
}
try {
@ -825,14 +960,16 @@ export class CLI {
await plugin.run();
} catch (error) {
if (error instanceof UsageError) {
return this.showUsage(error.message, 1);
}
if (debug) {
console.log("----- <DEBUG> ------")
console.log(error);
console.log("----- </DEBUG> -----")
}
if (error instanceof UsageError) {
return this.showUsage(error.message, 1);
}
console.log("Error: " + error.message);
process.exit(2);
}