From 46ea485fbd3fb9b8fbd35059ef8a4a11aa420eb7 Mon Sep 17 00:00:00 2001 From: Alberto Ricart Date: Tue, 6 Apr 2021 21:25:37 -0500 Subject: [PATCH] [feat] made flags easier to handle on the client side --- main.ts | 15 ++++++--- mod.ts | 95 +++++++++++++++++++++++++++++++++++++++++++-------------- test.ts | 12 ++++---- 3 files changed, 88 insertions(+), 34 deletions(-) diff --git a/main.ts b/main.ts index 88c0c80..00b1cdf 100644 --- a/main.ts +++ b/main.ts @@ -1,12 +1,17 @@ import { cli } from "./mod.ts"; -const root = cli("greeting"); +// create the root command +const root = cli({ use: "greeting (hello|goodbye) [--name name] [--strong]" }); +// add a subcommand const hello = root.addCommand({ use: "hello --name string [--strong]", short: "says hello", + // this is the handler for the command, you get + // the command being executed, any args following a `--` + // and an object to let you access relevant flags. run: (cmd, args, flags): Promise => { - const strong = (flags.get("strong")?.value ?? false) ? "!!!" : ""; - const n = flags.get("name")?.value ?? "mystery person"; + const strong = (flags.value("strong") ?? false) ? "!!!" : ""; + const n = flags.value("name") ?? "mystery person"; console.log(`hello ${n}${strong}`); return Promise.resolve(0); }, @@ -29,8 +34,8 @@ const goodbye = root.addCommand({ use: "goodbye --name string [--strong]", short: "says goodbye", run: (cmd, args, flags): Promise => { - const strong = (flags.get("strong")?.value ?? false) ? "!!!" : ""; - const n = flags.get("name")?.value ?? "mystery person"; + const strong = flags.value("strong") ? "!!!" : ""; + const n = flags.value("name") ?? "mystery person"; console.log(`goodbye ${n}${strong}`); return Promise.resolve(0); }, diff --git a/mod.ts b/mod.ts index 95a1289..6ca18c3 100644 --- a/mod.ts +++ b/mod.ts @@ -4,7 +4,7 @@ import { sprintf } from "https://deno.land/std/fmt/printf.ts"; export type Run = ( cmd: Command, args: string[], - flags: Map, + flags: Flags, ) => Promise; export interface Cmd { @@ -44,17 +44,70 @@ function flag(f: Partial): Flag { return Object.assign(d, f); } -function flagMap(flags: Flag[]): Map { - const m = new Map(); - flags.forEach((f) => { - if (f.name) { - m.set(f.name, f); +export interface Flags { + value(n: string): T; + values(n: string): T[]; + getFlag(n: string): Flag | null; +} + +export class FlagsImpl implements Flags { + m: Map; + + constructor(flags: Flag[]) { + this.m = new Map(); + flags.forEach((f) => { + if (f.name) { + this.m.set(f.name, f); + } + if (f.short) { + this.m.set(f.short, f); + } + }); + } + + getFlag(n: string): Flag | null { + const f = this.m.get(n); + if (f === undefined) { + return null; } - if (f.short) { - m.set(f.short, f); + return f; + } + + defaultValue(f: Flag): T { + let t; + if (f.type === "string") { + t = ""; + } else if (f.type === "boolean") { + t = false; + } else if (f.type === "number") { + t = 0; } - }); - return m; + return t as unknown as T; + } + + value(n: string): T { + const f = this.m.get(n); + if (!f) { + throw new Error("unknown flag ${n}"); + } + let v = f.value ?? f.default ?? this.defaultValue(f); + if (Array.isArray(v)) { + v = v[0]; + } + return v as T; + } + + values(n: string): T[] { + const f = this.m.get(n); + if (!f) { + throw new Error("unknown flag ${n}"); + } + let v = f.value ?? f.default ?? this.defaultValue(f); + if (!Array.isArray(v)) { + v = [v]; + } + return v as T[]; + } } export class Command implements Cmd { @@ -188,7 +241,7 @@ export class Command implements Cmd { return this.parent.getFlag(name); } - run(cmd: Command, args: string[], flags: Map): Promise { + run(cmd: Command, args: string[], flags: Flags): Promise { return this.cmd.run(cmd, args, flags); } @@ -235,7 +288,7 @@ export class RootCommand extends Command implements Execute { lastCmd!: { cmd: Command; args: string[]; - flags: Map; + flags: Flags; helped?: boolean; }; _help: Flag; @@ -289,11 +342,11 @@ export class RootCommand extends Command implements Execute { execute(args: string[] = Deno.args): Promise { const [cmd, a] = this.matchCmd(args); - const opts = cmd.getFlags(); + const flags = cmd.getFlags(); // deno-lint-ignore no-explicit-any const parseOpts = { "--": true } as any; - opts.forEach((f) => { + flags.forEach((f) => { const key = f.short.length ? f.short : f.name; if (f.short && f.name) { @@ -316,8 +369,8 @@ export class RootCommand extends Command implements Execute { const argv = parse(args, parseOpts); argv._ = a; - const cf: Flag[] = []; - opts.forEach((f) => { + flags.forEach((f) => { + f.changed = false; const key = f.short.length ? f.short : f.name; if (argv[key]) { f.value = argv[key]; @@ -326,11 +379,11 @@ export class RootCommand extends Command implements Execute { } else { f.changed = true; } - cf.push(f); } }); - const fm = flagMap(cf); + const fm = new FlagsImpl(flags); + this.lastCmd = { cmd: cmd, args: a, flags: fm }; if (this._help.value) { @@ -350,7 +403,7 @@ export function cli(opts: Partial): RootCommand { if (opts.run) { const orig = opts.run; opts.run = (cmd, args, flags): Promise => { - const h = flags.get("help"); + const h = flags.getFlag("help"); if (h && h.value) { cmd.help(); return Promise.resolve(1); @@ -389,10 +442,6 @@ export function calcPad( return { short: s[0].length, long: s[1].length }; } -export function argNames(f: Partial): [string?, string?] { - return [f.short, f.name]; -} - export function flagHelp( f: Partial, pad: { short: number; long: number }, diff --git a/test.ts b/test.ts index 6f9243b..1cf524d 100644 --- a/test.ts +++ b/test.ts @@ -3,14 +3,14 @@ import { assertEquals, assertThrows, } from "https://deno.land/std/testing/asserts.ts"; -import { cli, Cmd, Command, Flag } from "./mod.ts"; +import { cli, Cmd, Command, Flag, Flags } from "./mod.ts"; export function buildCmd(v: Partial, debug = false): Cmd { const d = { run: ( cmd: Command, args: string[], - flags: Map, + flags: Flags, ): Promise => { if (debug) { console.info(cmd.name, args, flags); @@ -76,9 +76,9 @@ Deno.test("matches nested commands", async () => { const rv = await root.execute(["-S", "short flag"]); assertEquals(rv, 0); - assert(root.lastCmd.flags.get("S")); - assertEquals(root.lastCmd.flags.get("S")!.value, "short flag"); - assertEquals(root.lastCmd.flags.get("long-flag")!.value, "short flag"); + assert(root.lastCmd.flags.getFlag("S")); + assertEquals(root.lastCmd.flags.value("S"), "short flag"); + assertEquals(root.lastCmd.flags.value("long-flag"), "short flag"); }); Deno.test("nested command will get help", async () => { @@ -87,7 +87,7 @@ Deno.test("nested command will get help", async () => { const rv = await root.execute(["a", "--help"]); assertEquals(rv, 1); assertEquals(root.lastCmd.cmd.name, "a"); - assertEquals(root.lastCmd.flags.get("help")!.value, true); + assertEquals(root.lastCmd.flags.value("help"), true); }); Deno.test("command needs use", () => {