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 { 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 { header as Header, generate as generateTypeScript } from "../typescript";
import { compile, ContractCode } from "../solc"; import { compile, ContractCode } from "../solc";
@ -50,7 +50,11 @@ function walkFilenames(filenames: Array<string>): Array<string> {
return result; return result;
} }
let cli = new CLI("generate"); let cli = new CLI(null, {
account: false,
provider: false,
transaction: false
});
class GeneratePlugin extends Plugin { class GeneratePlugin extends Plugin {
@ -60,12 +64,39 @@ class GeneratePlugin extends Plugin {
optimize: boolean; optimize: boolean;
noBytecode: 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> { async prepareOptions(argParser: ArgParser): Promise<void> {
await super.prepareOptions(argParser); await super.prepareOptions(argParser);
this.output = argParser.consumeOption("output"); this.output = argParser.consumeOption("output");
this.force = argParser.consumeFlag("force"); this.force = argParser.consumeFlag("force");
this.optimize = argParser.consumeFlag("no-optimize"); this.optimize = !argParser.consumeFlag("no-optimize");
this.noBytecode = argParser.consumeFlag("no-bytecode"); this.noBytecode = argParser.consumeFlag("no-bytecode");
} }
@ -121,6 +152,6 @@ class GeneratePlugin extends Plugin {
return Promise.resolve(null); return Promise.resolve(null);
} }
} }
cli.addPlugin("generate", GeneratePlugin); cli.setPlugin(GeneratePlugin);
cli.run(process.argv.slice(2)) cli.run(process.argv.slice(2))

