Skip to content

Commit

Permalink
refactor: use argv-iterator (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber committed Oct 31, 2022
1 parent 2baa4ce commit 9d2fe79
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 288 deletions.
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

0 comments on commit 9d2fe79

Please sign in to comment.