Teach options parser about switches

This commit is contained in:
Tangent Wantwight 2023-11-20 19:51:38 -05:00
parent 0114209320
commit c8fc4678fc
2 changed files with 90 additions and 16 deletions

View File

@ -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 = <P extends Options>(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",
]));
});

View File

@ -1,4 +1,4 @@
import { AsText, ErrorResult, ProcResult, TextPiece } from "../words";
import { AsText, ErrorResult, ProcResult, TextPiece } from '../words';
export type Options = Record<string, number> & {
$min?: number;
@ -9,7 +9,7 @@ type ParsedOptions<P extends Options> = {
? never
: P[K] extends 0
? boolean
: string[];
: string[] | [];
} & {
error?: string;
};
@ -68,20 +68,66 @@ export function getOptRaw<P extends Options>(
}
}
const SWITCH_REGEX = /^-([^]*)/;
function getOptCore<P extends Options>(
argv: TextPiece[],
options: P
): [ParsedOptions<P>, TextPiece[]] {
const [cmd, ...textPiece] = argv;
const [cmd, ...textPieces] = argv;
const positionalArgs: TextPiece[] = [];
const flags: ParsedOptions<P> = Object.fromEntries(
Object.entries(options)
.filter(([name]) => name != "$min" && name != "$max")
.map(([name]) => [])
);
.map(([name]) => [name, options[name] == 0 ? false : []])
) as ParsedOptions<P>;
// 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<P> & string;
if (!(switchName in flags)) {
return [
{
error: `${AsText(
cmd
)}: -${switchName} is not a switch this command knows about`,
} as ParsedOptions<P>,
[],
];
}
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<P>,
[],
];
} 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<P extends Options>(
[],
];
}
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<P extends Options>(
];
}
return [flags, textPiece];
return [flags, positionalArgs];
}