diff --git a/packages/taler-util/src/clk.test.ts b/packages/taler-util/src/clk.test.ts new file mode 100644 index 000000000..bec93947b --- /dev/null +++ b/packages/taler-util/src/clk.test.ts @@ -0,0 +1,46 @@ +/* + This file is part of GNU Taler + (C) 2018-2019 GNUnet e.V. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Type-safe codecs for converting from/to JSON. + */ + +import test from "ava"; +import { clk } from "./clk.js"; +import { + Codec, + buildCodecForObject, + codecForConstString, + codecForString, + buildCodecForUnion, +} from "./codec.js"; + +test("bla", (t) => { + const prog = clk.program("foo", { + help: "Hello", + }); + + let success = false; + + prog.maybeOption("opt1", ["-o", "--opt1"], clk.INT).action((args) => { + success = true; + t.deepEqual(args.foo.opt1, 42); + }); + + prog.run(["bla", "-o", "42"]); + + t.true(success); +}); diff --git a/packages/taler-util/src/clk.ts b/packages/taler-util/src/clk.ts index d172eed48..e99ebf733 100644 --- a/packages/taler-util/src/clk.ts +++ b/packages/taler-util/src/clk.ts @@ -20,6 +20,7 @@ import process from "process"; import path from "path"; import readline from "readline"; +import { devNull } from "os"; export namespace clk { class Converter {} @@ -329,6 +330,20 @@ export namespace clk { const myArgs: any = (parsedArgs[this.argKey] = {}); const foundOptions: { [name: string]: boolean } = {}; const currentName = this.name ?? progname; + const storeOption = (def: OptionDef, value: string) => { + foundOptions[def.name] = true; + if (def.conv === INT) { + myArgs[def.name] = Number.parseInt(value); + } else if (def.conv == null || def.conv === STRING) { + myArgs[def.name] = value; + } else { + throw Error("unknown converter"); + } + }; + const storeFlag = (def: OptionDef, value: boolean) => { + foundOptions[def.name] = true; + myArgs[def.name] = value; + }; for (i = 0; i < unparsedArgs.length; i++) { const argVal = unparsedArgs[i]; if (argsTerminated == false) { @@ -353,8 +368,7 @@ export namespace clk { process.exit(-1); throw Error("not reached"); } - foundOptions[d.name] = true; - myArgs[d.name] = true; + storeFlag(d, true); } else { if (r.value === undefined) { if (i === unparsedArgs.length - 1) { @@ -362,12 +376,11 @@ export namespace clk { process.exit(-1); throw Error("not reached"); } - myArgs[d.name] = unparsedArgs[i + 1]; + storeOption(d, unparsedArgs[i + 1]); i++; } else { - myArgs[d.name] = r.value; + storeOption(d, r.value); } - foundOptions[d.name] = true; } continue; } @@ -381,8 +394,7 @@ export namespace clk { process.exit(-1); } if (opt.isFlag) { - myArgs[opt.name] = true; - foundOptions[opt.name] = true; + storeFlag(opt, true); } else { if (si == optShort.length - 1) { if (i === unparsedArgs.length - 1) { @@ -390,13 +402,12 @@ export namespace clk { process.exit(-1); throw Error("not reached"); } else { - myArgs[opt.name] = unparsedArgs[i + 1]; + storeOption(opt, unparsedArgs[i + 1]); i++; } } else { - myArgs[opt.name] = optShort.substring(si + 1); + storeOption(opt, optShort.substring(si + 1)); } - foundOptions[opt.name] = true; break; } } @@ -508,16 +519,21 @@ export namespace clk { }); } - run(): void { - const args = process.argv; - if (args.length < 2) { + run(cmdlineArgs?: string[]): void { + let args: string[]; + if (cmdlineArgs) { + args = cmdlineArgs; + } else { + args = process.argv.slice(1); + } + if (args.length < 1) { console.error( "Error while parsing command line arguments: not enough arguments", ); process.exit(-1); } - const progname = path.basename(args[1]); - const rest = args.slice(2); + const progname = path.basename(args[0]); + const rest = args.slice(1); this.mainCommand.run(progname, [], rest, {}); } diff --git a/packages/taler-wallet-cli/src/clk.ts b/packages/taler-wallet-cli/src/clk.ts deleted file mode 100644 index ca6dcc1a4..000000000 --- a/packages/taler-wallet-cli/src/clk.ts +++ /dev/null @@ -1,614 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see - */ - -/** - * Imports. - */ -import process from "process"; -import path from "path"; -import readline from "readline"; - -class Converter {} - -export const INT = new Converter(); -export const STRING: Converter = new Converter(); - -export interface OptionArgs { - help?: string; - default?: T; - onPresentHandler?: (v: T) => void; -} - -export interface ArgumentArgs { - metavar?: string; - help?: string; - default?: T; -} - -export interface SubcommandArgs { - help?: string; -} - -export interface FlagArgs { - help?: string; -} - -export interface ProgramArgs { - help?: string; -} - -interface ArgumentDef { - name: string; - conv: Converter; - args: ArgumentArgs; - required: boolean; -} - -interface SubcommandDef { - commandGroup: CommandGroup; - name: string; - args: SubcommandArgs; -} - -type ActionFn = (x: TG) => void; - -type SubRecord = { - [Y in S]: { [X in N]: V }; -}; - -interface OptionDef { - name: string; - flagspec: string[]; - /** - * Converter, only present for options, not for flags. - */ - conv?: Converter; - args: OptionArgs; - isFlag: boolean; - required: boolean; -} - -function splitOpt(opt: string): { key: string; value?: string } { - const idx = opt.indexOf("="); - if (idx == -1) { - return { key: opt }; - } - return { key: opt.substring(0, idx), value: opt.substring(idx + 1) }; -} - -function formatListing(key: string, value?: string): string { - const res = " " + key; - if (!value) { - return res; - } - if (res.length >= 25) { - return res + "\n" + " " + value; - } else { - return res.padEnd(24) + " " + value; - } -} - -export class CommandGroup { - private shortOptions: { [name: string]: OptionDef } = {}; - private longOptions: { [name: string]: OptionDef } = {}; - private subcommandMap: { [name: string]: SubcommandDef } = {}; - private subcommands: SubcommandDef[] = []; - private options: OptionDef[] = []; - private arguments: ArgumentDef[] = []; - - private myAction?: ActionFn; - - constructor( - private argKey: string, - private name: string | null, - private scArgs: SubcommandArgs, - ) {} - - action(f: ActionFn): void { - if (this.myAction) { - throw Error("only one action supported per command"); - } - this.myAction = f; - } - - requiredOption( - name: N, - flagspec: string[], - conv: Converter, - args: OptionArgs = {}, - ): CommandGroup> { - const def: OptionDef = { - args: args, - conv: conv, - flagspec: flagspec, - isFlag: false, - required: true, - name: name as string, - }; - this.options.push(def); - for (const flag of flagspec) { - if (flag.startsWith("--")) { - const flagname = flag.substring(2); - this.longOptions[flagname] = def; - } else if (flag.startsWith("-")) { - const flagname = flag.substring(1); - this.shortOptions[flagname] = def; - } else { - throw Error("option must start with '-' or '--'"); - } - } - return this as any; - } - - maybeOption( - name: N, - flagspec: string[], - conv: Converter, - args: OptionArgs = {}, - ): CommandGroup> { - const def: OptionDef = { - args: args, - conv: conv, - flagspec: flagspec, - isFlag: false, - required: false, - name: name as string, - }; - this.options.push(def); - for (const flag of flagspec) { - if (flag.startsWith("--")) { - const flagname = flag.substring(2); - this.longOptions[flagname] = def; - } else if (flag.startsWith("-")) { - const flagname = flag.substring(1); - this.shortOptions[flagname] = def; - } else { - throw Error("option must start with '-' or '--'"); - } - } - return this as any; - } - - requiredArgument( - name: N, - conv: Converter, - args: ArgumentArgs = {}, - ): CommandGroup> { - const argDef: ArgumentDef = { - args: args, - conv: conv, - name: name as string, - required: true, - }; - this.arguments.push(argDef); - return this as any; - } - - maybeArgument( - name: N, - conv: Converter, - args: ArgumentArgs = {}, - ): CommandGroup> { - const argDef: ArgumentDef = { - args: args, - conv: conv, - name: name as string, - required: false, - }; - this.arguments.push(argDef); - return this as any; - } - - flag( - name: N, - flagspec: string[], - args: OptionArgs = {}, - ): CommandGroup> { - const def: OptionDef = { - args: args, - flagspec: flagspec, - isFlag: true, - required: false, - name: name as string, - }; - this.options.push(def); - for (const flag of flagspec) { - if (flag.startsWith("--")) { - const flagname = flag.substring(2); - this.longOptions[flagname] = def; - } else if (flag.startsWith("-")) { - const flagname = flag.substring(1); - this.shortOptions[flagname] = def; - } else { - throw Error("option must start with '-' or '--'"); - } - } - return this as any; - } - - subcommand( - argKey: GN, - name: string, - args: SubcommandArgs = {}, - ): CommandGroup { - const cg = new CommandGroup(argKey as string, name, args); - const def: SubcommandDef = { - commandGroup: cg, - name: name as string, - args: args, - }; - cg.flag("help", ["-h", "--help"], { - help: "Show this message and exit.", - }); - this.subcommandMap[name as string] = def; - this.subcommands.push(def); - this.subcommands = this.subcommands.sort((x1, x2) => { - const a = x1.name; - const b = x2.name; - if (a === b) { - return 0; - } else if (a < b) { - return -1; - } else { - return 1; - } - }); - return cg as any; - } - - printHelp(progName: string, parents: CommandGroup[]): void { - let usageSpec = ""; - for (const p of parents) { - usageSpec += (p.name ?? progName) + " "; - if (p.arguments.length >= 1) { - usageSpec += " "; - } - } - usageSpec += (this.name ?? progName) + " "; - if (this.subcommands.length != 0) { - usageSpec += "COMMAND "; - } - for (const a of this.arguments) { - const argName = a.args.metavar ?? a.name; - usageSpec += `<${argName}> `; - } - usageSpec = usageSpec.trimRight(); - console.log(`Usage: ${usageSpec}`); - if (this.scArgs.help) { - console.log(); - console.log(this.scArgs.help); - } - if (this.options.length != 0) { - console.log(); - console.log("Options:"); - for (const opt of this.options) { - let optSpec = opt.flagspec.join(", "); - if (!opt.isFlag) { - optSpec = optSpec + "=VALUE"; - } - console.log(formatListing(optSpec, opt.args.help)); - } - } - - if (this.subcommands.length != 0) { - console.log(); - console.log("Commands:"); - for (const subcmd of this.subcommands) { - console.log(formatListing(subcmd.name, subcmd.args.help)); - } - } - } - - /** - * Run the (sub-)command with the given command line parameters. - */ - run( - progname: string, - parents: CommandGroup[], - unparsedArgs: string[], - parsedArgs: any, - ): void { - let posArgIndex = 0; - let argsTerminated = false; - let i; - let foundSubcommand: CommandGroup | undefined = undefined; - const myArgs: any = (parsedArgs[this.argKey] = {}); - const foundOptions: { [name: string]: boolean } = {}; - const currentName = this.name ?? progname; - for (i = 0; i < unparsedArgs.length; i++) { - const argVal = unparsedArgs[i]; - if (argsTerminated == false) { - if (argVal === "--") { - argsTerminated = true; - continue; - } - if (argVal.startsWith("--")) { - const opt = argVal.substring(2); - const r = splitOpt(opt); - const d = this.longOptions[r.key]; - if (!d) { - console.error( - `error: unknown option '--${r.key}' for ${currentName}`, - ); - process.exit(-1); - throw Error("not reached"); - } - if (d.isFlag) { - if (r.value !== undefined) { - console.error(`error: flag '--${r.key}' does not take a value`); - process.exit(-1); - throw Error("not reached"); - } - foundOptions[d.name] = true; - myArgs[d.name] = true; - } else { - if (r.value === undefined) { - if (i === unparsedArgs.length - 1) { - console.error(`error: option '--${r.key}' needs an argument`); - process.exit(-1); - throw Error("not reached"); - } - myArgs[d.name] = unparsedArgs[i + 1]; - i++; - } else { - myArgs[d.name] = r.value; - } - foundOptions[d.name] = true; - } - continue; - } - if (argVal.startsWith("-") && argVal != "-") { - const optShort = argVal.substring(1); - for (let si = 0; si < optShort.length; si++) { - const chr = optShort[si]; - const opt = this.shortOptions[chr]; - if (!opt) { - console.error(`error: option '-${chr}' not known`); - process.exit(-1); - } - if (opt.isFlag) { - myArgs[opt.name] = true; - foundOptions[opt.name] = true; - } else { - if (si == optShort.length - 1) { - if (i === unparsedArgs.length - 1) { - console.error(`error: option '-${chr}' needs an argument`); - process.exit(-1); - throw Error("not reached"); - } else { - myArgs[opt.name] = unparsedArgs[i + 1]; - i++; - } - } else { - myArgs[opt.name] = optShort.substring(si + 1); - } - foundOptions[opt.name] = true; - break; - } - } - continue; - } - } - if (this.subcommands.length != 0) { - const subcmd = this.subcommandMap[argVal]; - if (!subcmd) { - console.error(`error: unknown command '${argVal}'`); - process.exit(-1); - throw Error("not reached"); - } - foundSubcommand = subcmd.commandGroup; - break; - } else { - const d = this.arguments[posArgIndex]; - if (!d) { - console.error(`error: too many arguments for ${currentName}`); - process.exit(-1); - throw Error("not reached"); - } - myArgs[d.name] = unparsedArgs[i]; - posArgIndex++; - } - } - - if (parsedArgs[this.argKey].help) { - this.printHelp(progname, parents); - process.exit(0); - throw Error("not reached"); - } - - for (let i = posArgIndex; i < this.arguments.length; i++) { - const d = this.arguments[i]; - if (d.required) { - if (d.args.default !== undefined) { - myArgs[d.name] = d.args.default; - } else { - console.error( - `error: missing positional argument '${d.name}' for ${currentName}`, - ); - process.exit(-1); - throw Error("not reached"); - } - } - } - - for (const option of this.options) { - if (option.isFlag == false && option.required == true) { - if (!foundOptions[option.name]) { - if (option.args.default !== undefined) { - myArgs[option.name] = option.args.default; - } else { - const name = option.flagspec.join(","); - console.error(`error: missing option '${name}'`); - process.exit(-1); - throw Error("not reached"); - } - } - } - } - - for (const option of this.options) { - const ph = option.args.onPresentHandler; - if (ph && foundOptions[option.name]) { - ph(myArgs[option.name]); - } - } - - if (foundSubcommand) { - foundSubcommand.run( - progname, - Array.prototype.concat(parents, [this]), - unparsedArgs.slice(i + 1), - parsedArgs, - ); - } else if (this.myAction) { - let r; - try { - r = this.myAction(parsedArgs); - } catch (e) { - console.error(`An error occurred while running ${currentName}`); - console.error(e); - process.exit(1); - } - Promise.resolve(r).catch((e) => { - console.error(`An error occurred while running ${currentName}`); - console.error(e); - process.exit(1); - }); - } else { - this.printHelp(progname, parents); - process.exit(-1); - throw Error("not reached"); - } - } -} - -export class Program { - private mainCommand: CommandGroup; - - constructor(argKey: string, args: ProgramArgs = {}) { - this.mainCommand = new CommandGroup(argKey, null, { - help: args.help, - }); - this.mainCommand.flag("help", ["-h", "--help"], { - help: "Show this message and exit.", - }); - } - - run(): void { - const args = process.argv; - if (args.length < 2) { - console.error( - "Error while parsing command line arguments: not enough arguments", - ); - process.exit(-1); - } - const progname = path.basename(args[1]); - const rest = args.slice(2); - - this.mainCommand.run(progname, [], rest, {}); - } - - subcommand( - argKey: GN, - name: string, - args: SubcommandArgs = {}, - ): CommandGroup { - const cmd = this.mainCommand.subcommand(argKey, name as string, args); - return cmd as any; - } - - requiredOption( - name: N, - flagspec: string[], - conv: Converter, - args: OptionArgs = {}, - ): Program> { - this.mainCommand.requiredOption(name, flagspec, conv, args); - return this as any; - } - - maybeOption( - name: N, - flagspec: string[], - conv: Converter, - args: OptionArgs = {}, - ): Program> { - this.mainCommand.maybeOption(name, flagspec, conv, args); - return this as any; - } - - /** - * Add a flag (option without value) to the program. - */ - flag( - name: N, - flagspec: string[], - args: OptionArgs = {}, - ): Program> { - this.mainCommand.flag(name, flagspec, args); - return this as any; - } - - /** - * Add a required positional argument to the program. - */ - requiredArgument( - name: N, - conv: Converter, - args: ArgumentArgs = {}, - ): Program> { - this.mainCommand.requiredArgument(name, conv, args); - return this as any; - } - - /** - * Add an optional argument to the program. - */ - maybeArgument( - name: N, - conv: Converter, - args: ArgumentArgs = {}, - ): Program> { - this.mainCommand.maybeArgument(name, conv, args); - return this as any; - } -} - -export type GetArgType = T extends Program - ? AT - : T extends CommandGroup - ? AT - : any; - -export function program( - argKey: PN, - args: ProgramArgs = {}, -): Program { - return new Program(argKey as string, args); -} - -export function prompt(question: string): Promise { - const stdinReadline = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - return new Promise((resolve, reject) => { - stdinReadline.question(question, (res) => { - resolve(res); - stdinReadline.close(); - }); - }); -} diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 5fd608f77..31e0b0f65 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -17,65 +17,60 @@ /** * Imports. */ -import os from "os"; -import fs from "fs"; -import path from "path"; import { deepStrictEqual } from "assert"; +import fs from "fs"; +import os from "os"; +import path from "path"; // Polyfill for encoding which isn't present globally in older nodejs versions -import { TextEncoder, TextDecoder } from "util"; +import { + addPaytoQueryParams, + AgeRestriction, + Amounts, + classifyTalerUri, + clk, + codecForList, + codecForString, + Configuration, + decodeCrock, + encodeCrock, + getRandomBytes, + j2s, + Logger, + parsePaytoUri, + PreparePayResultType, + RecoveryMergeStrategy, + rsaBlind, + setDangerousTimetravel, + setGlobalLogLevelFromString, + TalerUriType, +} from "@gnu-taler/taler-util"; +import { + CryptoDispatcher, + getDefaultNodeWallet, + getErrorDetailFromException, + nativeCrypto, + NodeHttpLib, + NodeThreadCryptoWorkerFactory, + summarizeTalerErrorDetail, + SynchronousCryptoWorkerFactory, + Wallet, + WalletApiOperation, + WalletCoreApiClient, + walletCoreDebugFlags, +} from "@gnu-taler/taler-wallet-core"; +import type { TalerCryptoInterface } from "@gnu-taler/taler-wallet-core/src/crypto/cryptoImplementation"; +import { TextDecoder, TextEncoder } from "util"; +import { runBench1 } from "./bench1.js"; +import { runBench2 } from "./bench2.js"; +import { runBench3 } from "./bench3.js"; +import { runEnv1 } from "./env1.js"; +import { GlobalTestState, runTestWithState } from "./harness/harness.js"; +import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; +import { lintExchangeDeployment } from "./lint.js"; // @ts-ignore global.TextEncoder = TextEncoder; // @ts-ignore global.TextDecoder = TextDecoder; -import * as clk from "./clk.js"; -import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; -import { - PreparePayResultType, - setDangerousTimetravel, - classifyTalerUri, - TalerUriType, - RecoveryMergeStrategy, - Amounts, - addPaytoQueryParams, - codecForList, - codecForString, - Logger, - Configuration, - decodeCrock, - rsaBlind, - LogLevel, - setGlobalLogLevelFromString, - parsePaytoUri, - AgeRestriction, - getRandomBytes, - encodeCrock, - j2s, -} from "@gnu-taler/taler-util"; -import { - NodeHttpLib, - getDefaultNodeWallet, - NodeThreadCryptoWorkerFactory, - walletCoreDebugFlags, - WalletApiOperation, - WalletCoreApiClient, - Wallet, - getErrorDetailFromException, - CryptoDispatcher, - SynchronousCryptoWorkerFactory, - nativeCrypto, - performanceNow, - summarizeTalerErrorDetail, -} from "@gnu-taler/taler-wallet-core"; -import { lintExchangeDeployment } from "./lint.js"; -import { runBench1 } from "./bench1.js"; -import { runEnv1 } from "./env1.js"; -import { GlobalTestState, runTestWithState } from "./harness/harness.js"; -import { runBench2 } from "./bench2.js"; -import { runBench3 } from "./bench3.js"; -import { - TalerCryptoInterface, - TalerCryptoInterfaceR, -} from "@gnu-taler/taler-wallet-core/src/crypto/cryptoImplementation"; // This module also serves as the entry point for the crypto // thread worker, and thus must expose these two handlers. @@ -390,13 +385,10 @@ walletCli help: "Withdraw with a taler://withdraw/ URI", }) .requiredArgument("uri", clk.STRING) - .maybeOption("restrictAge", ["--restrict-age"], clk.STRING) + .maybeOption("restrictAge", ["--restrict-age"], clk.INT) .action(async (args) => { const uri = args.withdraw.uri; - const restrictAge = - args.withdraw.restrictAge == null - ? undefined - : Number.parseInt(args.withdraw.restrictAge); + const restrictAge = args.withdraw.restrictAge; console.log(`age restriction requested (${restrictAge})`); await withWallet(args, async (wallet) => { const withdrawInfo = await wallet.client.call(