@ -1,12 +1,15 @@
"use strict"; "use strict";
import fs from "fs"; import fs from "fs";
import { basename } from "path";
import { ethers } from "ethers"; import { ethers } from "ethers";
import scrypt from "scrypt-js"; import scrypt from "scrypt-js";
import { getChoice, getPassword, getProgressBar } from "./prompt"; import { getChoice, getPassword, getProgressBar } from "./prompt";
import { version } from "./_version";
class UsageError extends Error { } class UsageError extends Error { }
@ -473,7 +476,7 @@ export interface PluginType {
getOptionHelp?: () => Array<Help>; getOptionHelp?: () => Array<Help>;
} }
export class Plugin { export abstract class Plugin {
network: ethers.providers.Network; network: ethers.providers.Network;
provider: ethers.providers.Provider; provider: ethers.providers.Provider;
@ -654,110 +657,212 @@ export class Plugin {
} }
if (address === ethers.constants.AddressZero && !allowZero) { if (address === ethers.constants.AddressZero && !allowZero) {
this.throwError(message); this.throwError(message || "cannot use the zero address");
} }
return 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 { throwUsageError(message?: string): never {
throw new UsageError(message); throw new UsageError(message);
} }
// Shows error.message
throwError(message: string): never { throwError(message: string): never {
throw new Error(message); throw new Error(message);
} }
} }
class CheckPlugin extends Plugin { }
///////////////////////////// /////////////////////////////
// Command Line Runner // Command Line Runner
export type Options = {
account?: boolean;
provider?: boolean;
transaction?: boolean;
};
export class CLI { export class CLI {
readonly defaultCommand: string; readonly defaultCommand: string;
readonly plugins: { [ command: string ]: PluginType }; 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, "defaultCommand", defaultCommand || null);
ethers.utils.defineReadOnly(this, "plugins", { }); 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) { 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 { showUsage(message?: string, status?: number): never {
// Limit: | | // Limit: | |
console.log("Usage:"); console.log("Usage:");
let lines: Array<string> = []; if (this.standAlone) {
for (let cmd in this.plugins) { let help = ethers.utils.getStatic<() => Help>(this.standAlone, "getHelp")();
let plugin = this.plugins[cmd]; console.log(` ${ CLI.getAppName() } ${ help.name } [ OPTIONS ]`);
let help = (plugin.getHelp ? plugin.getHelp(): null); console.log("");
if (help == null) { continue; }
let helpLine = " " + help.name; let lines: Array<string> = [];
if (helpLine.length > 28) { let optionHelp = ethers.utils.getStatic<() => Array<Help>>(this.standAlone, "getOptionHelp")();
lines.push(helpLine); optionHelp.forEach((help) => {
lines.push(repeat(" ", 30) + help.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 { } else {
helpLine += repeat(" ", 30 - helpLine.length); console.log(` ${ CLI.getAppName() } COMMAND [ ARGS ] [ OPTIONS ]`);
lines.push(helpLine + help.help); console.log("");
} }
let optionHelp = (plugin.getOptionHelp ? plugin.getOptionHelp(): [ ]); let lines: Array<string> = [];
optionHelp.forEach((help) => { for (let cmd in this.plugins) {
lines.push(" " + help.name + repeat(" ", 27 - help.name.length) + help.help); 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.options.account) {
if (this.defaultCommand) { console.log("ACCOUNT OPTIONS");
console.log(" ethers [ COMMAND ] [ ARGS ] [ OPTIONS ]"); console.log(" --account FILENAME Load from a file (JSON, RAW or mnemonic)");
console.log(""); console.log(" --account RAW_KEY Use a private key (insecure *)");
console.log(`COMMANDS (default: ${this.defaultCommand})`); console.log(" --account 'MNEMONIC' Use a mnemonic (insecure *)");
} else { console.log(" --account - Use secure entry for a raw key or mnemonic");
console.log(" ethers COMMAND [ ARGS ] [ OPTIONS ]"); console.log(" --account-void ADDRESS Udd an address as a void signer");
console.log(""); console.log(" --account-void ENS_NAME Add the resolved address as a void signer");
console.log("COMMANDS"); 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");
lines.forEach((line) => { console.log(" --xxx-mnemonic-password Prompt for a (experimental) hard password");
console.log(line); 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("");
} }
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("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("");
console.log("(*) By including mnemonics or private keys on the command line they are"); 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"); 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> { async run(args: Array<string>): Promise<void> {
args = args.slice(); args = args.slice();
if (this.defaultCommand && !this.plugins[this.defaultCommand]) {
throw new Error("missing defaultCommand plugin");
}
let command: string = null; let command: string = null;
// We run a temporary argument parser to check for a command by processing standard options // We run a temporary argument parser to check for a command by processing standard options
{ {
let argParser = new ArgParser(args); 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); argParser.consumeFlag(key);
}); });
@ -792,31 +909,49 @@ export class CLI {
[ "network", "rpc", "account", "account-rpc", "account-void", "gas-price", "gas-limit", "nonce" ].forEach((option) => { [ "network", "rpc", "account", "account-rpc", "account-void", "gas-price", "gas-limit", "nonce" ].forEach((option) => {
argParser.consumeOption(option); argParser.consumeOption(option);
}); });
*/
let commandIndex = argParser._checkCommandIndex(); // Find the first unconsumed argument
if (commandIndex === -1) { if (!this.standAlone) {
command = this.defaultCommand; let commandIndex = argParser._checkCommandIndex();
} else { if (commandIndex === -1) {
command = args[commandIndex]; command = this.defaultCommand;
args.splice(commandIndex, 1); } else {
command = args[commandIndex];
args.splice(commandIndex, 1);
}
} }
} }
// Reset the argument parser // Reset the argument parser
let argParser = new ArgParser(args); 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")) { if (argParser.consumeFlag("help")) {
return this.showUsage(); return this.showUsage();
} }
let debug = argParser.consumeFlag("debug"); let debug = argParser.consumeFlag("debug");
// Create PLug-in instance // Create Plug-in instance
let plugin: Plugin = null; let plugin: Plugin = null;
try { if (this.standAlone) {
plugin = new this.plugins[command](); plugin = new this.standAlone;
} catch (error) { } else {
if (command) { this.showUsage("unknown command - " + command); } try {
return this.showUsage("no command provided", 1); plugin = new this.plugins[command]();
} catch (error) {
if (command) { this.showUsage("unknown command - " + command); }
return this.showUsage("no command provided", 1);
}
} }
try { try {
@ -825,14 +960,16 @@ export class CLI {
await plugin.run(); await plugin.run();
} catch (error) { } catch (error) {
if (error instanceof UsageError) {
return this.showUsage(error.message, 1);
}
if (debug) { if (debug) {
console.log("----- <DEBUG> ------") console.log("----- <DEBUG> ------")
console.log(error); console.log(error);
console.log("----- </DEBUG> -----") console.log("----- </DEBUG> -----")
} }
if (error instanceof UsageError) {
return this.showUsage(error.message, 1);
}
console.log("Error: " + error.message); console.log("Error: " + error.message);
process.exit(2); process.exit(2);
} }