From c8fc4678fcb7df0bd5d37649dac1def46f358b85 Mon Sep 17 00:00:00 2001 From: Tangent Wantwight Date: Mon, 20 Nov 2023 19:51:38 -0500 Subject: [PATCH] Teach options parser about switches --- src/lib/options.test.ts | 42 ++++++++++++++++++++++----- src/lib/options.ts | 64 +++++++++++++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 16 deletions(-) diff --git a/src/lib/options.test.ts b/src/lib/options.test.ts index 77c08cd..9e72e01 100644 --- a/src/lib/options.test.ts +++ b/src/lib/options.test.ts @@ -1,6 +1,6 @@ -import { parse } from "../parser"; -import { Script, TextPiece } from "../words"; -import { getOpt, Options } from "./options"; +import { parse } from '../parser'; +import { Script, TextPiece } from '../words'; +import { getOpt, Options } from './options'; describe("getOpt", () => { const expectOpts =

(command: string, opts: P) => { @@ -16,13 +16,41 @@ describe("getOpt", () => { expectOpts("cmd apple banana", {}).toEqual([{}, "apple", "banana"])); it("enforces minimum # of arguments", () => - expectOpts("cmd", { $min: 1 }).toEqual([ - { error: "cmd: Not enough arguments" }, + expectOpts("extrovert", { $min: 1 }).toEqual([ + { error: "extrovert: Not enough arguments" }, ])); it("enforces maximum # of arguments", () => - expectOpts("cmd apple banana", { $max: 1 }).toEqual([ - { error: "cmd: Too many arguments" }, + expectOpts("introvert apple banana", { $max: 1 }).toEqual([ + { error: "introvert: Too many arguments" }, ])); it("allows # of arguments in-spec", () => expectOpts("cmd apple", { $min: 1, $max: 1 }).toEqual([{}, "apple"])); + + it("parses boolean switches", () => + expectOpts("cmd -red", { red: 0, blue: 0 }).toEqual([ + { red: true, blue: false }, + ])); + it("parses switches that take arguments", () => + expectOpts("cmd -onPepper {sneeze}", { onPepper: 1 }).toEqual([ + { onPepper: ["sneeze"] }, + ])); + it("enforces switch arguments exist", () => + expectOpts("cmd -onPepper", { onPepper: 1 }).toEqual([ + { error: "cmd: Not enough arguments to -onPepper" }, + ])); + it("raises an error on unknown switches", () => + expectOpts("cmd -unsolicited", {}).toEqual([ + { error: "cmd: -unsolicited is not a switch this command knows about" }, + ])); + + it("doesn't count switches as positional arguments", () => + expectOpts("cmd -yellow banana", { $min: 1, $max: 1, yellow: 0 }).toEqual([ + { yellow: true }, + "banana", + ])); + it("doesn't count quoted words as switches", () => + expectOpts('cmd "-purple"', { purple: 0 }).toEqual([ + { purple: false }, + "-purple", + ])); }); diff --git a/src/lib/options.ts b/src/lib/options.ts index 30f3aa0..e1fddf7 100644 --- a/src/lib/options.ts +++ b/src/lib/options.ts @@ -1,4 +1,4 @@ -import { AsText, ErrorResult, ProcResult, TextPiece } from "../words"; +import { AsText, ErrorResult, ProcResult, TextPiece } from '../words'; export type Options = Record & { $min?: number; @@ -9,7 +9,7 @@ type ParsedOptions

= { ? never : P[K] extends 0 ? boolean - : string[]; + : string[] | []; } & { error?: string; }; @@ -68,20 +68,66 @@ export function getOptRaw

( } } +const SWITCH_REGEX = /^-([^]*)/; + function getOptCore

( argv: TextPiece[], options: P ): [ParsedOptions

, TextPiece[]] { - const [cmd, ...textPiece] = argv; + const [cmd, ...textPieces] = argv; + const positionalArgs: TextPiece[] = []; const flags: ParsedOptions

= Object.fromEntries( Object.entries(options) .filter(([name]) => name != "$min" && name != "$max") - .map(([name]) => []) - ); + .map(([name]) => [name, options[name] == 0 ? false : []]) + ) as ParsedOptions

; - // TODO: parse switches + // loop over args & extract switches + for (let i = 0; i < textPieces.length; i++) { + const word = textPieces[i]; + if (!("bare" in word)) { + positionalArgs.push(word); + continue; + } - if (options.$min !== undefined && options.$min > textPiece.length) { + const switchMatch = SWITCH_REGEX.exec(word.bare); + if (switchMatch == null) { + positionalArgs.push(word); + continue; + } + + const switchName = switchMatch[1] as keyof ParsedOptions

& string; + if (!(switchName in flags)) { + return [ + { + error: `${AsText( + cmd + )}: -${switchName} is not a switch this command knows about`, + } as ParsedOptions

, + [], + ]; + } + + const switchArgCount = options[switchName]; + if (switchArgCount == 0) { + (flags[switchName] as boolean) = true; + } else if (i + switchArgCount >= textPieces.length) { + return [ + { + error: `${AsText(cmd)}: Not enough arguments to -${switchName}`, + } as ParsedOptions

, + [], + ]; + } else { + const takeUntil = i + switchArgCount; + for (i++; i <= takeUntil; i++) { + (flags[switchName] as string[]).push(AsText(textPieces[i])); + } + } + } + + // check correct number of positional arguments + if (options.$min !== undefined && options.$min > positionalArgs.length) { return [ { error: `${AsText(cmd)}: Not enough arguments`, @@ -89,7 +135,7 @@ function getOptCore

( [], ]; } - if (options.$max !== undefined && options.$max < textPiece.length) { + if (options.$max !== undefined && options.$max < positionalArgs.length) { return [ { error: `${AsText(cmd)}: Too many arguments`, @@ -98,5 +144,5 @@ function getOptCore

( ]; } - return [flags, textPiece]; + return [flags, positionalArgs]; }