Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use argv-iterator #16

Merged
merged 13 commits into from
Oct 31, 2022
145 changes: 145 additions & 0 deletions src/argv-iterator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
export const DOUBLE_DASH = '--';

type BreakIteration = false;

type onValueCallbackType = (
value?: string,
index?: number,
) => void | BreakIteration;

type onFlag = (
name: string,
value: string | undefined,
index: number,
) => void | BreakIteration | onValueCallbackType;

type onArgument = (
args: string[],
index: number,
isEoF?: boolean,
) => void | BreakIteration;

const valueDelimiterPattern = /[.:=]/;

const isFlagPattern = /^-{1,2}[\da-z]/i;

const parseFlagArgv = (
flagArgv: string,
): [
flagName: string,
flagValue: string | undefined,
isAlias: boolean,
] | undefined => {
if (!isFlagPattern.test(flagArgv)) {
return;
}

const isAlias = !flagArgv.startsWith(DOUBLE_DASH);
let flagName = flagArgv.slice(isAlias ? 1 : 2);

let flagValue;

const hasValueDalimiter = flagName.match(valueDelimiterPattern);
if (hasValueDalimiter?.index) {
const equalIndex = hasValueDalimiter.index;
flagValue = flagName.slice(equalIndex + 1);
flagName = flagName.slice(0, equalIndex);
}

return [flagName, flagValue, isAlias];
};

export const argvIterator = (
argv: string[],
callbacks: {
onFlag?: onFlag;
onArgument?: onArgument;
},
) => {
let onValueCallback: undefined | onValueCallbackType;

const triggerCallback = (
value?: string,
index?: number,
) => {
if (!onValueCallback) {
return true;
}

const result = onValueCallback(value, index);
onValueCallback = undefined;
return result;
};

ARGV_ITERATION:
for (let i = 0; i < argv.length; i += 1) {
const argvElement = argv[i];

if (argvElement === DOUBLE_DASH) {
if (triggerCallback() === false) {
break;
}

const remaining = argv.slice(i + 1);
callbacks.onArgument?.(remaining, i, true);
break;
}

const parsedFlag = parseFlagArgv(argvElement);

if (parsedFlag) {
if (triggerCallback() === false) {
break;
}

if (!callbacks.onFlag) {
continue;
}

const [flagName, flagValue, isAlias] = parsedFlag;

if (isAlias) {
for (let j = 0; j < flagName.length; j += 1) {
const alias = flagName[j];
const isLastAlias = j === flagName.length - 1;
const result = callbacks.onFlag(
alias,
isLastAlias ? flagValue : undefined,
i,
);

if (result === false) {
break ARGV_ITERATION;
} else if (typeof result === 'function') {
onValueCallback = result;
}
}
} else {
const result = callbacks.onFlag(
flagName,
flagValue,
i,
);

if (result === false) {
break;
} else if (typeof result === 'function') {
onValueCallback = result;
}
}
} else {
const result = triggerCallback(argvElement, i);
if (
result === false
|| (
result === true // no callback set
&& callbacks.onArgument?.([argvElement], i) === false
)
) {
break;
}
}
}

triggerCallback();
};
171 changes: 46 additions & 125 deletions src/type-flag.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import type {
Flags,
FlagTypeOrSchema,
ParsedFlags,
TypeFlag,
} from './types';
import {
DOUBLE_DASH,
kebabToCamel,
createFlagsObject,
mapAliases,
parseFlagArgv,
createRegistry,
normalizeBoolean,
applyParser,
setDefaultFlagValues,
parseFlagType,
hasOwn,
getOwn,
} from './utils';
import {
argvIterator,
DOUBLE_DASH,
} from './argv-iterator';

/**
type-flag: typed argv parser
Expand Down Expand Up @@ -46,130 +42,55 @@ export const typeFlag = <Schemas extends Flags>(
} = {},
) => {
const { ignoreUnknown } = options;
const aliasesMap = mapAliases(schemas);
const parsed: ParsedFlags<Record<string, unknown>> = {
flags: createFlagsObject(schemas),
unknownFlags: {},
_: Object.assign([], {
[DOUBLE_DASH]: [],
}),
};

let setValueOnPreviousFlag: undefined | ((value?: string | boolean) => void);

const setKnown = (
flagName: string,
flagSchema: FlagTypeOrSchema,
flagValue: any,
) => {
const [flagType] = parseFlagType(flagSchema);

flagValue = normalizeBoolean(flagType, flagValue);

setValueOnPreviousFlag = (value) => {
const parsedValue = applyParser(flagType, value || '');
const flagsArray = parsed.flags[flagName];
if (Array.isArray(flagsArray)) {
flagsArray.push(parsedValue);
const [flagRegistry, flags] = createRegistry(schemas);
const unknownFlags: ParsedFlags['unknownFlags'] = {};
const _ = [] as unknown as ParsedFlags['_'];
_[DOUBLE_DASH] = [];

argvIterator(argv, {
onFlag(name, explicitValue, index) {
if (hasOwn(flagRegistry, name)) {
const [parser, values] = flagRegistry[name];
const flagValue = normalizeBoolean(parser, explicitValue);
const getFollowingValue = (value?: string | boolean) => {
values.push(
applyParser(parser, value || ''),
);
};

return (
flagValue === undefined
? getFollowingValue
: getFollowingValue(flagValue)
);
} if (ignoreUnknown) {
_.push(argv[index]);
} else {
parsed.flags[flagName] = parsedValue;
}

setValueOnPreviousFlag = undefined;
};

if (flagValue !== undefined) {
setValueOnPreviousFlag(flagValue);
}
};

const setUnknown = (
flagName: string,
flagValue: any,
) => {
if (!hasOwn(parsed.unknownFlags, flagName)) {
parsed.unknownFlags[flagName] = [];
}

if (flagValue === undefined) {
flagValue = true;
}

parsed.unknownFlags[flagName].push(flagValue);
};

for (let i = 0; i < argv.length; i += 1) {
const argvElement = argv[i];

if (argvElement === DOUBLE_DASH) {
const remainingArgs = argv.slice(i + 1);
parsed._[DOUBLE_DASH] = remainingArgs;
parsed._.push(...remainingArgs);
break;
}

const parsedFlag = parseFlagArgv(argvElement);
if (parsedFlag) {
if (setValueOnPreviousFlag) {
setValueOnPreviousFlag();
}

const [flagName, flagValue, isAlias] = parsedFlag;

if (isAlias) {
for (let j = 0; j < flagName.length; j += 1) {
const alias = flagName[j];
const hasAlias = getOwn(aliasesMap, alias);
const isLastAlias = j === flagName.length - 1;

if (hasAlias) {
setKnown(
hasAlias.name,
hasAlias.schema,
isLastAlias ? flagValue : true,
);
} else if (ignoreUnknown) {
parsed._.push(argvElement);
} else {
setUnknown(alias, isLastAlias ? flagValue : true);
}
}
continue;
}

let name = flagName;
let flagSchema = getOwn(schemas, flagName);
if (!flagSchema) {
const camelized = kebabToCamel(flagName);
flagSchema = getOwn(schemas, camelized);

if (flagSchema) {
name = camelized;
if (!hasOwn(unknownFlags, name)) {
unknownFlags[name] = [];
}
}

if (flagSchema) {
setKnown(name, flagSchema, flagValue);
} else if (ignoreUnknown) {
parsed._.push(argvElement);
} else {
setUnknown(flagName, flagValue);
unknownFlags[name].push(
explicitValue === undefined ? true : explicitValue,
);
}
} else if (setValueOnPreviousFlag) { // Not a flag, but expecting a value
setValueOnPreviousFlag(argvElement);
} else { // Unexpected value
parsed._.push(argvElement);
}
}
},

if (setValueOnPreviousFlag) {
setValueOnPreviousFlag();
}
onArgument(args, _index, isEoF) {
_.push(...args);

setDefaultFlagValues(schemas, parsed.flags);
if (isEoF) {
_[DOUBLE_DASH] = args;
}
},
});

type Result = TypeFlag<Schemas>;
return parsed as {
return {
flags,
unknownFlags,
_,
} as {
// This exposes the content of "TypeFlag<T>" in type hints
[Key in keyof Result]: Result[Key];
};
Expand Down
6 changes: 4 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DOUBLE_DASH } from './utils';
import type { DOUBLE_DASH } from './argv-iterator';

export type TypeFunction<ReturnType = any> = (value: any) => ReturnType;

Expand Down Expand Up @@ -93,7 +93,9 @@ export type InferFlagType<

export type ParsedFlags<Schemas = Record<string, unknown>> = {
flags: Schemas;
unknownFlags: Record<string, (string | boolean)[]>;
unknownFlags: {
[flagName: string]: (string | boolean)[];
};
_: string[] & {
[DOUBLE_DASH]: string[];
};
Expand Down
Loading