core/converters/invocation.converter.ts

core/converters/invocation.converter.ts is a file in Argv Contract. 83 lines of code and 17 definitions.

import type { ArgvOutcome, ArgvSpec } from "#types/invocation.types";
import { FLAG_PREFIX, HELP_FLAGS } from "#configuration/constants/invocation.constants";
import {
    missingValue,
    repeatedFlag,
    tooFewPositionals,
    tooManyPositionals,
    undeclaredFlag,
} from "#configuration/strings/invocation.strings";
import { usageOf } from "#core/formatters/invocation.formatter";

interface Scan {
    readonly flags: ReadonlyMap<string, readonly string[]>;
    readonly positionals: readonly string[];
    readonly rest: readonly string[];
    readonly refused: string | null;
    readonly help: boolean;
    readonly consumed: number;
}

const refused = function refused(scan: Scan, reason: string): Scan {
    return { ...scan, consumed: 1, refused: scan.refused ?? reason };
};

const withFlag = function withFlag(scan: Scan, word: string, value: string, consumed: number): Scan {
    const seen = scan.flags.get(word) ?? [];
    return { ...scan, consumed, flags: new Map([...scan.flags, [word, [...seen, value]]]) };
};

const takeFlag = function takeFlag(scan: Scan, spec: ArgvSpec, word: string, next: string | undefined): Scan {
    const flag = spec.flags.find((held) => held.name === word);
    if (flag === undefined) {
        return refused(scan, undeclaredFlag(word, spec.command));
    }
    if ((scan.flags.get(word) ?? []).length > 0 && flag.repeatable !== true) {
        return refused(scan, repeatedFlag(word));
    }
    if (!flag.takesValue) {
        return withFlag(scan, word, "", 1);
    }
    if (next === undefined || next.startsWith(FLAG_PREFIX)) {
        return refused(scan, missingValue(word));
    }
    return withFlag(scan, word, next, 2);
};

const restBegins = function restBegins(scan: Scan, spec: ArgvSpec): boolean {
    const declared = spec.positionals ?? [];
    return spec.rest !== undefined && declared.at(-1)?.variadic !== true && scan.positionals.length === declared.length;
};

const takeWord = function takeWord(scan: Scan, spec: ArgvSpec, word: string, next: string | undefined): Scan {
    if (restBegins(scan, spec)) {
        return { ...scan, consumed: 1, rest: [...scan.rest, word] };
    }
    if (HELP_FLAGS.has(word)) {
        return { ...scan, consumed: 1, help: true };
    }
    if (!word.startsWith(FLAG_PREFIX)) {
        return { ...scan, consumed: 1, positionals: [...scan.positionals, word] };
    }
    return takeFlag(scan, spec, word, next);
};

const arityRefusal = function arityRefusal(scan: Scan, spec: ArgvSpec): string | null {
    const declared = spec.positionals ?? [];
    const variadic = declared.at(-1)?.variadic === true;
    const given = scan.positionals.length;
    if (!variadic && given > declared.length) {
        return tooManyPositionals(given, declared.length, scan.positionals);
    }
    const required = declared.filter((held) => held.optional !== true).length;
    return given < required ? tooFewPositionals(given, required) : null;
};

export const parseArgv = function parseArgv(spec: ArgvSpec, argv: readonly string[]): ArgvOutcome {
    let scan: Scan = { consumed: 0, flags: new Map(), help: false, positionals: [], refused: null, rest: [] };
    let at = 0;
    while (at < argv.length && !scan.help) {
        scan = takeWord(scan, spec, argv[at] ?? "", argv[at + 1]);
        at += scan.consumed;
    }
    if (scan.help) {
        return { kind: "help", usage: usageOf(spec) };
    }
    const reason = scan.refused ?? arityRefusal(scan, spec);
    if (reason !== null) {
        return { kind: "refused", reason, usage: usageOf(spec) };
    }
    return { argv: { flags: scan.flags, positionals: scan.positionals, rest: scan.rest }, kind: "parsed" };
};