Teach options parser about switches
This commit is contained in:
parent
0114209320
commit
c8fc4678fc
2 changed files with 90 additions and 16 deletions
|
@ -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",
|
||||
]));
|
||||
});
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue